Skip to content

feat(file-safety): serialized publish primitive over the shared file lock (split 1 of #1914, #1375) - #2383

Open
easonLiangWorldedtech wants to merge 1 commit into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fws/u9-1914-cut1-safe-write-file-lock
Open

easonLiangWorldedtech wants to merge 1 commit into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fws/u9-1914-cut1-safe-write-file-lock

Conversation

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor

What this is

Unit 1 of the five-way split of #1914, planned and issued on #2020. Whole-file module cut, 6 files, no partial hunks: src/services/file-safety/safeWriteText.ts with its spec and integration spec, src/utils/fileLock.ts with its two spec files, and src/services/file-safety/__tests__/safeWriteText.integration.spec.ts. It is one of five independent pull requests rather than a stack - nothing in it depends on any other unit, and it compiles on this main tip alone.

This unit introduces no new version of anything. safeWriteText.ts is blob 9701d4b3b688 and fileLock.ts is blob 2ab8179b00cf, and both are #1914 own blobs, already under review there. Nothing in this pull request is a fresh take on a contested file. What it does is make 386 of #1914 1,044 counted executable lines reviewable at a size one person can read in a sitting.

Numbers, with the base named

  • Base: main tip e487cd7b2a497aa1d9b60f9b0cb497f7839bb629, which is this commit own parent (git merge-base --is-ancestor exits 0 against it, so the gate merge-base and the base coincide).

  • Counted executable lines: 386 against the 500 limit, headroom 114. Per file: safeWriteText.ts 352 and fileLock.ts 34. The four __tests__ files count for nothing, because packageForPath returns no package for test paths.

  • Added and deleted lines, beside that number and not instead of it: 2,942 (2,936 added, 6 deleted) over 6 files.

  • zdt split measure against this base printed the following, quoted exactly as measured, with the exit code exactly as measured:

    standalone e487cd7b2a497aa1d9b60f9b0cb497f7839bb629..3edac64036eb6666aa177c7ec502c36d2a7379fb: 2936+ / 6- = 2942 a+d, 6 files  -> HARD-OVERSHOOT (must split)
    

    exit code 1. That exit code is reporting the 2,942 against the 1,000 hard added-deleted cap, not the executable-line cap; the executable-line number, which is what the CI gate measures against 500, is 386.

  • Typecheck: tsc --noEmit -p src/tsconfig.pcmap.json from the verification worktree root (node v22.23.4): exit 0, 0 error lines.

  • Tests, run from the same worktree src: vitest.services.config.ts printed Test Files 2 passed (2) and Tests 76 passed (76); vitest.misc.config.ts printed Test Files 2 passed (2) and Tests 9 passed (9). Both exit 0, with 0 failed and 0 skipped. Named rather than left silent: this unit has no skipped tests, and the six test.skipIf(process.platform === "win32") skips in this split chain live in src/utils/__tests__/safeWriteJson.test.ts, which belongs to held unit cut5 - POSIX-only behaviour that does run on Linux CI.

  • Why 2,942 is not pushed lower, stated so it reads as a decision and not an oversight: five of the six files are whole new files, and the largest is a test file whose single top-level describe spans lines 113-1486, 1,374 added-deleted lines on its own. Any cap-legal re-cut of that file needs a test-layout change on main first, and that prerequisite is booked as a named zero-counted-line row on Tracking fws U5: guarded-write core (create-if-absent and compare-and-swap) #2020 (comment 6739719219) with its evidence - a slice ending inside one of those describes does not parse (TS1005 at safeWriteText.spec.ts:819).

Gate numbers and what they mean

Two different questions get two numbers, and the base is named for both. Counted executable lines come from the repository own buildManifest in scripts/stryker-diff.mjs, run through .tmp-cr-audit/u8-measure-chain.mjs (node v22.23.4), with per-path diffs at --unified=0 because parseChangedLines marks every line a hunk covers and selectFromGit passes --unified=0 at scripts/stryker-diff.mjs:306. Added-and-deleted lines are the plain git diff --shortstat figure, printed beside the counted number and not instead of it.

The base named below is the current main tip, which is also this commit own parent. git merge-base --is-ancestor exits 0 against it, so the CI gate merge-base computation and my base coincide exactly and no two-dot approximation is involved in these figures.

Mutation gate: local attempt, and an honest deferral

A local zdt mutation preflight was attempted at this base. The wrapper printed preflight exit code: 0 and => PASS (local gate green) while writing no reports (mutation testing not applicable or run incomplete) and a digest with an empty package list, and the wrapped Stryker log says why: it instrumented the changed files and then died with Cannot find module 'vitest/config' from its .stryker-tmp sandbox copy of vitest.config.ts, whose require stack resolves vite out of the parent clone .pnpm store rather than through this worktree junctioned node_modules. A green verdict with an empty report set is not a green verdict, so this pull request carries no local mutation reading. CI mutation-diff will be the first real one.

That deferral is safe for two measured reasons rather than evasive ones. First, #1914 mutation-diff has never produced a mutant verdict for this content at all: it fails on the 1,044-versus-500 width gate before the runner reports, so there is no mutation history being inherited here. Second, if a surviving mutant does appear, the answer is test hardening, which lands in __tests__ paths and therefore costs zero counted executable lines, because packageForPath returns nothing for test paths. A mutation-red cannot move this unit further from mergeable than it already is.

Content fidelity

Every file here is #1914 verbatim content, taken from the merge commit that closes it (7209a5f1681b387e397434172707cb028e33ec3b, tree 3c919730a813) with no edits. The five whole-file units of this split are path-disjoint and their file sets sum to the 26 files #1914 changes. Applied onto this main tip they produce tree 83cdd9cb0223, and git diff --name-only 83cdd9cb0223 7209a5f1681b387e397434172707cb028e33ec3b prints 14 files - every one of them newer content that arrived on main after the merge base 5319fe91fb0caf4aa4ec66aac68fa03744ebbc23, none of them among the 26. The split therefore neither loses nor invents content, and each unit measures the same counted executable lines against the merge base and against this tip.

Series housekeeping

This split was planned and issued on #2020 before any split pull request opened (comments 6738707507, 6739312036, 6739376708, 6739428842, 6739719219), and the version question for the contested files is asked there in comment 6739877102. #1914 stays open until the units of its split have been reviewed, because its review history is the only record of what has already been said about this content; its 14 open review rows were answered on the pull request itself. On #1914 there is no review event from anyone except the bot and the author, and no non-empty review body from anyone except the bot (140 review events over 2 pages, 81 from the bot and 59 from the author account, 0 from anyone else).

No .changeset file and no CHANGELOG edit, per AGENTS.md. src/eslint-suppressions.json is not touched here: the 96-to-94 count change for core/tools/__tests__/readFileTool.spec.ts belongs to held unit cut4 and to that unit alone, so no two pull requests in this split edit that file. knip.json is likewise not touched here - held unit cut3 owns its one entry, and the fact that #1914 and #2359 both carry blob 34450fc441a3 while main, #1911, #1912, #1913, #1917 and #2368 carry 1f4956faa6cc is a known conflict cost of the series, recorded on #2020, not a mistake of this unit.

@coderabbitai

coderabbitai Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one): 1

  • coderabbit-review-active

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Review status

Thanks for contributing. This comment tracks the review sequence and the next action.

Current step: Fix the failing required CI checks; awaiting-maintainer requires CI and automated review completion.

Review-state labels are managed by this workflow; do not edit them manually. community-approved is managed the same way — do not add or remove it manually. It signals a fresh community code approval for the current head as an advisory priority only; maintainer review is still required.

@codecov

codecov Bot commented Nov 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.07958% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/services/file-safety/safeWriteText.ts 93.05% 7 Missing and 11 partials ⚠️
src/utils/fileLock.ts 93.33% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Changed-code mutation testing

Report for PR head 3edac64036eb.

Surviving / uncovered mutants Advisories
86 1
Details: mutants, guidance, and reproduction

Changed-code mutation testing

PackageChanged executable linesValidKilledTimeoutSurvivedNo coverageResult
extension38637227412788Advisory findings

Focused tests

  • extension (cwd src): core/task/__tests__/ask-auto-deny.spec.ts, core/webview/__tests__/ClineProvider.spec.ts, api/providers/__tests__/kimi-code.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.deleteSemantics.spec.ts, __tests__/extension.spec.ts, core/task/__tests__/ask-queued-message-drain.spec.ts, core/task/__tests__/Task.spec.ts, utils/__tests__/safeWriteJson.interruption.test.ts, core/task/__tests__/Task.persistence.spec.ts, core/tools/__tests__/executeCommand.spec.ts, activate/__tests__/registerCommands.spec.ts, core/webview/__tests__/webviewMessageHandler.spec.ts, core/webview/__tests__/ClineProvider.taskHistory.spec.ts, core/webview/__tests__/ClineProvider.sticky-mode.spec.ts, core/tools/__tests__/executeCommandTool.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts, api/providers/fetchers/__tests__/modelCache.spec.ts, core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts, core/webview/__tests__/ClineProvider.sticky-profile.spec.ts, api/providers/__tests__/native-ollama.spec.ts, core/config/__tests__/importExport.spec.ts, api/providers/__tests__/lmstudio.spec.ts, __tests__/ClineProvider.history-resume-delegation.spec.ts, core/task/__tests__/Task.dispose.test.ts, core/task/__tests__/ask-clear-approval-buttons.spec.ts, api/providers/__tests__/lmstudio-native-tools.spec.ts, utils/__tests__/safeWriteJson.test.ts, api/providers/__tests__/lm-studio.spec.ts, services/mcp/__tests__/McpHub.spec.ts, core/task/__tests__/Task.streaming-tool-calls.spec.ts, api/providers/__tests__/lite-llm.spec.ts, core/task/__tests__/ask-allowlist-cwd.spec.ts, api/providers/__tests__/opencode-go.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.crossInstance.spec.ts, services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts, core/webview/__tests__/ClineProvider.stale-view.spec.ts, api/providers/__tests__/lm-studio-timeout.spec.ts, core/config/__tests__/ProviderSettingsManager.spec.ts, utils/__tests__/fileLock.spec.ts, core/task/__tests__/grace-retry-errors.spec.ts, core/task/__tests__/flushPendingToolResultsToHistory.spec.ts, core/task-persistence/__tests__/apiMessages.spec.ts, __tests__/single-open-invariant.spec.ts, utils/__tests__/jsonFileRecovery.test.ts, core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts, api/providers/fetchers/__tests__/openrouter.spec.ts, api/providers/__tests__/openrouter.spec.ts, core/task/__tests__/Task.throttle.test.ts, services/file-safety/__tests__/safeWriteText.spec.ts, api/providers/__tests__/model-catalog-adapters.spec.ts, core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts, core/task/__tests__/Task.run-shadow.spec.ts, api/providers/__tests__/zoo-gateway.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts, core/tools/__tests__/CodebaseSearchTool.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts, core/task-persistence/__tests__/taskMessages.spec.ts, core/task/__tests__/reasoning-preservation.test.ts, core/webview/__tests__/webviewMessageHandler.delete.spec.ts, __tests__/ClineProvider.delegation.spec.ts, api/providers/__tests__/nanogpt.spec.ts, core/webview/__tests__/checkpointRestoreHandler.spec.ts, services/code-index/__tests__/manager.spec.ts, core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts, core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts, core/webview/__tests__/webviewMessageHandler.edit.spec.ts, api/providers/__tests__/vercel-ai-gateway.spec.ts, core/task/__tests__/grounding-sources.test.ts, activate/__tests__/registerCodeActions.spec.ts, utils/__tests__/autoImportSettings.spec.ts, core/prompts/__tests__/add-custom-instructions.spec.ts, api/providers/__tests__/requesty.spec.ts, services/code-index/__tests__/code-index-status-manager.spec.ts, api/providers/__tests__/friendli.spec.ts, core/webview/__tests__/webviewMessageHandler.openFile.spec.ts, __tests__/ClineProvider.subtask-links.spec.ts, api/providers/__tests__/io-intelligence.spec.ts, api/providers/fetchers/__tests__/lmstudio.test.ts, core/webview/__tests__/generateSystemPrompt.spec.ts, core/prompts/__tests__/system-prompt.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts, core/task/__tests__/Task.resume-eviction-race.spec.ts, core/task/__tests__/Task.ignored-disabled-tools-notice.spec.ts, core/task-persistence/__tests__/readFileWithMissingRetry.spec.ts, core/task/__tests__/build-tools-readiness.integration.spec.ts, services/code-index/__tests__/code-index-manager-registry.spec.ts, extension/__tests__/api-task-conversation-history-length.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-auto-deny.spec.ts, __tests__/removeClineFromStack-delegation.spec.ts, core/task/__tests__/Task.abort-reason-race.spec.ts, api/providers/__tests__/kenari.spec.ts, extension/__tests__/api-start-task-logging.spec.ts, services/code-index/processors/__tests__/processor-factories.spec.ts, core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts, activate/__tests__/handleUri.spec.ts, api/providers/fetchers/__tests__/modelEndpointCache.spec.ts, services/file-safety/__tests__/safeWriteText.integration.spec.ts, api/providers/__tests__/poe.spec.ts, core/webview/__tests__/messageEnhancer.test.ts, services/code-index/__tests__/cache-manager.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts, core/tools/__tests__/generateImageTool.test.ts, core/assistant-message/__tests__/presentAssistantMessage-tool-repetition.spec.ts, core/task/__tests__/build-tools.spec.ts, extension/__tests__/api-configuration.spec.ts, services/code-index/__tests__/code-index-workspace-scope.spec.ts, api/__tests__/index.spec.ts, core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts, core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts, core/webview/__tests__/webviewMessageHandler.readFileContent.spec.ts, extension/__tests__/api-start-new-task.spec.ts, core/task/__tests__/Task.pending-action.spec.ts, extension/__tests__/api-delegation-events.spec.ts, core/webview/__tests__/webviewMessageHandler.readOriginalContent.spec.ts, core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts, extension/__tests__/api-has-task-ui-message-text.spec.ts, services/marketplace/__tests__/marketplace-setting-check.spec.ts, core/webview/__tests__/webviewMessageHandler.cloudAuth.spec.ts, core/webview/__tests__/webviewMessageHandler.abandonSubtask.spec.ts, __tests__/abandonSubtask.spec.ts, activate/__tests__/handleTask.spec.ts, core/task/__tests__/ask-abort-race.spec.ts, api/providers/__tests__/unbound.spec.ts, utils/__tests__/enhance-prompt.spec.ts, core/assistant-message/__tests__/presentAssistantMessage.spec.ts, extension/__tests__/api-send-message.spec.ts, services/code-index/__tests__/code-index-scope.spec.ts, utils/__tests__/fileLock.test.ts, __tests__/nested-delegation-resume.spec.ts, core/webview/__tests__/ClineProvider.pending-action.spec.ts, __tests__/delegation-concurrent.spec.ts, core/webview/__tests__/ClineProviderFactory.spec.ts, core/task/__tests__/ToolExecutionContext.spec.ts, extension/__tests__/api-delete-queued-message.spec.ts, __tests__/task-run-dispatch.spec.ts, __tests__/api-subtask.spec.ts, core/task/__tests__/Task.sticky-profile-race.spec.ts, extension/__tests__/api-theme-fixture.spec.ts, core/prompts/__tests__/get-prompt-component.spec.ts, test-utils/__tests__/provider.spec.ts, core/task/__tests__/Task.terminal-operation.spec.ts, __tests__/new-task-delegation.spec.ts, utils/__tests__/fileLock.canonicalKey.spec.ts, core/assistant-message/__tests__/toTelemetryToolName.spec.ts, extension/__tests__/api-terminal-profile.spec.ts, core/context-tracking/__tests__/FileContextTracker.spec.ts

All surviving and uncovered mutants

Warning annotations highlight up to 20 unique locations (maximum 7 per file). This summary lists every advisory mutant.

extension

src/services/file-safety/safeWriteText.ts

LineStatusMutatorReplacement
102SurvivedStringLiteral""
123SurvivedStringLiteral``
124SurvivedStringLiteral``
125SurvivedObjectLiteral{}
127SurvivedStringLiteral""
137SurvivedStringLiteral``
139SurvivedStringLiteral``
154SurvivedStringLiteral""
168SurvivedBooleanLiteraltrue
171NoCoverageStringLiteral""
172SurvivedStringLiteral""
173SurvivedObjectLiteral{}
175SurvivedStringLiteral""
188SurvivedStringLiteral``
189SurvivedStringLiteral""
205SurvivedStringLiteral``
206SurvivedObjectLiteral{}
208SurvivedStringLiteral""
225SurvivedStringLiteral""
241SurvivedStringLiteral""
249SurvivedStringLiteral""
249SurvivedStringLiteral""
249SurvivedMethodExpressionMath.random().toString(36)
249SurvivedStringLiteral""
279SurvivedObjectLiteral{}
279SurvivedBooleanLiteralfalse
295SurvivedObjectLiteral{}
295SurvivedBooleanLiteralfalse
358SurvivedConditionalExpressionfalse
359SurvivedMethodExpressionmissing
361SurvivedCallExpression;
363SurvivedConditionalExpressiontrue
363SurvivedEqualityOperatorparent !== cursor
385SurvivedConditionalExpressiontrue
385SurvivedEqualityOperatormissing.length >= 0
388NoCoverageConditionalExpressiontrue
388NoCoverageEqualityOperatorcode === "ENOTDIR"
388NoCoverageStringLiteral""
388SurvivedConditionalExpressionfalse
472SurvivedStringLiteral``
482SurvivedStringLiteral``
487SurvivedLogicalOperatorstagingStat.isSymbolicLink() && !stagingStat.isFile()
489NoCoverageStringLiteral""
489SurvivedStringLiteral``
489SurvivedStringLiteral""
501SurvivedBooleanLiteraltrue
574SurvivedCallExpression;
576SurvivedCallExpression;
586SurvivedOptionalChainingoptions.execFileRunner
612SurvivedObjectLiteral{}
612SurvivedBooleanLiteralfalse
613SurvivedConditionalExpressiontrue
618SurvivedConditionalExpressionfalse
633NoCoverageBlockStatement{}
633SurvivedConditionalExpressionfalse
633SurvivedLogicalOperatorcode === "ENOENT" && code === "ENOTDIR"
633SurvivedConditionalExpressionfalse
633SurvivedStringLiteral""
633SurvivedConditionalExpressionfalse
633SurvivedStringLiteral""
638SurvivedConditionalExpressionfalse
670SurvivedCallExpression;
679SurvivedCallExpression;
695SurvivedConditionalExpressionfalse
695SurvivedStringLiteral""
705SurvivedConditionalExpressiontrue
737SurvivedConditionalExpressionfalse
762SurvivedConditionalExpressiontrue
762SurvivedStringLiteral""
787SurvivedOptionalChainingoptions.execFileRunner
791SurvivedLogicalOperatorreleaseBackupOnSuccess || backupPath
802SurvivedConditionalExpressionfalse
802SurvivedStringLiteral""
836SurvivedConditionalExpressiontrue
836SurvivedLogicalOperatorbackupPath || releaseBackupOnSuccess
859SurvivedConditionalExpressiontrue
859SurvivedConditionalExpressionfalse
859SurvivedEqualityOperatordaclDumpPath === null

src/utils/fileLock.ts

LineStatusMutatorReplacement
61SurvivedEqualityOperatordepth <= frameBudget
73SurvivedMethodExpressionMath.min(0, remainingLinkHops - frameBudget)
80SurvivedConditionalExpressiontrue
80SurvivedEqualityOperatormissing.length >= 0
86SurvivedConditionalExpressiontrue
86SurvivedStringLiteral""
173NoCoverageStringLiteral``
183NoCoverageStringLiteral``

Resolve a mutation gap

Add or strengthen a focused test that fails under the mutation. If the mutant is equivalent, request maintainer approval for the narrowest mutator-specific exclusion and explain why it cannot change behavior:

// Stryker disable next-line ConditionalExpression: normalized input cannot reach the alternate branch
const result = condition ? value : fallback

Broad all exclusions and exclusions without a concrete reason are reported as advisories.

Reproduce locally

From a full checkout containing both commits:

pnpm install --frozen-lockfile
node scripts/stryker-diff.mjs ci --base e487cd7b2a497aa1d9b60f9b0cb497f7839bb629 --head 5b0de4adacd6e8d27e130e43abde7fc1b7b15581

Mutation reports

  • extension: reports/mutation/extension/mutation.html

The workflow uploads generated reports in the changed-code-mutation-report artifact. A direct artifact link appears below after upload.

Advisory findings

  • extension timed out 12 of 372 valid mutants. The result is inconclusive; consider fixing flaky or slow tests, or reducing the changed scope.

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

knip is the only failing arm, and the reason is the consumer graph, not the build

The arm. Code QA Roo Code run 42617459566 concluded failure at 12:38:39Z on exactly one of its eight jobs. The job is knip (job 129160792900), and the failing step is its sixth, "Run production Knip checks". Its log, lines 394-400 of 444, printed:

> knip --production --treat-config-hints-as-errors

Unused files (1)
src/services/file-safety/safeWriteText.ts
 ELIFECYCLE  Command failed with exit code 1.
##[error]Process completed with exit code 1.

The other seven jobs of that run completed success - invisible-chars, dependency-review, check-translations, compile, Build test VSIX, platform-unit-test (windows-latest) and platform-unit-test (ubuntu-latest) - as did Changed-code mutation testing, E2E Tests (Mocked), Visual Regression and CodeQL Advanced on this head. The knip job's own policy suite passed 8 of 8 first, and two of its subtest names are the rule it enforces: "a production file imported only by tests is still reported as unused" and "tests cannot make an otherwise unused production export pass the production check". knip is one of the six contexts the Protect Main ruleset requires, so this arm, not the width and not the mutants, is what holds the pull request.

Why, measured at this head. At 3edac64, git grep -n safeWriteText -- src returns 8 mentions outside __tests__: seven are inside src/services/file-safety/safeWriteText.ts itself (its own error strings, its own temp-name calls) and the eighth is a comment - src/utils/fileLock.ts:53: "// (resolveLockKey in safeWriteText). Walking readlink even when the referent is". There is no production import of the file this pull request adds. The consumer that justifies it, src/core/tools/guardedWrite.ts with its three import sites and two call sites, is the content that stays in the held third unit, so this cut ships a primitive nobody in production calls yet.

Four open pull requests carry four different implementations of this same primitive, and main carries none of them (blob ids read with git ls-tree -r, main's copy counted in the same sentence: absent at e487cd7):

Ref safeWriteText.ts blob
#1910 at 303af45 2842acbeb817
#1911 at its current head 5559f1f 64e3a2479d93
#1912 at 9d420ad cd715dcb9e57
#1914 at 7209a5f, and this pull request at 3edac64 9701d4b3b688
main at e487cd7 absent

src/utils/fileLock.ts is the other divergence: 2ab8179b00cf at #1914 and here, and main's untouched 49807ea154d4 everywhere else in the family, including #1911 and #1912.

The shape that does clear knip, measured rather than asserted. At #1911's current head, src/utils/safeWriteJson.ts:13-14 imports safeWriteText from ../services/file-safety/safeWriteText and calls it at lines 197 and 217, and src/utils/jsonFileRecovery.ts:14-15 documents the staging-name contract it imposes; that is 14 production mentions outside the primitive, and #1911's knip job completed success along with its compile, check-translations, e2e-mock, both platform-unit-test matrix arms and mutation-diff (Code QA run 42619208866, completed 12:48:39Z, all eight jobs success). Counted executable lines for #1911 measured the way the gate measures - base = its own pin 73931fe, which git merge-base --is-ancestor confirms is an ancestor of its head, per-path diffs at --unified=0, buildManifest imported out of scripts/stryker-diff.mjs - is 496 of 500 across 10 files. #1910 measures 333 the same way against the same pin, and it handles the identical consumer-less condition with one configuration line, inserting "!services/file-safety/safeWriteText.ts!", into the src workspace project list in knip.json (blob 1f4956faa6cc to 8b51352a3585); #1914 handles it by adding "core/tools/guardedWrite.ts!" to the entry array.

What I am proposing. That this pull request be closed as superseded by #1911, on the arithmetic rather than on preference: the only sub-500 shape in the family that contains a safeWriteText with a production importer in the same unit is #1911's, at 496 of 500, and widening this cut to carry safeWriteJson.ts and jsonFileRecovery.ts reaches 1911's own width while shipping a rival blob of the same file. Widening it to carry the guardedWrite.ts consumer instead costs 394 counted executable lines on top of these 386 (measured per file: guardedWrite.ts 394, fileLock.ts 34, safeWriteText.ts 352, base = main tip e487cd7), which is 780 against a 500 line cap. Nothing is being pushed on this branch, and I am not asking anybody to label or review anything.

Merge order this produces, which is the useful part. The family starts at #1910 (333 counted executable lines) and #1911 (496), because those are the two units whose production files have consumers inside themselves. Once #1911 is on main, #1914's remaining content re-cuts to guardedWrite.ts at 394 counted lines plus the canonical-key change in fileLock.ts at 34, about 428, and that unit is knip-viable precisely because safeWriteText will already be on main and guardedWrite will import it. The companion pull request #2384 (observation registry plus the Task run-state fields, 25 counted executable lines, 11 mutants generated and 11 killed, no survivors) is unaffected by any of this and stays open as it is.

The mutation reading on this pull request, for the record, since the conclusion alone does not carry it. Changed-code mutation testing succeeded and uploaded its report (artifact changed-code-mutation-report, 1,493,396 B, containing extension/mutation.html, extension/mutation.json and pr-comment.md). The report row reads extension | 386 changed executable lines | 372 valid | 274 killed | 12 timeout | 78 survived | 8 no coverage | Advisory findings, and its advisory says "extension timed out 12 of 372 valid mutants. The result is inconclusive". The success conclusion is the width gate (386 against the 500 cap), not the mutants: 86 surviving and uncovered mutants are annotations. Test-only hardening of those 86 would add zero counted executable lines, because packageForPath in scripts/stryker-diff.mjs returns nothing for test paths, so this is fixable without moving the unit further from mergeable.

Measured by me on node v22.23.4 with git from C:/Program Files/Git/bin; the CI lines quoted above are from the job logs I read at 12:47-12:52Z, and the blob ids and counted-line figures are my own measurements against the bases named beside each of them.

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

The mutation reading on this pull request, and how it compares with the three rival units

Report set named before any verdict. CI generated and ran mutants on this head once: artifact changed-code-mutation-report, id 13675510301, 1,493,396 B, package list [extension], three report files (extension/mutation.html, extension/mutation.json, pr-comment.md). Its machine header reads head:3edac64036eb6666aa177c7ec502c36d2a7379fb blocking:86 advisories:1 and its table row reads:

| extension | 386 | 372 valid | 274 killed | 12 timeout | 78 survived | 8 no coverage | Advisory findings |

274 + 12 + 78 + 8 = 372 reconciles. The report locates every advisory mutant: 70 of them in src/services/file-safety/safeWriteText.ts between lines 102 and 859 - StringLiteral and ObjectLiteral replacements clustered at 102-249, then conditional, equality, logical and optional-chaining mutants at 358-363, 385-388, 472-501, 574-638 and 670-859 - and 6 in src/utils/fileLock.ts at lines 61, 73, 80 and 86; the no-coverage entries are at safeWriteText.ts 171, 388 (three mutants) and 489, and at fileLock.ts 173 and 183. Its own advisory: "extension timed out 12 of 372 valid mutants. The result is inconclusive; consider fixing flaky or slow tests, or reducing the changed scope." CI's own changed-executable-lines figure, 386, is the number I measured with the gate's own accounting, so the width is confirmed from both sides. The job concluded success because 386 is under the 500-line cap; the 86 surviving and uncovered mutants are annotations, not a verdict.

Where this leaves the four implementations of the same primitive. These are all CI-printed rows, read from each unit's own artifact with /actions/runs?head_sha= using the full 40-character sha (a 12-character prefix returns an empty list rather than an error, which is a trap worth naming):

Unit and safeWriteText blob Lines Valid Killed Timeout Survived No coverage Result
#2384 (this family's observation-registry unit, different content) 25 11 11 0 0 0 Passed
#1910 at 303af45 - 2842acbeb817 333 305 239 8 56 2 Advisory findings
this pull request, #2383 - 9701d4b3b688 (the blob #1914 carries) 386 372 274 12 78 8 Advisory findings
#1911 at 5559f1f - 64e3a2479d93 496 0 0 0 0 0 Advisory incomplete
#1912 at 9d420ad - cd715dcb9e57 672 no run: gate threw before generating mutants mutation-diff failure

The kill percentages and the per-line ratios in the next sentence are my arithmetic on those printed rows, not CI's. Of the two rival implementations that have ever been mutation-measured, #1910's kills 78.4% of its valid mutants and leaves 0.174 surviving-or-uncovered mutants per counted executable line; this pull request's (the blob #1914 carries) kills 73.7% and leaves 0.223. #1911's row is Advisory incomplete with the report line "The mutation run was incomplete; selected test file names were not recorded", and its only advisory is "extension generated 510 mutants in preflight (limit 400). Split the PR or obtain a maintainer-reviewed narrow exclusion." - so the version my coordinator is recommending as the family's carrier currently has no mutation measurement at all, and the version maintainers would be reading if they compared implementations can only be read here and on #1910. #1910's reading is against its head 303af45 from before the 12:55Z push; I checked git ls-tree -r at the new head c5556a5 and safeWriteText.ts is still 2842acbeb817 and knip.json is still 8b51352a3585, so that reading still describes the same implementation. #1912 has no reading because its gate threw: job 129040967378, step 6, printed "Mutation gate failed: extension has 672 changed executable lines (limit 500)" - 672 is also what I measure for #1912 against its pin 73931fe.

The knip arm, priced both ways, at zero counted lines. Two answers already exist in this family for a publish file with no production importer yet. #1910 carries one: its knip.json goes 1f4956faa6cc to 8b51352a3585 by inserting "!services/file-safety/safeWriteText.ts!", into the src workspace project list, and its knip job completed success. #1914 carries the other shape: knip.json 34450fc441a3 adds "core/tools/guardedWrite.ts!" to the entry array. knip.json is configuration, so neither line counts toward the 500 executable-line cap. Whichever blob a maintainer chooses to keep, that ignore or entry line is a merge-order artefact and not a defence of the file: it records that the consumer has not landed yet, and it stops being needed the moment guardedWrite or safeWriteJson imports the version that survives.

What is still owed on this pull request before it is mergeable in anyone's eyes, stated as owed and not as done. The 78 surviving and 8 uncovered mutants have not been triaged into real gaps and equivalent mutants. That work is cheap in the currency this gate spends - packageForPath in scripts/stryker-diff.mjs returns nothing for test paths, so adding or hardening tests costs zero counted executable lines - and I have not done it. I am specifically not asserting that the survivors are equivalent mutants: the Math.random().toString(36) replacements in the temp-name literals and the empty-StringLiteral replacements in error messages look like the equivalent-mutant class, and I have not tested that claim, and the 12 timeouts make CI's own result explicitly inconclusive rather than merely incomplete.

I am asking for nothing here: no label, no review, no thread resolution. My measurements are mine on node v22.23.4 (git from C:/Program Files/Git/bin), the CI rows are quotations from the artifacts and job logs I downloaded at 12:47-13:14Z on 2026-11-09, and the percentages are labelled as my arithmetic where they appear.

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Triage of the 78 surviving and 8 uncovered mutants: three named piles, nothing in a fourth

Read, not pushed: this comment changes no file and asks for nothing. The 86 advisory mutants named in CI's own report for head 3edac64036eb6666aa1d9b60f9b0cb497f7839bb629 (78 survived, 8 never covered) are sorted here against the source as it stands at that commit and against the two specs that unit ships - src/services/file-safety/__tests__/safeWriteText.spec.ts (1,676 lines) and safeWriteText.integration.spec.ts (67 lines) - plus src/utils/__tests__/fileLock.spec.ts (169 lines) for the six fileLock entries. Pile totals: 5 equivalent with the reason named, 81 test-gap with the specific missing assertion named, 0 needing a reasoned exclusion. A reasoned // Stryker disable next-line MutatorName: reason comment costs zero counted executable lines, because comments do not charge against the 500-line cap; blanket or unreasoned disables are rejected by the gate and by me, and I have proposed none.

Pile 1 - equivalent mutant, reason named per mutant (5)

All five are the same class: a StringLiteral emptied inside a comparison against a constant that no reachable value can equal. They are equivalents by argument about the value range, and I state the argument rather than the label. Nothing else in the 86 lands in this pile, and in particular I am not asserting equivalence for the Math.random().toString(36) temp-name literals: I read each of the four mutants at safeWriteText.ts:249 (three StringLiterals - the leading dot, the two separators - and the MethodExpression that removes .substring(2)), and all four are killable by an assertion the suite does not have, which makes them pile 2 and not pile 1.

Pile 2 - test gap, the specific missing assertion named per mutant (81 entries across 59 locations)

Each line below is one or more mutants at one source line, with what the suite fails to assert. Where a line carries several mutants, the named assertion kills them together and I say so.

  • FL:61 (EqualityOperator -> "depth <= frameBudget") - no test drives canonicalLockPath to the frame budget, so depth < frameBudget versus depth <= frameBudget is unobserved
  • FL:73 (MethodExpression -> "Math.min(0, remainingLinkHops - frameBudget)") - no test consumes the remaining-hop budget to a nonzero value, so max versus min is unobserved
  • FL:80 (ConditionalExpression -> "true"; EqualityOperator -> "missing.length >= 0") - no test reaches this site with an empty missing list, so the join-versus-bare branch is unverified
  • FL:86 (ConditionalExpression -> "true"; StringLiteral -> """") - no test makes this realpath catch report ENOTDIR, so the early return and the code comparison are unverified
  • FL:173 (StringLiteral -> "``" [uncovered]) - uncovered: no test makes the in-catch lock release fail, so the console.error line that reports it is never executed
  • FL:183 (StringLiteral -> "``" [uncovered]) - uncovered: no test makes the post-success lock release fail, so the console.error line that reports it is never executed
  • SWS:102 (StringLiteral -> """") - no assertion on the error's name field - expect(orph.name).toBe("OrphanedBackupError") is absent anywhere in the 1,676-line spec, which asserts the class via toBeInstanceOf and the message via toContain but never the name
  • SWS:123 (StringLiteral -> "``") - no assertion pins the second clause of the orphaned-backup message: toContain("safeWriteText") at spec 575 and toContain("EPERM") at 576 both still pass when this literal is emptied; add toContain("could not be removed")
  • SWS:124 (StringLiteral -> "``") - no assertion pins the retention clause ("The copy is still on disk and should be removed"); the existing toContain checks pass with it emptied
  • SWS:125 (ObjectLiteral -> "{}") - cause is asserted for one error only (spec 579 expect(orphan.cause).toBe(orphan.originalError)); no test asserts cause propagation for OrphanedPublishedBackupError, PostCommitDurabilityError or UnsupportedFilesystemError, so replacing the options object with an empty one is silent
  • SWS:127 (StringLiteral -> """") - same name-field gap for OrphanedPublishedBackupError
  • SWS:137 (StringLiteral -> "``") - no assertion pins the backup-creation-failure message's first clause; the test at 517 asserts the class and the leftover path, not this prose
  • SWS:139 (StringLiteral -> "``") - no assertion pins "still on disk and must be deleted" even though the test at 545 exists precisely to carry the leftover path
  • SWS:154 (StringLiteral -> """") - same name-field gap for StagingPathError
  • SWS:168 (BooleanLiteral -> "true") - no call site in the suite omits the third constructor argument and then asserts which message was chosen, so flipping the default from false to true is unobserved
  • SWS:171 (StringLiteral -> """" [uncovered]) - uncovered: no test ever constructs PostCommitDurabilityError with copiedTargetFsync true, so the whole true-branch message is never executed
  • SWS:172 (StringLiteral -> """") - no assertion distinguishes the copiedTargetFsync=false message from the true branch, so emptying it is silent
  • SWS:173 (ObjectLiteral -> "{}") - no assertion that PostCommitDurabilityError carries its cause
  • SWS:175 (StringLiteral -> """") - same name-field gap for PostCommitDurabilityError
  • SWS:188 (StringLiteral -> "``") - no assertion on the TargetExistsError message text; the test at 1236 asserts only the class
  • SWS:189 (StringLiteral -> """") - same name-field gap for TargetExistsError
  • SWS:205 (StringLiteral -> "``") - no assertion on the UnsupportedFilesystemError message text; the test at 1289 asserts only the class
  • SWS:206 (ObjectLiteral -> "{}") - no assertion that UnsupportedFilesystemError carries its cause
  • SWS:208 (StringLiteral -> """") - same name-field gap for UnsupportedFilesystemError
  • SWS:225 (StringLiteral -> """") - same name-field gap for TargetMovedError
  • SWS:241 (StringLiteral -> """") - same name-field gap for AncestorReplacedError
  • SWS:249 (StringLiteral -> """"; StringLiteral -> """"; MethodExpression -> "Math.random().toString(36)"; StringLiteral -> """") - no assertion on the staging file's own name: the shape regex at spec 246 pins the staging directory (.file-safety-staging_), not the dot-prefixed temp file built here, and openSync and unlink are asserted only with stringContaining(stagingDir); assert the basename against ^.[a-z0-9-]+\d+_[a-z0-9]+.tmp$ - that single assertion kills all four mutants here, including the Math.random().toString(36) one, which is why I am not calling it equivalent
  • SWS:279 (ObjectLiteral -> "{}"; BooleanLiteral -> "false") - no test asserts the spawn options object passed to the injected runner, so windowsHide: true becoming false, or the whole options object becoming {}, is unobserved
  • SWS:295 (ObjectLiteral -> "{}"; BooleanLiteral -> "false") - same options-blindness on the restore call
  • SWS:358 (ConditionalExpression -> "false") - no test drives the canonical === undefined branch of the lexical fallback
  • SWS:359 (MethodExpression -> "missing") - no test reaches the reverse-and-rejoin expression with a non-empty missing list at this site
  • SWS:361 (CallExpression -> ";") - no test distinguishes whether the basename was pushed onto the missing list at this step
  • SWS:363 (ConditionalExpression -> "true"; EqualityOperator -> "parent !== cursor") - no test constructs a cursor whose parent equals itself here, so the loop-termination condition is unverified
  • SWS:385 (ConditionalExpression -> "true"; EqualityOperator -> "missing.length >= 0") - the two mutants here (the conditional and the length comparison) survive because no test produces an empty missing list at this site, so the join-versus-bare-realPath choice is unverified
  • SWS:388 (ConditionalExpression -> "false"; ConditionalExpression -> "true" [uncovered]; EqualityOperator -> "code === "ENOTDIR"" [uncovered]; StringLiteral -> """" [uncovered]) - uncovered and surviving together: no test makes the realpath attempt at this site throw a code other than the two absent-path codes, so neither the fail-closed throw nor the ENOTDIR arm is ever taken
  • SWS:472 (StringLiteral -> "``") - no assertion on the staging-containment message text; the test at 1546 asserts the class and that nothing was written
  • SWS:482 (StringLiteral -> "``") - no assertion on the staging-names-the-target message text; the test at 293 asserts the class only
  • SWS:487 (LogicalOperator -> "stagingStat.isSymbolicLink() && !stagingStat.isFile()") - no test supplies a staging path that is a regular-file-looking non-file (for example a FIFO-shaped double) so the negated isFile arm is only half-covered
  • SWS:489 (StringLiteral -> "``"; StringLiteral -> """"; StringLiteral -> """" [uncovered]) - no assertion on the not-a-regular-file message, and its ternary arms ("a symlink" versus "another file type") are never distinguished - the test at 1559 covers the symlink arm only through the class
  • SWS:501 (BooleanLiteral -> "true") - no test observes that releaseBackupOnSuccess starts false: the backup-release decision is asserted through outcomes, not through the flag's initial value
  • SWS:574 (CallExpression -> ";") - fsyncSync is asserted at the staging-file site (spec 148, 150) but not at this one, so deleting this call is silent
  • SWS:576 (CallExpression -> ";") - closeSync is asserted for the staging descriptor elsewhere, not for this descriptor
  • SWS:586 (OptionalChaining -> "options.execFileRunner") - no test walks the win32 DACL save path with options undefined, so removing the optional chain (which would throw a TypeError) is unobserved
  • SWS:612 (ObjectLiteral -> "{}"; BooleanLiteral -> "false") - no test asserts the { bigint: true } option on the stat call used for ancestor identity checks; the assertions compare the returned dev and ino, not the request
  • SWS:613 (ConditionalExpression -> "true") - no test drives the ENOENT arm of the ancestor-stat catch here
  • SWS:618 (ConditionalExpression -> "false") - no test drives the dev-or-ino mismatch arm at this site with only one half differing
  • SWS:633 (ConditionalExpression -> "false"; LogicalOperator -> "code === "ENOENT" && code === "ENOTDIR""; ConditionalExpression -> "false"; StringLiteral -> """"; ConditionalExpression -> "false"; StringLiteral -> """"; BlockStatement -> "{}" [uncovered]) - uncovered BlockStatement plus six surviving mutants: no test enters this ENOENT-or-ENOTDIR block, so the whole recovery body is unverified
  • SWS:638 (ConditionalExpression -> "false") - no test drives the resolvedDir-mismatch arm here
  • SWS:670 (CallExpression -> ";") - no assertion closes the seed descriptor: the suite counts unlink and rename calls, not this close
  • SWS:679 (CallExpression -> ";") - no assertion closes the backup descriptor for the same reason
  • SWS:695 (ConditionalExpression -> "false"; StringLiteral -> """") - no test makes the backup-cleanup catch report ENOENT at this site (the surviving conditional and the uncovered path show the same gap)
  • SWS:705 (ConditionalExpression -> "true") - no test drives the backupCleanupError non-null arm here
  • SWS:737 (ConditionalExpression -> "false") - no test drives a fs error carrying ENOTSUP or ENOSYS through this gate, so the unsupported-fallback classification is half-verified
  • SWS:762 (ConditionalExpression -> "true"; StringLiteral -> """") - no test calls with platform set to a value other than win32 and linux while the DACL gate is open, and CI's matrix is ubuntu and windows only, so the non-win32 early return is verified for two platform strings and no others
  • SWS:787 (OptionalChaining -> "options.execFileRunner") - same gap on the restore path
  • SWS:791 (LogicalOperator -> "releaseBackupOnSuccess || backupPath") - no test separates releaseBackupOnSuccess true with backupPath empty from the both-set case
  • SWS:802 (ConditionalExpression -> "false"; StringLiteral -> """") - no test drives ENOENT through this cleanup catch
  • SWS:836 (ConditionalExpression -> "true"; LogicalOperator -> "backupPath || releaseBackupOnSuccess") - no test drives the both-set case at this cleanup site to separate it from the single-flag cases
  • SWS:859 (ConditionalExpression -> "true"; ConditionalExpression -> "false"; EqualityOperator -> "daclDumpPath === null") - no test drives the daclDumpPath non-null cleanup path to the point where the null comparison is the only difference

Pile 3 - needs a reasoned exclusion (0)

Empty, and deliberately so. Every one of the 86 has an observable difference that an assertion could catch on the two runners CI actually has, so I am not asking for any disable comment and I am not offering a blanket one. If a maintainer decides the five pile-1 items should be silenced rather than left as advisory findings, the exclusion route is a reasoned Stryker disable next-line at each of the five lines, which costs nothing in counted executable lines; the alternative - hardening the tests - costs nothing there either, for the same reason: test paths charge nothing.

What the shape of the gap says, stated as mine and not as CI's

78 survivors against 372 valid mutants is a 73.7% kill rate (my arithmetic on CI's printed row), and the gaps cluster in three places rather than spreading evenly. The largest is error identity: eight of the classes in this file set this.name and no assertion anywhere in the suite reads .name - I grepped for it and got zero hits - so any mutant that empties an error name survives, and one assertion per class would retire eight entries here. The second cluster is post-commit cleanup and descriptor release: five of the mutants are deleted fsyncSync or closeSync calls at four distinct sites (574, 576, 670, 679 and the seed path), and the suite does assert those calls elsewhere (spec 148 and 150 for the staging descriptor), which tells me the harness is capable of the assertion and simply does not make it at these sites. The third is the errno-shaped branches: 20 conditional, 5 equality and 4 logical mutants are all comparisons against ENOENT, ENOTDIR, EPERM, ENOTSUP, ENOSYS or win32, and the suite drives some of those codes at some sites and not at others - the six uncovered entries at 388 and 633 in safeWriteText.ts and 173 and 183 in fileLock.ts are entire catch-block bodies never entered, which is the cheapest kind of test to add because it needs a double and an error object, not a filesystem.

One limitation stated before anyone reads this as a completeness claim: this triage judges what CI's report says survived against what the committed specs assert. It is not a re-run of Stryker, and the 12 timeouts are not in any pile because they are neither survivors nor uncovered - CI's own advisory calls the result inconclusive on their account. And the win32-gated sites (279, 295, 586, 762, 787, 859) are judged against a suite that contains exactly one it.skipIf(process.platform !== "win32") at spec 631 on a ubuntu-and-windows matrix; I make no claim about any other platform because I have not run one.

Interpreter and instruments: node v22.23.4 with git from C:/Program Files/Git/bin/git.exe; survivor and uncovered rows parsed from CI's pr-comment.md inside artifact 13675510301 (the report's own | line | Survived/NoCoverage | mutator | replacement | rows), source and spec text read with git show 3edac64:, assertion presence checked with git grep -n` on that same commit. Nothing asked: no label, no review, no thread resolution.

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Correction to my own triage comment (6741531527): the pile counts were wrong in the published text, and here is the pile it dropped

The comment I posted at 13:42:30Z on this pull request announced "5 equivalent with the reason named, 81 test-gap with the specific missing assertion named, 0 needing a reasoned exclusion" and then rendered zero equivalent entries and 86 test-gap entries across 59 locations. The counts and the list disagree, and the fault is mine alone: the classification step compared a mutated-label string against a key built differently, matched nothing, and silently left every entry in pile 2 while the heading kept the number I had intended. The per-location descriptions in that comment are unaffected and stand as written; what was missing is pile 1, and the totals below are the ones the earlier comment should have carried.

Pile 1, as it should have read - equivalent mutant, reason named per mutant (5)

  • src/services/file-safety/safeWriteText.ts:633, the StringLiteral replaced by """" - the comparison is against the fixed constants ENOENT and ENOTDIR; emptying one of them changes the decision only for an error whose code is the empty string, and no fs call reachable from this file produces one. Reason, not proof: a double carrying an empty code would tell them apart and no test injects one.
  • src/services/file-safety/safeWriteText.ts:633, the StringLiteral replaced by """" - the comparison is against the fixed constants ENOENT and ENOTDIR; emptying one of them changes the decision only for an error whose code is the empty string, and no fs call reachable from this file produces one. Reason, not proof: a double carrying an empty code would tell them apart and no test injects one.
  • src/services/file-safety/safeWriteText.ts:695, the StringLiteral replaced by """" - getErrorCode returns undefined or a real errno string, never the empty string, so the mutated comparison is permanently false and the branch decision is unchanged.
  • src/services/file-safety/safeWriteText.ts:762, the StringLiteral replaced by """" - the comparison is against the platform identifier win32; Node reports a short list of platform identifiers and none of them is the empty string, so every reachable input takes the same branch.
  • src/services/file-safety/safeWriteText.ts:802, the StringLiteral replaced by """" - the same shape as 695, at the second backup-cleanup site.

Corrected totals

Equivalent mutant with the reason named: 5. Test gap with the specific missing assertion named: 81 entries across 59 locations. Needs a reasoned exclusion: 0. Sum 5 + 81 + 0 = 86, which is CI's 78 surviving plus 8 uncovered for head 3edac64036eb6666aa1d9b60f9b0cb497f7839bb629, with nothing in a fourth category.

Five of the entries that comment listed under pile 2 belong to the same source lines as the five above, and they stay in pile 2 because their observable difference is real: src/services/file-safety/safeWriteText.ts:633 (ConditionalExpression "false"; LogicalOperator "code === "ENOENT" && code === "ENOTDIR""; ConditionalExpression "false"; ConditionalExpression "false"; BlockStatement "{}"); src/services/file-safety/safeWriteText.ts:695 (ConditionalExpression "false"); src/services/file-safety/safeWriteText.ts:737 (ConditionalExpression "false"); src/services/file-safety/safeWriteText.ts:762 (ConditionalExpression "true"); src/services/file-safety/safeWriteText.ts:802 (ConditionalExpression "false").

A rendering defect from the same comment, since it makes the entries hard to read: I wrapped CI's replacement values in quotes when several of them are already quoted, so sixteen rows printed a run of three quote characters instead of naming the value. From here on I write it as words - "the StringLiteral replaced by the empty string" - rather than nesting quotes. Nothing else in that comment is being restated, retracted or amended; a published comment is append-only for me, so this correction sits beside it rather than inside it.

Nothing asked: no label, no review, no thread resolution. Same instruments as before - node v22.23.4, git from C:/Program Files/Git/bin/git.exe, mutant rows parsed from CI's pr-comment.md inside artifact 13675510301, source read with git show at 3edac64.

This branch has not been deployed

No deployments
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.

1 participant