Skip to content

Bring effects/node/virtual to 100% coverage: 89.12% → 100% branch - #1559

Merged
sergey-shandar merged 5 commits into
mainfrom
claude/effects-node-virtual-coverage-improvement
Aug 14, 2026
Merged

Bring effects/node/virtual to 100% coverage: 89.12% → 100% branch#1559
sergey-shandar merged 5 commits into
mainfrom
claude/effects-node-virtual-coverage-improvement

Conversation

@sergey-shandar

Copy link
Copy Markdown
Contributor

Summary

fjs/effects/node/virtual/module.f.mjs was at 89.12% branch coverage (20 uncovered branches). This brings it to 100%:

Three genuinely unreachable guards, removed/restructured per AGENTS.md §3.2:

  • readFile/readBytesOp's !Array.isArray(file) check, after the JsModule throw: operation's wrapper always descends into any plain-object (Dir) entry before a leaf op ever runs, so file at that point can only be a Vec[] (a Dir is intercepted earlier, a JsModule already threw above). Replaced with assert(Array.isArray(file), …), which narrows the same way while pushing the (unreachable) failure into assert's own already-fully-covered branch.
  • rmOp's "is a directory" guard: unreachable for the same structural reason. Removed entirely — nothing downstream depends on entry's narrowed type. This also surfaces a pre-existing bug in the test suite: rm.isDirectory's comment claimed to hit this guard, but it actually hits path.length !== 1 once the wrapper has already descended into the directory. Renamed the test to onDirectory and corrected the comment.
  • insertEntityAt's path.length === 0 guard: unreachable through its only external caller, rename — an empty dst is always rejected earlier by the ancestor/subtree check (isProperPrefix([], srcParsed) is true whenever srcParsed is non-empty). Replaced with an assert documenting why.

Proof cases added for branches that were genuinely reachable but untested: a zero-length chunk in readFile, an explicitly-undefined Dir entry in readdir (the type's index signature is optional, so this is a legal shape), several rename src/dst edge cases (empty src, src through a file, a 3-level missing path, dst through a file, a nested insertEntityAt error), createExclusive/writeBytes/stat on a missing nested path, writeBytes on a missing file and on a JsModule entry (unlike readFile/readBytes, writeBytes has no separate JsModule throw, so this is the reachable way to hit its "not a file"), and a negative writeBytes offset.

Test plan

  • npx tsc --noEmit
  • node --test --experimental-test-coverage --test-coverage-include='fjs/effects/node/virtual/module.f.mjs' fjs/emergent_testing/all.test.mjs → 100.00% line/branch/func
  • node ./fjs/module.mjs t → 2707 pass, 0 fail

🤖 Generated with Claude Code


Generated by Claude Code

`operation`'s wrapper always descends into every plain-object (`Dir`)
entry before a leaf op ever runs, so several "not a file" / "is a
directory" guards further down the file could only ever see a bare
`Dir` if that invariant broke — never in practice:

- readFile/readBytesOp: the `!Array.isArray(file)` check after the
  JsModule throw was unreachable (a Dir entry is always intercepted
  by the wrapper first). Replaced with `assert(Array.isArray(file), …)`,
  which narrows the type the same way while pushing the (unreachable)
  failure into asserts' own already-covered branch.
- rmOp: the "is a directory" guard was unreachable for the same
  reason — removed entirely, since nothing downstream depends on
  entry's narrowed type. Fixed the pre-existing `rm.isDirectory` test
  (renamed `onDirectory`), whose comment claimed to hit this guard but
  actually hits `path.length !== 1` once the wrapper has descended.
- insertEntityAt: the `path.length === 0` guard was unreachable
  through its only external caller, `rename` — an empty dst is always
  rejected earlier by the ancestor/subtree check. Replaced with an
  `assert` documenting why.

Also added proof cases for branches that were genuinely reachable but
untested: a zero-length chunk in readFile, an explicitly-`undefined`
Dir entry in readdir, several rename src/dst edge cases (empty src,
src through a file, a multi-level missing path, dst through a file,
a nested insertEntityAt error), createExclusive/writeBytes/stat on a
missing nested path, writeBytes on a missing file and on a JsModule
entry, and a negative writeBytes offset.

fjs/effects/node/virtual/module.f.mjs reaches 100% line/branch/
function coverage.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript 922e69f Commit Preview URL

Branch Preview URL
Aug 14 2026, 07:53 PM

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 8c3a9287f6ef9ca30580fc59daccd0753d658708, baseline origin/main = cb1fcdc457ecb5ba3a20d994e4ba4e0c31d9577e (the PR's merge-base is that same commit, so no rebase skew).

The headline claim measured

npm run cov in both trees, reading the effects/node/virtual/module.f.mjs row:

tree line branch func
origin/main cb1fcdc 100.00 89.12 100.00
this PR 8c3a928 100.00 100.00 100.00

So "89.12% → 100% branch" reproduces exactly. Repo-wide branch coverage moves 99.04 → 99.48. npx tsc --noEmit 0, npm run prepack 0 from a clean tree, npm test 2707 pass / 0 fail (main: 2691 under the same runner). linkcheck broken-link sets are byte-identical to main. No public-surface change (no types.ts touched, no new export type — so no §6.2 question), and changelog/unreleased/1559.md is a correct per-entry file linking only /pull/1559.

The deleted / asserted branches really are dead — including on main

Four guards are removed or converted to assert. I instrumented each of them on origin/main, where they still exist, replacing the guard body with throw new Error('DEAD-…'), and ran the full suite:

  • readFile's !Array.isArray(file)
  • readBytesOp's !Array.isArray(file)
  • rmOp's !Array.isArray(entry) && typeof entry === 'object'
  • insertEntityAt's path.length === 0

Result: 2691 pass / 0 fail — none of the four is reached. Negative control: making readFile's throw unconditional gives 24 failures, so the instrumentation is live. (First attempt also hit the identical line in statOp, which failed immediately via statOnJsModule — that one is genuinely reachable and correctly left alone in this PR. Good check on the method.)

The reasoning behind each also holds independently of the suite: operation's f calls op only when path is empty or when dir[path[0]] is not a plain non-array object, so after the function/undefined cases are handled, the remaining entity must be an array. For insertEntityAt I confirmed the specific premise: parse('') === [] and isProperPrefix([], ['a']) === true, so rename(src, '') with non-empty src is rejected as "onto an ancestor" first, and rename('', …) dies in extractEntity as "cannot extract root". The recursive self-calls only fire for path.length > 1. So this is not "100% by deleting live branches" — and unlike #1540 the deadness is not an artifact of something else this PR tightened, since it reproduces on unmodified main.

Mutation testing the new cases, two-sided

15 mutations, each applied to the PR head and then identically to origin/main, scored against the effects/node/virtual proof:

Killed on PR, survive on main (i.e. the new cases earn their keep): readdir's content === undefined skip; extractEntity's "cannot extract root"; insertEntityAt's 'not a directory' tag; writeBytesRawOp's "not a file"; statOp's missing-file enoent; readBytesOp's JsModule throw.

Equivalent mutants, correctly not a defect: descending into an array in extractEntity, and dropping either recursion's if (result[0] === 'error') re-wrap — in all three the produced value is structurally identical, and rename discards the returned dir on the error path anyway. Same for insertEntityAt's sub === undefined returning a clobbered dir: rename throws dstRoot away when dstResult is an error, so it is unobservable. These are unkillable by construction, not weak assertions.

One real weakness — the #1528 shape again, in this same module

Two of the new cases sit in front of ops that are wrapped by operation, whose result dir is threaded straight back into state.root. There, a wrong dir is observable, and the cases do not look at it:

createExclusiveNestedMissing: () => {
    const [, result] = virtual(emptyState)(createExclusive('a/b'))
    assert(result[0] === 'error')
},
writeBytesMissingFile: () => {
    const [, result] = virtual(emptyState)(writeBytes('missing', 0, vec8(0x1n)))
    assert(result[0] === 'error')
},

I mutated the two lines they newly cover to return the correct error tag but a wiped directory:

  • createExclusiveOp: if (path.length !== 1) { return [dir, invalidPath] }return [{}, invalidPath]
  • writeBytesRawOp: if (file === undefined) { return [dir, enoent] }return [{}, enoent]

Both are observable through the public entry point — with a populated root, createExclusive('a/b') and writeBytes('missing', …) now return state.root === {}, silently destroying the whole virtual filesystem while still reporting 'error'. With both mutations applied at once, the full suite is 2707 pass / 0 fail. Nothing catches it.

The cause is that both cases start from emptyState (root {}, so a wipe is invisible) and discard the returned state with const [, result] =. This is the same weakness found on #1525/#1528 — and #1528's instance was in this very file. It is distinguishable from the #1535 case, which was fine because proof.throw asserts only that a throw happens; here the case does return a state and simply declines to look at it.

Suggested fix: give these two a non-empty root and assert it survives, e.g.

const root = { keep: [vec8(0x1n)] }
const [state, result] = virtual({ ...emptyState, root })(createExclusive('a/b'))
assert(result[0] === 'error')
assertEq(Object.keys(state.root).length, 1)

A smaller version of the same thing: writeBytesNegativeOffset only asserts result[0] === 'error', and weakening the guard from !Number.isInteger(offset) || offset < 0 to !Number.isInteger(offset) still survives — -1 !== fileSizeBytes(chunks) produces a different error further down. Pinning result[1] would make the case actually about the negative-offset branch.

Everything else here is solid work, and the coverage claim is exact — but since the whole point of the PR is proof strength, the two lines that reach 100% while remaining insensitive to a filesystem-wiping mutant seem worth closing before merge.

createExclusiveNestedMissing, writeBytesNestedMissing, and
writeBytesMissingFile all started from emptyState and only checked
the error tag, so a mutant returning the right tag alongside a wiped
root would pass unnoticed (the #1525/#1528 shape, previously found in
this same file). Start each from a non-empty root and assert it
survives untouched.

writeBytesNegativeOffset only checked the error tag too, so weakening
the offset guard from `!Number.isInteger(offset) || offset < 0` to
`!Number.isInteger(offset)` would still pass (a different, later
guard catches -1 as a size mismatch instead). Pin the exact message.

Copy link
Copy Markdown
Contributor Author

Fixed in b6952cd — thanks for catching the #1525/#1528 shape again here:

  • createExclusiveNestedMissing, writeBytesNestedMissing, and writeBytesMissingFile now start from a non-empty root and assert it survives untouched, so a mutant that returns the right error tag alongside a wiped state.root is caught.
  • writeBytesNegativeOffset now pins the exact error message, so weakening the offset guard to drop the < 0 check (which a later size-mismatch guard would otherwise mask) is caught too.

npx tsc --noEmit, targeted coverage (still 100.00% line/branch/func), and the full suite (2707 pass, 0 fail) all still pass.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Both findings from the previous round are fixed, and I killed the mutants myself rather than taking the new assertions on faith.

Baseline: origin/main at cb1fcdc4.

The state-clobbering weakness is closed

The previous review's concern was that createExclusiveNestedMissing / writeBytesMissingFile started from emptyState and asserted only result[0] === 'error', so a mutant that returned the right error tag while wiping state.root survived the whole suite. The three cases now start from root = { keep: [vec8(0x1n)] } and assert Object.keys(state.root).length === 1.

I constructed the clobbering mutants against module.f.mjs at the head and ran the full suite (node ./fjs/module.mjs t) for each. Baseline: 2707 pass / 0 fail.

mutation result
createExclusiveOp: path.length !== 1return [{}, invalidPath] killedcreateExclusiveNestedMissing fails (2706/1)
writeBytesRawOp: file === undefinedreturn [{}, enoent] killedwriteBytesMissingFile fails (2706/1)
writeBytesRawOp: p.length !== 1return [{}, enoent] killedwriteBytesNestedMissing fails (2706/1)

Each mutant is caught by exactly the case that was supposed to catch it, and by no other — so the assertions are load-bearing and precisely aimed.

The offset < 0 guard is now covered

writeBytesNegativeOffset gained assertEq(result[1], 'Offset -1 is invalid'). Removing || offset < 0 from

if (!Number.isInteger(offset) || offset < 0) { return [dir, error(`Offset ${offset} is invalid`)] }

lets -1 fall through to the append-only check and produce a different message; the suite now goes to 2706 pass / 1 fail on writeBytesNegativeOffset. Previously this mutation survived. Killed.

Re-verified at this head

  • npx tsc --noEmit → exit 0.
  • node ./fjs/module.mjs t → 2707 pass, 0 fail.
  • Coverage re-measured (the proof changed, so I did not carry the old number forward): fjs/effects/node/virtual/module.f.mjs100.00 line / 100.00 branch / 100.00 func.
  • Broken-link sets identical to cb1fcdc4 (diff of the two linkcheck runs is empty), so the three new todo/*.md files strand nothing.
  • changelog/unreleased/1559.md is a per-entry file carrying its own [#1559](…/pull/1559) link and no heading. The todo/ additions correctly get no entry (§8.3).

Carried forward from the 8c3a9287 review

fjs/effects/node/virtual/module.f.mjs is byte-identical to what I verified there, so those results stand unchanged: the four removed/asserted guards were proven dead on origin/main itself by throw instrumentation (2691 pass / 0 fail, with a negative control giving 24 failures), and of 15 two-sided mutations 6 were killed on the PR while surviving on main — the new cases earn their keep — with the 3 survivors provably equivalent mutants.

The last three commits add todo/commit-message-standard.md, todo/commit-message-enforcement.md and a cross-reference in todo/changelog-from-git-history.md. They are docs-only and touch no code, but they are also unrelated to this PR's subject; worth splitting out if you want the coverage change to stand alone in the history. Not a blocker.

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 14, 2026
Merged via the queue into main with commit 2df303a Aug 14, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/effects-node-virtual-coverage-improvement branch August 14, 2026 20:46
sergey-shandar pushed a commit that referenced this pull request Aug 14, 2026
- '1e5+' didn't distinguish the mutant that merges plusSignToToken's
  default arm into its 'e' arm (both eof-cut a still-invalid token to
  the same message). Replaced with '1e5+3': under the mutant, the '+'
  restarts the exponent and the trailing '3' completes a valid number
  instead, which the pinned error message catches.
- tokenizeCharCodeOpAfterEof/tokenizeEofOpAfterEof only asserted the
  returned tokens, not the passthrough state — since these arms are
  unreachable through tokenize(), the co-located proof is the only
  thing that can ever catch a regression in the state half. Assert it
  too (same shape as #1559's fix for the same weakness).
- changelog/unreleased/1564.md still had a PR link; AGENTS.md §8.3
  now says entries carry no PR number or link (the file name already
  carries it).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants