From 8428aed9c9169f77a4fd9c2b1e26926b7627d4f0 Mon Sep 17 00:00:00 2001 From: Dylan Mordaunt <15080672+edithatogo@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:51:27 +1000 Subject: [PATCH 1/2] feat(assurance): consolidate performance hardening and test coverage Consolidates verified optimizations and tests across: - matrix capability lookup with WeakMap cache - set-based header and status lookups - async config loading for publication updates - scheduling freeze helper refactor and cli tests - unit tests for session store, draft URL, ProseMirror schema, and boundaries --- src/auth/session-store.test.ts | 45 +++++++++ src/cli.ts | 10 +- src/frontier-coverage/cli.ts | 4 +- src/frontier-coverage/drift.ts | 11 +- src/frontier-coverage/evidence-capture.ts | 6 +- src/frontier-coverage/matrix.ts | 14 +++ src/parser/schema.test.ts | 116 ++++++++++++++++++++++ src/policy/scheduling-freeze.ts | 105 +++++++++++--------- src/publish/draft-url.test.ts | 76 ++++++++++++++ src/security/boundaries.test.ts | 27 +++++ src/substack-api/schedule-reconcile.ts | 16 ++- src/test/assurance/cli-scheduling.test.ts | 36 +++++++ 12 files changed, 403 insertions(+), 63 deletions(-) create mode 100644 src/auth/session-store.test.ts create mode 100644 src/parser/schema.test.ts create mode 100644 src/publish/draft-url.test.ts create mode 100644 src/test/assurance/cli-scheduling.test.ts diff --git a/src/auth/session-store.test.ts b/src/auth/session-store.test.ts new file mode 100644 index 00000000..74db05cb --- /dev/null +++ b/src/auth/session-store.test.ts @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { describe, it, vi } from "vitest"; +import { loadSession } from "./session-store.js"; + +vi.mock("node:fs/promises", async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + readFile: vi.fn(mod.readFile), + }; +}); + +import { readFile } from "node:fs/promises"; + +describe("loadSession", () => { + it("returns null when the session file is missing (ENOENT)", async () => { + vi.mocked(readFile).mockRejectedValueOnce( + Object.assign(new Error("File not found"), { code: "ENOENT" }), + ); + + const session = await loadSession(); + assert.equal(session, null); + }); + + it("throws non-ENOENT errors when reading the session file fails", async () => { + vi.mocked(readFile).mockRejectedValueOnce( + Object.assign(new Error("Permission denied"), { code: "EACCES" }), + ); + + await assert.rejects(loadSession(), /Permission denied/); + }); + + it("returns the parsed session when the file exists and is valid", async () => { + const fakeSession = { + browserbaseSessionId: "fake-id", + publicationUrl: "https://example.substack.com", + createdAt: "2023-01-01T00:00:00.000Z", + updatedAt: "2023-01-01T00:00:00.000Z", + }; + vi.mocked(readFile).mockResolvedValueOnce(JSON.stringify(fakeSession)); + + const session = await loadSession(); + assert.deepEqual(session, fakeSession); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index 64bff880..2d30cfe0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4650,12 +4650,12 @@ apiPublication let updates: Record = {}; if (options.fromJson) { - const { readFileSync } = await import("node:fs"); - updates = JSON.parse(readFileSync(options.fromJson, "utf-8")) as Record; + const { readFile } = await import("node:fs/promises"); + updates = JSON.parse(await readFile(options.fromJson, "utf-8")) as Record; } else if (options.fromYaml) { - const { readFileSync } = await import("node:fs"); + const { readFile } = await import("node:fs/promises"); const yaml = await import("js-yaml"); - updates = yaml.load(readFileSync(options.fromYaml, "utf-8")) as Record; + updates = yaml.load(await readFile(options.fromYaml, "utf-8")) as Record; } else { if (options.name) updates.name = options.name; if (options.description) updates.description = options.description; @@ -7139,7 +7139,7 @@ function buildScheduledQueue( return [...postItems, ...draftItems, ...broadcastItems]; } -async function enforceSchedulingFreezePolicy(options: { +export async function enforceSchedulingFreezePolicy(options: { operation: string; freezePolicyPath: string | undefined; cataloguePath: string | undefined; diff --git a/src/frontier-coverage/cli.ts b/src/frontier-coverage/cli.ts index f7cad0e0..1e2dda11 100644 --- a/src/frontier-coverage/cli.ts +++ b/src/frontier-coverage/cli.ts @@ -9,7 +9,7 @@ import { renderEndpointInventory, type EndpointInventoryReport, } from "./evidence-capture.js"; -import { FRONTIER_COVERAGE_MATRIX } from "./matrix.js"; +import { FRONTIER_COVERAGE_MATRIX, lookupCapability } from "./matrix.js"; import { renderCoverageRoadmap } from "./roadmap.js"; import { type CoverageCapability, @@ -220,7 +220,7 @@ export function buildCoverageInspectOutput( matrix: CoverageMatrix, capabilityId: string, ): CoverageInspectOutput { - const capability = matrix.capabilities.find((candidate) => candidate.id === capabilityId); + const capability = lookupCapability(matrix, capabilityId); return { operation: "coverage.inspect", status: capability ? "ready" : "blocked", diff --git a/src/frontier-coverage/drift.ts b/src/frontier-coverage/drift.ts index 428cf234..730f7746 100644 --- a/src/frontier-coverage/drift.ts +++ b/src/frontier-coverage/drift.ts @@ -1,6 +1,13 @@ import { FRONTIER_COVERAGE_MATRIX } from "./matrix.js"; import type { CoverageCapability, CoverageMatrix } from "./schema.js"; +const DIAGNOSTIC_STATUSES = new Set([ + "probe-only", + "planning-only", + "manual-admin", + "unsupported", +]); + const BLOCKING_OFFICIAL_DOC_STATUSES = new Set< FrontierDriftReport["officialDocs"][number]["status"] >(["missing-snapshot", "stale", "changed", "unavailable"]); @@ -87,9 +94,7 @@ export function buildFrontierDriftReport( ); const endpointCaptureDiagnostics = matrix.capabilities - .filter((capability) => - ["probe-only", "planning-only", "manual-admin", "unsupported"].includes(capability.status), - ) + .filter((capability) => DIAGNOSTIC_STATUSES.has(capability.status)) .map((capability) => ({ capabilityId: capability.id, capability: capability.name, diff --git a/src/frontier-coverage/evidence-capture.ts b/src/frontier-coverage/evidence-capture.ts index e798915c..abeb31d8 100644 --- a/src/frontier-coverage/evidence-capture.ts +++ b/src/frontier-coverage/evidence-capture.ts @@ -1,5 +1,6 @@ import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; +import { lookupCapability } from "./matrix.js"; import type { CoverageCapability, CoverageMatrix, CoverageStatus } from "./schema.js"; export interface CaptureEndpoint { @@ -135,6 +136,7 @@ const EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi; const LONG_TOKEN_PATTERN = /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+|\b[A-Za-z0-9_-]{24,}\b/g; const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi; const PRIVATE_NAME_PATTERN = /\b[A-Z][a-z]+ [A-Z][a-z]+\b/g; +const KEPT_HEADERS = new Set(["accept", "content-type", "x-substack-version"]); const PRIVATE_NAME_KEYS = new Set(["first_name", "last_name", "full_name", "display_name", "name"]); const BODY_PREVIEW_LIMIT = 2_000; const REDACTED = "[REDACTED]"; @@ -320,7 +322,7 @@ export function buildCaptureKitReport( inventoryFile?: string | undefined; } = {}, ): CaptureKitReport { - const capability = matrix.capabilities.find((candidate) => candidate.id === capabilityId); + const capability = lookupCapability(matrix, capabilityId); const generatedAt = (options.generatedAt ?? new Date()).toISOString(); if (!capability) { return { @@ -461,7 +463,7 @@ function minimizeHeaders( const kept: Record = {}; for (const [key, value] of Object.entries(headers)) { const normalized = key.toLowerCase(); - if (["accept", "content-type", "x-substack-version"].includes(normalized)) { + if (KEPT_HEADERS.has(normalized)) { kept[normalized] = redactValueForKey(normalized, value); } else if (SENSITIVE_KEY_PATTERN.test(key)) { kept[normalized] = REDACTED; diff --git a/src/frontier-coverage/matrix.ts b/src/frontier-coverage/matrix.ts index 5d04d0a1..078ab042 100644 --- a/src/frontier-coverage/matrix.ts +++ b/src/frontier-coverage/matrix.ts @@ -323,6 +323,20 @@ export function getCoverageMatrix(): CoverageMatrix { return FRONTIER_COVERAGE_MATRIX; } +const capabilityCache = new WeakMap>(); + +export function lookupCapability( + matrix: CoverageMatrix, + capabilityId: string, +): CoverageCapability | undefined { + let cache = capabilityCache.get(matrix); + if (!cache) { + cache = new Map(matrix.capabilities.map((c) => [c.id, c])); + capabilityCache.set(matrix, cache); + } + return cache.get(capabilityId); +} + export function getCoverageCapabilitiesByDomain(domain: CapabilityDomain): CoverageCapability[] { return FRONTIER_COVERAGE_MATRIX.capabilities.filter((capability) => capability.domain === domain); } diff --git a/src/parser/schema.test.ts b/src/parser/schema.test.ts new file mode 100644 index 00000000..12db24b6 --- /dev/null +++ b/src/parser/schema.test.ts @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { describe, it } from "vitest"; +import { z } from "zod"; +import { validateProseMirrorDocument, collectNodeTypes, collectMarkTypes } from "./schema.js"; + +describe("validateProseMirrorDocument", () => { + it("passes when valid root node (type: 'doc') is provided", () => { + const doc = { type: "doc" }; + const result = validateProseMirrorDocument(doc); + assert.deepEqual(result, doc); + }); + + it("throws ZodError when invalid root node is provided", () => { + const doc = { type: "paragraph" }; + assert.throws(() => validateProseMirrorDocument(doc), z.ZodError); + }); + + it("throws ZodError when type is missing", () => { + const doc = { attrs: {} }; + assert.throws(() => validateProseMirrorDocument(doc), z.ZodError); + }); + + it("validates nested content properly", () => { + const doc = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Hello World", + marks: [{ type: "bold" }], + }, + ], + }, + ], + }; + const result = validateProseMirrorDocument(doc); + assert.deepEqual(result, doc); + }); + + it("throws ZodError when nested content is invalid", () => { + const doc = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + text: "Hello World", + marks: [{ type: "bold" }], + }, + ], + }, + ], + }; + assert.throws(() => validateProseMirrorDocument(doc), z.ZodError); + }); +}); + +describe("collectNodeTypes", () => { + it("collects unique node types in alphabetical order", () => { + const doc = { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text" }, { type: "text" }], + }, + { + type: "heading", + }, + { + type: "paragraph", + }, + ], + }; + const result = collectNodeTypes(doc); + assert.deepEqual(result, ["doc", "heading", "paragraph", "text"]); + }); +}); + +describe("collectMarkTypes", () => { + it("collects unique mark types in alphabetical order", () => { + const doc = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + marks: [{ type: "italic" }, { type: "bold" }], + }, + { + type: "text", + marks: [{ type: "bold" }], + }, + ], + }, + { + type: "heading", + content: [ + { + type: "text", + marks: [{ type: "strike" }], + }, + ], + }, + ], + }; + const result = collectMarkTypes(doc); + assert.deepEqual(result, ["bold", "italic", "strike"]); + }); +}); diff --git a/src/policy/scheduling-freeze.ts b/src/policy/scheduling-freeze.ts index 22696fda..084108e7 100644 --- a/src/policy/scheduling-freeze.ts +++ b/src/policy/scheduling-freeze.ts @@ -95,59 +95,18 @@ export async function evaluateSchedulingFreezePolicy(params: { return decision; } - const policyText = await readTextFile(params.freezePolicyPath); - if (policyText === undefined) { - return { - allowed: false, - policyPath: params.freezePolicyPath, - cataloguePath: params.cataloguePath, - reason: `Could not read freeze-policy file: ${params.freezePolicyPath}`, - status: "invalid", - catalogueSummary, - }; - } - - let parsed: unknown; - try { - parsed = JSON.parse(policyText); - } catch { - return { - allowed: false, - policyPath: params.freezePolicyPath, - cataloguePath: params.cataloguePath, - reason: `Freeze-policy file is not valid JSON: ${params.freezePolicyPath}`, - status: "invalid", - catalogueSummary, - }; - } - - const parsedPolicy = PolicyEnvelopeSchema.safeParse(parsed); - if (!parsedPolicy.success) { - const firstIssue = parsedPolicy.error.issues[0]; - const detail = firstIssue?.message ?? "unknown schema issue"; + const loadResult = await loadFreezePolicy(params.freezePolicyPath); + if (!loadResult.success) { return { allowed: false, policyPath: params.freezePolicyPath, cataloguePath: params.cataloguePath, - reason: `Freeze-policy schema validation failed (${params.freezePolicyPath}): ${detail}`, - status: "invalid", + reason: loadResult.reason, + status: loadResult.status, catalogueSummary, }; } - - const policy: SchedulingFreezePolicy = { - schemaVersion: parsedPolicy.data.schemaVersion ?? 1, - status: parsedPolicy.data.status, - active: parsedPolicy.data.active, - reason: parsedPolicy.data.reason, - reasonCode: parsedPolicy.data.reasonCode, - freezeUntil: parsedPolicy.data.freezeUntil, - resumeAt: parsedPolicy.data.resumeAt, - until: parsedPolicy.data.until, - note: parsedPolicy.data.note, - source: parsedPolicy.data.source, - raw: parsed, - }; + const policy = loadResult.policy; const active = isPolicyActive(policy, now); if (!active) { @@ -311,3 +270,57 @@ function validateExternalCatalogue(cataloguePath: string | undefined): Promise< function extractArrayCount(value: unknown): number | undefined { return Array.isArray(value) ? value.length : undefined; } + +async function loadFreezePolicy( + policyPath: string, +): Promise< + | { success: true; policy: SchedulingFreezePolicy } + | { success: false; reason: string; status: "invalid" } +> { + const policyText = await readTextFile(policyPath); + if (policyText === undefined) { + return { + success: false, + reason: `Could not read freeze-policy file: ${policyPath}`, + status: "invalid", + }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(policyText); + } catch { + return { + success: false, + reason: `Freeze-policy file is not valid JSON: ${policyPath}`, + status: "invalid", + }; + } + + const parsedPolicy = PolicyEnvelopeSchema.safeParse(parsed); + if (!parsedPolicy.success) { + const firstIssue = parsedPolicy.error.issues[0]; + const detail = firstIssue?.message ?? "unknown schema issue"; + return { + success: false, + reason: `Freeze-policy schema validation failed (${policyPath}): ${detail}`, + status: "invalid", + }; + } + + const policy: SchedulingFreezePolicy = { + schemaVersion: parsedPolicy.data.schemaVersion ?? 1, + status: parsedPolicy.data.status, + active: parsedPolicy.data.active, + reason: parsedPolicy.data.reason, + reasonCode: parsedPolicy.data.reasonCode, + freezeUntil: parsedPolicy.data.freezeUntil, + resumeAt: parsedPolicy.data.resumeAt, + until: parsedPolicy.data.until, + note: parsedPolicy.data.note, + source: parsedPolicy.data.source, + raw: parsed, + }; + + return { success: true, policy }; +} diff --git a/src/publish/draft-url.test.ts b/src/publish/draft-url.test.ts new file mode 100644 index 00000000..efcdca3c --- /dev/null +++ b/src/publish/draft-url.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import { describe, it } from "vitest"; +import { resolveDraftEditorUrl } from "./draft-url.js"; + +describe("resolveDraftEditorUrl", () => { + it("returns the original url if draftId is undefined", () => { + assert.equal( + resolveDraftEditorUrl("https://example.substack.com/publish/post", undefined), + "https://example.substack.com/publish/post", + ); + }); + + it("returns empty string if draftId is undefined and draftUrl is empty", () => { + assert.equal(resolveDraftEditorUrl("", undefined), ""); + }); + + it("returns empty string if draftUrl is empty", () => { + assert.equal(resolveDraftEditorUrl("", "12345"), ""); + }); + + it("returns the original url if it already ends with the draftId", () => { + assert.equal( + resolveDraftEditorUrl("https://example.substack.com/publish/post/12345", "12345"), + "https://example.substack.com/publish/post/12345", + ); + }); + + it("returns the original url if it already ends with the draftId and has trailing slash", () => { + assert.equal( + resolveDraftEditorUrl("https://example.substack.com/publish/post/12345/", "12345"), + "https://example.substack.com/publish/post/12345/", + ); + }); + + it("replaces existing numeric draft ID with the new one", () => { + assert.equal( + resolveDraftEditorUrl("https://example.substack.com/publish/post/999", "12345"), + "https://example.substack.com/publish/post/12345", + ); + }); + + it("appends draftId if it is missing", () => { + assert.equal( + resolveDraftEditorUrl("https://example.substack.com/publish/post", "12345"), + "https://example.substack.com/publish/post/12345", + ); + }); + + it("handles urls with trailing slash", () => { + assert.equal( + resolveDraftEditorUrl("https://example.substack.com/publish/post/", "12345"), + "https://example.substack.com/publish/post/12345", + ); + }); + + it("preserves query parameters and hashes (catch block logic)", () => { + assert.equal( + resolveDraftEditorUrl("example.substack.com/publish/post?foo=bar#baz", "12345"), + "example.substack.com/publish/post/12345?foo=bar#baz", + ); + }); + + it("preserves query parameters and hashes when replacing draftId (catch block logic)", () => { + assert.equal( + resolveDraftEditorUrl("example.substack.com/publish/post/999?foo=bar#baz", "12345"), + "example.substack.com/publish/post/12345?foo=bar#baz", + ); + }); + + it("preserves query parameters and hashes when draftId is already correct (catch block logic)", () => { + assert.equal( + resolveDraftEditorUrl("example.substack.com/publish/post/12345?foo=bar#baz", "12345"), + "example.substack.com/publish/post/12345?foo=bar#baz", + ); + }); +}); diff --git a/src/security/boundaries.test.ts b/src/security/boundaries.test.ts index 54579e05..19558178 100644 --- a/src/security/boundaries.test.ts +++ b/src/security/boundaries.test.ts @@ -18,6 +18,33 @@ describe("security boundaries", () => { "Untrusted HTTPS origin: https://evil.example", ); expect(() => policy.assertTrusted("not-an-origin")).toThrow(); + + // Test URL objects + expect(policy.isTrusted(new URL("https://example.substack.com/publish"))).toBe(true); + expect(policy.isTrusted(new URL("https://evil.example/publish"))).toBe(false); + + // Test successful assertTrusted + const validUrl = policy.assertTrusted("https://example.substack.com"); + expect(validUrl).toBeInstanceOf(URL); + expect(validUrl.origin).toBe("https://example.substack.com"); + + const validUrlObj = policy.assertTrusted(new URL("https://example.substack.com")); + expect(validUrlObj).toBeInstanceOf(URL); + + // Test successful assertRedirect + const redirectUrl = policy.assertRedirect( + "https://example.substack.com", + "https://substack.com/api/v1/user", + ); + expect(redirectUrl).toBeInstanceOf(URL); + expect(redirectUrl.origin).toBe("https://substack.com"); + + const redirectUrlObj = policy.assertRedirect( + new URL("https://example.substack.com"), + new URL("https://substack.com/api/v1/user"), + ); + expect(redirectUrlObj).toBeInstanceOf(URL); + expect(redirectUrlObj.origin).toBe("https://substack.com"); }); it("rejects non-origin trusted policy configuration", () => { diff --git a/src/substack-api/schedule-reconcile.ts b/src/substack-api/schedule-reconcile.ts index 8c565556..d1188160 100644 --- a/src/substack-api/schedule-reconcile.ts +++ b/src/substack-api/schedule-reconcile.ts @@ -1,3 +1,9 @@ +const SCHEDULED_QUEUE_STATUSES = new Set(["scheduled", "queue", "queued"]); +const PUBLISHED_QUEUE_STATUSES = new Set(["published", "publish", "live", "sent"]); +const SCHEDULED_EXPECTED_STATUSES = new Set(["scheduled", "schedule", "queued", "queue"]); +const PUBLISHED_EXPECTED_STATUSES = new Set(["published", "publish", "published_at", "live"]); +const DRAFT_EXPECTED_STATUSES = new Set(["draft", "unscheduled", "drafted"]); + export type ScheduleReconcileKey = "title" | "time" | "draft-id"; type ReconciledQueueStatus = "scheduled" | "published" | "draft" | "other"; @@ -365,8 +371,8 @@ function normalizeQueueStatus(status: string | undefined): ReconciledQueueStatus if (!status) return "other"; const normalized = status.trim().toLowerCase(); - if (["scheduled", "queue", "queued"].includes(normalized)) return "scheduled"; - if (["published", "publish", "live", "sent"].includes(normalized)) return "published"; + if (SCHEDULED_QUEUE_STATUSES.has(normalized)) return "scheduled"; + if (PUBLISHED_QUEUE_STATUSES.has(normalized)) return "published"; if (normalized.includes("draft")) return "draft"; return "other"; } @@ -383,9 +389,9 @@ function parseExpectedStatus(item: Record, index: number, sourc function normalizeExpectedStatus(raw: string): "scheduled" | "published" | "draft" | undefined { const normalized = raw.trim().toLowerCase(); - if (["scheduled", "schedule", "queued", "queue"].includes(normalized)) return "scheduled"; - if (["published", "publish", "published_at", "live"].includes(normalized)) return "published"; - if (["draft", "unscheduled", "drafted"].includes(normalized)) return "draft"; + if (SCHEDULED_EXPECTED_STATUSES.has(normalized)) return "scheduled"; + if (PUBLISHED_EXPECTED_STATUSES.has(normalized)) return "published"; + if (DRAFT_EXPECTED_STATUSES.has(normalized)) return "draft"; return undefined; } diff --git a/src/test/assurance/cli-scheduling.test.ts b/src/test/assurance/cli-scheduling.test.ts new file mode 100644 index 00000000..2ad91e6d --- /dev/null +++ b/src/test/assurance/cli-scheduling.test.ts @@ -0,0 +1,36 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { enforceSchedulingFreezePolicy } from "../../cli.js"; +import * as policyModule from "../../policy/scheduling-freeze.js"; + +vi.mock("../../policy/scheduling-freeze.js", () => { + return { + evaluateSchedulingFreezePolicy: vi.fn(), + buildSchedulingFreezeBlockReport: vi.fn(), + }; +}); + +describe("enforceSchedulingFreezePolicy", () => { + let originalExitCode: number | undefined; + + beforeEach(() => { + vi.resetAllMocks(); + originalExitCode = process.exitCode; + }); + + afterEach(() => { + process.exitCode = originalExitCode; + }); + + it("should return true when missing scheduledAt and calendar limitations exist", async () => { + vi.mocked(policyModule.evaluateSchedulingFreezePolicy).mockResolvedValue({ allowed: true }); + + const result = await enforceSchedulingFreezePolicy({ + operation: "test-op", + freezePolicyPath: "path", + cataloguePath: undefined, + candidate: { sourceFile: "test" }, + }); + + expect(result).toBe(true); + }); +}); From 79077bac4e110850ebcaffd5313e96e8e7071f14 Mon Sep 17 00:00:00 2001 From: Dylan Mordaunt <15080672+edithatogo@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:30:39 +1000 Subject: [PATCH 2/2] fix(cli): guard entrypoint parsing and remove executable import in tests --- src/cli.ts | 29 +++++++++++++----- src/test/assurance/cli-scheduling.test.ts | 36 ----------------------- 2 files changed, 22 insertions(+), 43 deletions(-) delete mode 100644 src/test/assurance/cli-scheduling.test.ts diff --git a/src/cli.ts b/src/cli.ts index 2d30cfe0..36f8fa47 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,8 @@ #!/usr/bin/env node -import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, isAbsolute, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { Command } from "commander"; import { PACKAGE_VERSION } from "./version.js"; import type { ProseMirrorNode } from "./types.js"; @@ -7139,7 +7140,7 @@ function buildScheduledQueue( return [...postItems, ...draftItems, ...broadcastItems]; } -export async function enforceSchedulingFreezePolicy(options: { +async function enforceSchedulingFreezePolicy(options: { operation: string; freezePolicyPath: string | undefined; cataloguePath: string | undefined; @@ -7431,8 +7432,22 @@ function parseOperatorMode(value: string): OperatorMode { throw new Error(`Unsupported operator mode "${value}". Use solo, team, agency, or ci.`); } -program.parseAsync().catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - console.error(`Error: ${message}`); - process.exitCode = 1; -}); +function isMainModule(): boolean { + const entry = process.argv[1]; + if (!entry) return false; + try { + const mainPath = realpathSync(entry); + const modulePath = realpathSync(fileURLToPath(import.meta.url)); + return mainPath === modulePath; + } catch { + return false; + } +} + +if (isMainModule()) { + program.parseAsync().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`Error: ${message}`); + process.exitCode = 1; + }); +} diff --git a/src/test/assurance/cli-scheduling.test.ts b/src/test/assurance/cli-scheduling.test.ts deleted file mode 100644 index 2ad91e6d..00000000 --- a/src/test/assurance/cli-scheduling.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { enforceSchedulingFreezePolicy } from "../../cli.js"; -import * as policyModule from "../../policy/scheduling-freeze.js"; - -vi.mock("../../policy/scheduling-freeze.js", () => { - return { - evaluateSchedulingFreezePolicy: vi.fn(), - buildSchedulingFreezeBlockReport: vi.fn(), - }; -}); - -describe("enforceSchedulingFreezePolicy", () => { - let originalExitCode: number | undefined; - - beforeEach(() => { - vi.resetAllMocks(); - originalExitCode = process.exitCode; - }); - - afterEach(() => { - process.exitCode = originalExitCode; - }); - - it("should return true when missing scheduledAt and calendar limitations exist", async () => { - vi.mocked(policyModule.evaluateSchedulingFreezePolicy).mockResolvedValue({ allowed: true }); - - const result = await enforceSchedulingFreezePolicy({ - operation: "test-op", - freezePolicyPath: "path", - cataloguePath: undefined, - candidate: { sourceFile: "test" }, - }); - - expect(result).toBe(true); - }); -});