From 105c4c6e7c995b9e540e1fad5c59b0b834947c85 Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 11 May 2026 22:43:16 +0000 Subject: [PATCH 1/2] fix(rf-b1hf): drop obsolete sub-docs mirror assertion in brief.test.ts rc-bc9 (1d43dd9) deleted node/.claude/skills/ entirely as a stale dev mirror of node/resources/skills/. tests/brief.test.ts:178-186 still asserted each RAFTER_SUBDOCS slug existed at the deleted mirror path, failing under the test-comprehensive test-node job since the rf-cfjc Verify build fix unblocked the suite from running. The canonical sub-doc paths under node/resources/skills/rafter/docs/ are already covered by the immediately-preceding 'every referenced sub-doc path resolves and is non-empty' test, so dropping the mirror assertion is a clean delete with no coverage loss. This was the last gate failure stopping v0.8.0 publish. Co-Authored-By: Claude Opus 4.7 --- node/tests/brief.test.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/node/tests/brief.test.ts b/node/tests/brief.test.ts index 69b47717..2625cf5d 100644 --- a/node/tests/brief.test.ts +++ b/node/tests/brief.test.ts @@ -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`, () => { From c12c080f4c8339db70140481722d75d4ff2882e5 Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 11 May 2026 23:48:08 +0000 Subject: [PATCH 2/2] test(cross-runtime-parity): update for wrapped JSON shape + fix stale brief topic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pre-existing failures in tests/cross-runtime-parity.test.ts that I incorrectly cleared as fixed when pushing rf-b1hf. Verified locally and on Node/Python this time. 1. **10 scanner tests assumed bare-array JSON shape**. They reach into `JSON.parse(stdout)[0].matches`, but the canonical shape since rf-0pch (2026-04-27, v0.7.7) is wrapped: `{_note, scan_mode, triage_applied, results: [...]}`. Tests were never updated; e2e-cli.test.ts and secret-scanning-e2e.test.ts had already moved to the wrapped shape — only this file was stale. Added a `parseResultsArray(stdout)` helper that handles both shapes (returns the inner array for wrapped, the array itself for legacy). Migrated all 10 tests in `parity: secrets` and `parity: secret pattern detection` to use it. 2. **`brief security` is not a topic** in either runtime. The test was added 2026-04-05 (commit a79dabe) and has never passed — both Node (Commander.process.exit(1)) and Python (typer.Exit(code=1)) emit "Unknown topic: security" because the topic registry has `scanning`, `commands`, `setup`, `setup/*`, `all`, plus the RAFTER_SUBDOCS slugs. Renamed the test to `brief scanning` (the canonical rafter-skill topic) and left a comment with the original-intent trail. This was the last set of failures blocking v0.8.0 publish. CI on PR #100 should now go fully green; the publish workflow will retry against the same package.json version (0.8.0) after the merge. Verified locally: - Both runtimes emit the wrapped shape (manually inspected stdout). - `node ./dist/index.js brief scanning` → exit 0. - `python3 -m rafter_cli brief scanning` → exit 0. - python tests/test_custom_patterns.py + test_agent_scan_history.py: 37 passed. Co-Authored-By: Claude Opus 4.7 --- node/tests/cross-runtime-parity.test.ts | 110 +++++++++++++----------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/node/tests/cross-runtime-parity.test.ts b/node/tests/cross-runtime-parity.test.ts index f2e7aed5..07efe4eb 100644 --- a/node/tests/cross-runtime-parity.test.ts +++ b/node/tests/cross-runtime-parity.test.ts @@ -90,6 +90,18 @@ function runBoth(args: string[], opts?: { cwd?: string; env?: Record { 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); @@ -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"); } }); @@ -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); }); @@ -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); }); @@ -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); }); @@ -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); }); @@ -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); }); }