Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/auth/session-store.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import("node:fs/promises")>();
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);
});
});
35 changes: 25 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -4650,27 +4651,27 @@
let updates: Record<string, unknown> = {};

if (options.fromJson) {
const { readFileSync } = await import("node:fs");
updates = JSON.parse(readFileSync(options.fromJson, "utf-8")) as Record<string, unknown>;
const { readFile } = await import("node:fs/promises");
updates = JSON.parse(await readFile(options.fromJson, "utf-8")) as Record<string, unknown>;
} 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<string, unknown>;
updates = yaml.load(await readFile(options.fromYaml, "utf-8")) as Record<string, unknown>;
} else {
if (options.name) updates.name = options.name;

Check failure on line 4661 in src/cli.ts

View workflow job for this annotation

GitHub Actions / Index Signature Strictness

Property 'name' comes from an index signature, so it must be accessed with ['name'].
if (options.description) updates.description = options.description;

Check failure on line 4662 in src/cli.ts

View workflow job for this annotation

GitHub Actions / Index Signature Strictness

Property 'description' comes from an index signature, so it must be accessed with ['description'].
if (options.heroText) updates.hero_text = options.heroText;

Check failure on line 4663 in src/cli.ts

View workflow job for this annotation

GitHub Actions / Index Signature Strictness

Property 'hero_text' comes from an index signature, so it must be accessed with ['hero_text'].
if (options.logoUrl) updates.logo_url = options.logoUrl;

Check failure on line 4664 in src/cli.ts

View workflow job for this annotation

GitHub Actions / Index Signature Strictness

Property 'logo_url' comes from an index signature, so it must be accessed with ['logo_url'].
if (options.faviconUrl) updates.favicon_url = options.faviconUrl;

Check failure on line 4665 in src/cli.ts

View workflow job for this annotation

GitHub Actions / Index Signature Strictness

Property 'favicon_url' comes from an index signature, so it must be accessed with ['favicon_url'].
if (
options.primaryColor ||
options.secondaryColor ||
options.backgroundColor ||
options.textColor
) {
updates.colors = {};

Check failure on line 4672 in src/cli.ts

View workflow job for this annotation

GitHub Actions / Index Signature Strictness

Property 'colors' comes from an index signature, so it must be accessed with ['colors'].
if (options.primaryColor)
(updates.colors as Record<string, string>).primary = options.primaryColor;

Check failure on line 4674 in src/cli.ts

View workflow job for this annotation

GitHub Actions / Index Signature Strictness

Property 'colors' comes from an index signature, so it must be accessed with ['colors'].
if (options.secondaryColor)
(updates.colors as Record<string, string>).secondary = options.secondaryColor;
if (options.backgroundColor)
Expand Down Expand Up @@ -7431,8 +7432,22 @@
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;
});
}
4 changes: 2 additions & 2 deletions src/frontier-coverage/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
11 changes: 8 additions & 3 deletions src/frontier-coverage/drift.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { FRONTIER_COVERAGE_MATRIX } from "./matrix.js";
import type { CoverageCapability, CoverageMatrix } from "./schema.js";

const DIAGNOSTIC_STATUSES = new Set<CoverageCapability["status"]>([
"probe-only",
"planning-only",
"manual-admin",
"unsupported",
]);

const BLOCKING_OFFICIAL_DOC_STATUSES = new Set<
FrontierDriftReport["officialDocs"][number]["status"]
>(["missing-snapshot", "stale", "changed", "unavailable"]);
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions src/frontier-coverage/evidence-capture.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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]";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -461,7 +463,7 @@ function minimizeHeaders(
const kept: Record<string, unknown> = {};
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;
Expand Down
14 changes: 14 additions & 0 deletions src/frontier-coverage/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,20 @@ export function getCoverageMatrix(): CoverageMatrix {
return FRONTIER_COVERAGE_MATRIX;
}

const capabilityCache = new WeakMap<CoverageMatrix, Map<string, CoverageCapability>>();

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);
}
Expand Down
116 changes: 116 additions & 0 deletions src/parser/schema.test.ts
Original file line number Diff line number Diff line change
@@ -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"]);
});
});
Loading
Loading