-
-
Notifications
You must be signed in to change notification settings - Fork 13.5k
test(harness): move JD-archive wiring out of test-all.mjs + catch rebound write APIs #4159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Zoubeir23
wants to merge
3
commits into
career-ops-hq:main
Choose a base branch
from
Zoubeir23:Feature/3935-move-jd-archive-wiring-out-of-test-all
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+202
−136
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
32a37c0
test(harness): move JD-archive wiring (section 75) out of test-all.mjs
Zoubeir23 6c37a86
test(jd-archive-wiring): catch a local re-binding of a write-capable …
Zoubeir23 e22ab93
fix(jd-archive-wiring): REBIND_RE catches a parenthesized initializer
Zoubeir23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| /** | ||
| * tests/jd-archive-wiring.test.mjs — JD-archive validator wiring + read-only | ||
| * boundary (#2789). | ||
| * | ||
| * check-jd-archive.mjs's own --self-test (invoked via test-all.mjs's CLI-check | ||
| * table) covers the finding logic on synthetic fixtures. This suite pins the | ||
| * wiring: the script ships, updates, is documented, the mode files state the | ||
| * archival step as required (not conditional), and the checker stays strictly | ||
| * read-only — it reports missing archives; it must never be able to "fix" one | ||
| * itself by writing a report or a jds/ file. | ||
| * | ||
| * Moved out of test-all.mjs (was section 75) per this repo's own path | ||
| * instructions (.coderabbit.yaml: new numbered sections belong in tests/ | ||
| * files, not the central harness) and its precedent (#3863 moved the | ||
| * context-budget suite the same way) — #3935. | ||
| * | ||
| * Run: node test-all.mjs --only jd-archive-wiring | ||
| */ | ||
|
|
||
| import { pass, fail, ROOT } from './helpers.mjs'; | ||
| import { readFileSync } from 'fs'; | ||
| import { join } from 'path'; | ||
|
|
||
| function readFile(path) { | ||
| return readFileSync(join(ROOT, path), 'utf-8'); | ||
| } | ||
|
|
||
| console.log('\nJD-archive validator wiring + read-only boundary (#2789)'); | ||
|
|
||
| try { | ||
| const jdArchiveSrc = readFile('check-jd-archive.mjs'); | ||
|
|
||
| const updaterSrc = readFile('update-system.mjs'); | ||
| const jdArchiveSysBlock = (updaterSrc.match(/SYSTEM_PATHS\s*=\s*\[([\s\S]*?)\]/) || [, ''])[1]; | ||
| if (jdArchiveSysBlock.includes("'check-jd-archive.mjs'")) { | ||
| pass('check-jd-archive.mjs is in update-system.mjs SYSTEM_PATHS (shipped + updatable)'); | ||
| } else { | ||
| fail('check-jd-archive.mjs is NOT in SYSTEM_PATHS — updates would never deliver it'); | ||
| } | ||
|
|
||
| const pkg = JSON.parse(readFile('package.json')); | ||
| if (pkg.scripts && pkg.scripts['jd-archive'] === 'node check-jd-archive.mjs') { | ||
| pass('package.json exposes npm run jd-archive'); | ||
| } else { | ||
| fail('package.json missing the jd-archive script entry'); | ||
| } | ||
|
|
||
| const scriptsDoc = readFile('docs/SCRIPTS.md'); | ||
| if (scriptsDoc.includes('## check-jd-archive') && scriptsDoc.includes('missing-jd-archive')) { | ||
| pass('docs/SCRIPTS.md documents check-jd-archive (section + finding type)'); | ||
| } else { | ||
| fail('docs/SCRIPTS.md missing the check-jd-archive section'); | ||
| } | ||
|
|
||
| const agentsDoc = readFile('AGENTS.md'); | ||
| if (agentsDoc.includes('`check-jd-archive.mjs`')) { | ||
| pass('AGENTS.md Main Files table lists check-jd-archive.mjs'); | ||
| } else { | ||
| fail('AGENTS.md Main Files table missing check-jd-archive.mjs'); | ||
| } | ||
| if (/REQUIRED.*Job Description \(archived verbatim\)|Job Description \(archived verbatim\).*REQUIRED/.test(agentsDoc)) { | ||
| pass('AGENTS.md states the JD-archive section as required, not conditional'); | ||
| } else { | ||
| fail('AGENTS.md does not state the JD-archive section as required'); | ||
| } | ||
|
|
||
| const ofertaDoc = readFile('modes/oferta.md'); | ||
| if (ofertaDoc.includes('## Job Description (archived verbatim)')) { | ||
| pass('modes/oferta.md report template carries a Job Description (archived verbatim) section'); | ||
| } else { | ||
| fail('modes/oferta.md report template missing the Job Description (archived verbatim) section'); | ||
| } | ||
| if (/JD archival \(required, #2789\)/.test(ofertaDoc)) { | ||
| pass('modes/oferta.md states JD archival as required (matches the Machine Summary "required" phrasing style)'); | ||
| } else { | ||
| fail('modes/oferta.md does not state JD archival as a required step'); | ||
| } | ||
|
|
||
| const pdfDoc = readFile('modes/pdf.md'); | ||
| if (!/write the JD to a scratch file[\s\S]{0,20}if it isn't already one/.test(pdfDoc)) { | ||
| pass('modes/pdf.md no longer phrases JD archival as conditional ("if it isn\'t already one")'); | ||
| } else { | ||
| fail('modes/pdf.md still phrases JD archival as conditional, not required'); | ||
| } | ||
| if (pdfDoc.includes('JD archival (required, #2789)')) { | ||
| pass('modes/pdf.md states JD archival as a required step'); | ||
| } else { | ||
| fail('modes/pdf.md does not state JD archival as a required step'); | ||
| } | ||
|
|
||
| // Read-only import boundary: the ONLY fs capabilities check-jd-archive.mjs | ||
| // may hold for scanning reports/jds are readFileSync/readdirSync/existsSync. | ||
| // It also imports mkdtempSync/mkdirSync/writeFileSync/rmSync — but ONLY for | ||
| // building its own self-test fixtures in a temp dir, never for reports/ or | ||
| // jds/. The boundary check below allows the self-test-fixture write APIs by | ||
| // name but asserts they never appear outside the self-test function body. | ||
| const SELF_TEST_ONLY_FS = new Set(['mkdtempSync', 'mkdirSync', 'writeFileSync', 'rmSync']); | ||
| const READ_ONLY_FS = new Set(['readFileSync', 'readdirSync', 'existsSync']); | ||
| const fsImportMatch = jdArchiveSrc.match(/import\s*\{([^}]*)\}\s*from\s*['"](?:node:)?fs['"]/); | ||
| const fsNames = fsImportMatch ? fsImportMatch[1].split(',').map(s => s.trim()).filter(Boolean) : []; | ||
| const unexpected = fsNames.filter(n => !READ_ONLY_FS.has(n) && !SELF_TEST_ONLY_FS.has(n)); | ||
| if (fsNames.length > 0 && unexpected.length === 0) { | ||
| pass('check-jd-archive.mjs fs imports are limited to read-only scanning APIs plus self-test-fixture builders'); | ||
| } else { | ||
| fail(`check-jd-archive.mjs fs import boundary violated: ${unexpected.join(', ') || 'no fs import matched'}`); | ||
| } | ||
|
|
||
| // The self-test-only write APIs must never be called from checkJdArchive, | ||
| // hasEmbeddedJdArchive, or parseReportFilename — only from runSelfTest. | ||
| // Extracting that function's body needs brace-counting, not a greedy | ||
| // regex: `[\s\S]*` backtracks to the LAST `\n}` in the whole file (e.g. the | ||
| // CLI-invocation block at the end), so `.replace(selfTestBody, '')` could | ||
| // strip out everything from runSelfTest onward — including real code after | ||
| // it — and a stray write call there would never get scanned, silently | ||
| // passing the very boundary check this is meant to enforce (CodeRabbit, | ||
| // PR #2791). Walk brace depth from the opening `{` instead, so nested | ||
| // blocks/arrow functions inside runSelfTest don't end the match early | ||
| // either. | ||
| const runSelfTestStart = jdArchiveSrc.indexOf('function runSelfTest()'); | ||
| let selfTestBody = ''; | ||
| if (runSelfTestStart !== -1) { | ||
| const openBrace = jdArchiveSrc.indexOf('{', runSelfTestStart); | ||
| let depth = 0; | ||
| let i = openBrace; | ||
| for (; i < jdArchiveSrc.length; i += 1) { | ||
| if (jdArchiveSrc[i] === '{') depth += 1; | ||
| else if (jdArchiveSrc[i] === '}') { | ||
| depth -= 1; | ||
| if (depth === 0) break; | ||
| } | ||
| } | ||
| selfTestBody = jdArchiveSrc.slice(openBrace, i + 1); | ||
| } | ||
| const codeOutsideSelfTest = jdArchiveSrc.replace(selfTestBody, ''); | ||
| const outsideSelfTest = codeOutsideSelfTest | ||
| .split('\n') | ||
| .filter(line => [...SELF_TEST_ONLY_FS].some(fn => line.includes(`${fn}(`)) && !/^\s*import\b/.test(line)); | ||
| if (outsideSelfTest.length === 0) { | ||
| pass('check-jd-archive.mjs never calls a write-capable fs API outside its own self-test fixtures'); | ||
| } else { | ||
| fail(`check-jd-archive.mjs calls a write-capable fs API outside runSelfTest: ${outsideSelfTest.join(' | ')}`); | ||
| } | ||
|
|
||
| // The by-name call scan above is blind to a local re-binding — `const write | ||
| // = writeFileSync; write(dest, data)` never contains the literal substring | ||
| // `writeFileSync(`, so a write through the alias sits outside runSelfTest | ||
| // invisibly (#3936). The import-list check already rejects an import-time | ||
| // alias (`writeFileSync as wfs`, since that exact string isn't in either | ||
| // allowed set), so the remaining gap is specifically a NEW binding created | ||
| // in the file body. Flagging the alias's creation — not just a subsequent | ||
| // call through it — closes the gap regardless of whether the alias is ever | ||
| // invoked: holding the capability outside runSelfTest is the violation. | ||
| // | ||
| // \(*\s*NAME\s*\)* (not just \s*NAME\b): a parenthesized initializer — | ||
| // `const write = (writeFileSync);` — is still a rebinding, and the bare | ||
| // version below missed it entirely since `(writeFileSync)` never matches | ||
| // `=\s*writeFileSync\b` (CodeRabbit, #4159 review). | ||
| const REBIND_RE = /\b(?:const|let|var)\s+\w+\s*=\s*\(*\s*(mkdtempSync|mkdirSync|writeFileSync|rmSync)\s*\)*\b/g; | ||
| const rebindings = [...codeOutsideSelfTest.matchAll(REBIND_RE)].map((m) => m[0].trim()); | ||
| if (rebindings.length === 0) { | ||
| pass('check-jd-archive.mjs never locally re-binds a write-capable fs API outside its own self-test fixtures'); | ||
| } else { | ||
| fail(`check-jd-archive.mjs re-binds a write-capable fs API outside runSelfTest: ${rebindings.join(' | ')}`); | ||
| } | ||
|
|
||
| // REBIND_RE itself, against literal fixtures — the real file has no | ||
| // rebinding to exercise the positive case against, so this pins the | ||
| // pattern's own behavior directly rather than only ever proving the | ||
| // negative ("the real file is clean") (CodeRabbit, #4159 review: add a | ||
| // regression case for the parenthesized-initializer alias form). | ||
| const rebindFixtureCases = [ | ||
| ["const write = writeFileSync;", true, 'bare initializer'], | ||
| ["const write = (writeFileSync);", true, 'single-parenthesized initializer'], | ||
| ["const write = ((writeFileSync));", true, 'double-parenthesized initializer'], | ||
| ["const write = ( writeFileSync );", true, 'parenthesized with inner spaces'], | ||
| ["const notReal = writeFileSyncButLonger;", false, 'a longer identifier merely prefixed by the name'], | ||
| ["someOtherThing(writeFileSync);", false, 'passed as a call argument, not assigned — a different code shape'], | ||
| ]; | ||
| const rebindFixtureFailures = rebindFixtureCases | ||
| .filter(([src, expectMatch]) => (new RegExp(REBIND_RE.source).test(src)) !== expectMatch) | ||
| .map(([src, expectMatch, label]) => `${label} (expected match=${expectMatch}): ${src}`); | ||
| if (rebindFixtureFailures.length === 0) { | ||
| pass('REBIND_RE matches bare and parenthesized initializers alike, without false-positiving on a longer identifier or a call argument'); | ||
| } else { | ||
| fail(`REBIND_RE fixture mismatch: ${rebindFixtureFailures.join(' | ')}`); | ||
| } | ||
|
|
||
| if (!/from\s*['"](?:node:)?fs\/promises['"]/.test(jdArchiveSrc)) { | ||
| pass('check-jd-archive.mjs does not import fs/promises'); | ||
| } else { | ||
| fail('check-jd-archive.mjs imports fs/promises — write-capable API surface'); | ||
| } | ||
| if (!/\brequire\s*\(/.test(jdArchiveSrc)) { | ||
| pass('check-jd-archive.mjs has no require() escape hatch'); | ||
| } else { | ||
| fail('check-jd-archive.mjs uses require() — bypasses the import whitelist'); | ||
| } | ||
| } catch (e) { | ||
| fail(`jd-archive wiring check: ${e.message}`); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Detect
$-prefixed write aliasesAt
tests/jd-archive-wiring.test.mjs:158,\w+does not match$. Therefore,const $write = writeFileSync; $write(...)bypasses both the rebinding assertion and the literal-call assertion. Extend the binding pattern and add a fixture for this valid alias form.Proposed fix
+ ["const $write = writeFileSync;", true, '$-prefixed alias'],📝 Committable suggestion
🤖 Prompt for AI Agents