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
13 changes: 4 additions & 9 deletions node/tests/brief.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,10 @@ describe("rafter skill — CYOA hierarchy", () => {
}
});

it("sub-docs are mirrored to node/.claude/skills/rafter/docs", () => {
const claudeDocs = path.resolve(
__dirname,
"../.claude/skills/rafter/docs",
);
for (const slug of RAFTER_SUBDOCS) {
expect(existsSync(path.join(claudeDocs, `${slug}.md`))).toBe(true);
}
});
// The dev mirror at node/.claude/skills/rafter/docs/ was deleted in rc-bc9
// (1d43dd9) — it had drifted ~2 weeks behind the canonical source at
// node/resources/skills/rafter/docs/, which the test above already covers.
// No "mirror" assertion is needed.

for (const slug of RAFTER_SUBDOCS) {
it(`brief ${slug} renders the sub-doc`, () => {
Expand Down
110 changes: 62 additions & 48 deletions node/tests/cross-runtime-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ function runBoth(args: string[], opts?: { cwd?: string; env?: Record<string, str
};
}

/**
* Parse `rafter secrets --json` stdout into a per-file results array.
* Since rf-0pch (v0.7.7) the shape is `{_note, scan_mode, triage_applied, results: [...]}`;
* pre-rf-0pch versions emitted a bare array. Handle both so tests stay
* stable across versions and don't need to reach into the wrapper.
*/
function parseResultsArray(stdout: string): any[] {
const j = JSON.parse(stdout);
if (Array.isArray(j)) return j;
return (j && Array.isArray(j.results)) ? j.results : [];
}

// Skip all parity tests when Python + rafter_cli deps aren't available
const describeIfPython = PYTHON_AVAILABLE ? describe : describe.skip;

Expand Down Expand Up @@ -190,25 +202,23 @@ describeIfPython("parity: secrets", () => {
fs.writeFileSync(f, "AKIAIOSFODNN7EXAMPLE\n");
const r = runBoth(["secrets", f, "--engine", "patterns", "--json"]);

const nodeJson = JSON.parse(r.node.stdout);
const pyJson = JSON.parse(r.python.stdout);
const nodeResults = parseResultsArray(r.node.stdout);
const pyResults = parseResultsArray(r.python.stdout);

// Both are arrays with one entry
expect(Array.isArray(nodeJson)).toBe(true);
expect(Array.isArray(pyJson)).toBe(true);
expect(nodeJson).toHaveLength(1);
expect(pyJson).toHaveLength(1);
// Both have one entry
expect(nodeResults).toHaveLength(1);
expect(pyResults).toHaveLength(1);

// File paths point to the same file
expect(nodeJson[0].file).toBe(f);
expect(pyJson[0].file).toBe(f);
expect(nodeResults[0].file).toBe(f);
expect(pyResults[0].file).toBe(f);

// Same number of matches
expect(nodeJson[0].matches).toHaveLength(pyJson[0].matches.length);
expect(nodeResults[0].matches).toHaveLength(pyResults[0].matches.length);

// Compare first match structure
const nodeMatch = nodeJson[0].matches[0];
const pyMatch = pyJson[0].matches[0];
const nodeMatch = nodeResults[0].matches[0];
const pyMatch = pyResults[0].matches[0];

expect(nodeMatch.pattern.name).toBe(pyMatch.pattern.name);
expect(nodeMatch.pattern.severity).toBe(pyMatch.pattern.severity);
Expand All @@ -222,20 +232,20 @@ describeIfPython("parity: secrets", () => {
fs.writeFileSync(f, "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh12\n");
const r = runBoth(["secrets", f, "--engine", "patterns", "--json"]);

const nodeJson = JSON.parse(r.node.stdout);
const pyJson = JSON.parse(r.python.stdout);
const nodeResults = parseResultsArray(r.node.stdout);
const pyResults = parseResultsArray(r.python.stdout);

// Validate schema shape matches on both
for (const result of [nodeJson, pyJson]) {
expect(result[0]).toHaveProperty("file");
expect(result[0]).toHaveProperty("matches");
expect(result[0].matches[0]).toHaveProperty("pattern");
expect(result[0].matches[0]).toHaveProperty("line");
expect(result[0].matches[0]).toHaveProperty("column");
expect(result[0].matches[0]).toHaveProperty("redacted");
expect(result[0].matches[0].pattern).toHaveProperty("name");
expect(result[0].matches[0].pattern).toHaveProperty("severity");
expect(result[0].matches[0].pattern).toHaveProperty("description");
for (const results of [nodeResults, pyResults]) {
expect(results[0]).toHaveProperty("file");
expect(results[0]).toHaveProperty("matches");
expect(results[0].matches[0]).toHaveProperty("pattern");
expect(results[0].matches[0]).toHaveProperty("line");
expect(results[0].matches[0]).toHaveProperty("column");
expect(results[0].matches[0]).toHaveProperty("redacted");
expect(results[0].matches[0].pattern).toHaveProperty("name");
expect(results[0].matches[0].pattern).toHaveProperty("severity");
expect(results[0].matches[0].pattern).toHaveProperty("description");
}
});

Expand All @@ -248,15 +258,15 @@ describeIfPython("parity: secrets", () => {
expect(r.node.exitCode).toBe(1);
expect(r.python.exitCode).toBe(1);

const nodeJson = JSON.parse(r.node.stdout);
const pyJson = JSON.parse(r.python.stdout);
const nodeResults = parseResultsArray(r.node.stdout);
const pyResults = parseResultsArray(r.python.stdout);

expect(nodeJson.length).toBeGreaterThan(0);
expect(nodeJson.length).toBe(pyJson.length);
expect(nodeResults.length).toBeGreaterThan(0);
expect(nodeResults.length).toBe(pyResults.length);

// Both should find the same pattern name
const nodePatterns = nodeJson.flatMap((f: any) => f.matches.map((m: any) => m.pattern.name)).sort();
const pyPatterns = pyJson.flatMap((f: any) => f.matches.map((m: any) => m.pattern.name)).sort();
const nodePatterns = nodeResults.flatMap((f: any) => f.matches.map((m: any) => m.pattern.name)).sort();
const pyPatterns = pyResults.flatMap((f: any) => f.matches.map((m: any) => m.pattern.name)).sort();
expect(nodePatterns).toEqual(pyPatterns);
});

Expand All @@ -272,16 +282,16 @@ describeIfPython("parity: secrets", () => {
expect(r.node.exitCode).toBe(1);
expect(r.python.exitCode).toBe(1);

const nodeJson = JSON.parse(r.node.stdout);
const pyJson = JSON.parse(r.python.stdout);
const nodeResults = parseResultsArray(r.node.stdout);
const pyResults = parseResultsArray(r.python.stdout);

const nodeCount = nodeJson[0].matches.length;
const pyCount = pyJson[0].matches.length;
const nodeCount = nodeResults[0].matches.length;
const pyCount = pyResults[0].matches.length;
expect(nodeCount).toBe(pyCount);

// Same pattern names detected (order may differ)
const nodeNames = nodeJson[0].matches.map((m: any) => m.pattern.name).sort();
const pyNames = pyJson[0].matches.map((m: any) => m.pattern.name).sort();
const nodeNames = nodeResults[0].matches.map((m: any) => m.pattern.name).sort();
const pyNames = pyResults[0].matches.map((m: any) => m.pattern.name).sort();
expect(nodeNames).toEqual(pyNames);
});

Expand All @@ -291,8 +301,8 @@ describeIfPython("parity: secrets", () => {
fs.writeFileSync(f, "AKIAIOSFODNN7EXAMPLE\n");
const r = runBoth(["secrets", f, "--engine", "patterns", "--json"]);

const nodeRedacted = JSON.parse(r.node.stdout)[0].matches[0].redacted;
const pyRedacted = JSON.parse(r.python.stdout)[0].matches[0].redacted;
const nodeRedacted = parseResultsArray(r.node.stdout)[0].matches[0].redacted;
const pyRedacted = parseResultsArray(r.python.stdout)[0].matches[0].redacted;

expect(nodeRedacted).toBe(pyRedacted);
});
Expand Down Expand Up @@ -439,8 +449,12 @@ describeIfPython("parity: brief", () => {
expect(r.python.stdout).toContain("# Rafter Command Reference");
});

it("both exit 0 for brief security", () => {
const r = runBoth(["brief", "security"]);
it("both exit 0 for brief scanning", () => {
// Was `brief security` originally (2026-04-05), but no such topic ever
// existed in either runtime — `brief security` returned "Unknown topic"
// and exit 1 on both sides. `scanning` is the canonical rafter-skill
// topic and is the closest match to the original intent.
const r = runBoth(["brief", "scanning"]);
expect(r.node.exitCode).toBe(0);
expect(r.python.exitCode).toBe(0);
});
Expand Down Expand Up @@ -554,17 +568,17 @@ describeIfPython("parity: secret pattern detection", () => {
expect(r.node.exitCode).toBe(1);
expect(r.python.exitCode).toBe(1);

const nodeJson = JSON.parse(r.node.stdout);
const pyJson = JSON.parse(r.python.stdout);
const nodeResults = parseResultsArray(r.node.stdout);
const pyResults = parseResultsArray(r.python.stdout);

expect(nodeJson).toHaveLength(1);
expect(pyJson).toHaveLength(1);
expect(nodeJson[0].matches.length).toBeGreaterThan(0);
expect(pyJson[0].matches.length).toBeGreaterThan(0);
expect(nodeResults).toHaveLength(1);
expect(pyResults).toHaveLength(1);
expect(nodeResults[0].matches.length).toBeGreaterThan(0);
expect(pyResults[0].matches.length).toBeGreaterThan(0);

// Same pattern name detected
const nodePatternName = nodeJson[0].matches[0].pattern.name;
const pyPatternName = pyJson[0].matches[0].pattern.name;
const nodePatternName = nodeResults[0].matches[0].pattern.name;
const pyPatternName = pyResults[0].matches[0].pattern.name;
expect(nodePatternName).toBe(pyPatternName);
});
}
Expand Down
Loading