Skip to content

Commit ff4686c

Browse files
test(tools): drive the guardedWrite double's abort through requestAbort
Task.abort became a get-only accessor on main. 5319fe9 ("refactor(task): read abort and abortReason from run-state latches", landed 2026-11-08T16:18:32Z) declares main's Task.ts:3582 as 'get abort(): boolean { return abort(this.runState) }' fed by runState.ts:188, with requestAbort as the write surface. compile typechecks the checkout CI produces, which is refs/pull/1408/merge - main plus this branch - and in that tree this file's two 'task.abort = true' lines are assignments to a getter, even though they are legal against this branch's Task, which still carries the mutable field at Task.ts:397. The failing step was 'Check types' (pnpm check-types, exit 2) with 'Check formatting' and 'Lint' green ahead of it, and the whole diagnostic set was: core/tools/__tests__/guardedWrite.spec.ts(745,9): error TS2540: Cannot assign to 'abort' because it is a core/tools/__tests__/guardedWrite.spec.ts(759,9): error TS2540: Cannot assign to 'abort' because it is a The parent 0c07c5b ran the same step as a cache miss with zero TS errors because its merge ref predated the 16:18 landing, so the file and its lines are unchanged between the clean run and the red one. The double now carries requestAbort and both rows call it rather than writing the field, because that is the entry point production calls and it does not depend on which of the two Task shapes the tree happens to carry. guardedWrite.ts:420 reads task.abort and nothing in this file asserts abandoned, so the double's method sets the flag guardedWrite consults and carries abandoned only to match the method it replaces; the comment at :63-77 names the accessor and the kernel line so the next reader does not have to rediscover the split between the two trees. Control. With the double's requestAbort changed so it no longer sets the flag, the file is 2 failed | 40 passed (42): 'does not publish a queued write after the issuing task is disposed' red at spec.ts:763 with 'promise resolved "undefined" instead of rejecting', and 'refuses an already-cancelled task's write before any I/O' red at spec.ts:775 with 'expected [Function] to throw error matching /was cancelled/'. So both rows still prove the cancellation they name rather than trusting the call's name. Against the shipped source the file is 42 passed (42) in the branch tree and 42 passed (42) in the merged tree, and safeWriteText is 46 passed (46) in both. Typecheck in both directions, because that is the proof this class of red needs. tsc --noEmit in the merged tree before the change exits 2 with exactly the two TS2540 errors above; after the change it exits 0 with no diagnostics. tsc --noEmit in the branch tree exits 0 before and after, which is precisely why a branch-tree typecheck cannot catch this: the property is mutable in that tree by construction. The merged tree used here is git merge-tree of main 5319fe9 and this branch - tree c7ee6ae, conflict-free - checked out into a scratch worktree with the usual node_modules junctions, which is the content CI's merge ref holds. +21/-6 in this one spec file - 7 executable lines, 14 comment lines, no blank lines, no production file touched. Measured with .tmp-node22 node v22.23.4. eslint --prune-suppressions --max-warnings=0 on the file exits 0 with src/eslint-suppressions.json restored byte-for-byte, and repo-root prettier --check on the file exits 0.
1 parent 8fff59a commit ff4686c

1 file changed

Lines changed: 21 additions & 6 deletions

File tree

‎src/core/tools/__tests__/guardedWrite.spec.ts‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,29 @@ interface MockTaskOptions {
6262

6363
/**
6464
* Minimal structural Task: guardedWrite reads task.cwd, task.observationRegistry and
65-
* task.abort (the flag Task.dispose() sets). The real Task constructor needs the
66-
* full provider machinery, so a single documented double cast stands in for the class.
65+
* task.abort. The real Task constructor needs the full provider machinery, so a single
66+
* documented double cast stands in for the class.
67+
*
68+
* The double carries requestAbort rather than letting a test write the flag, because that
69+
* is the surface Task uses to set it: requestAbort on this branch's Task.ts:3591, and on
70+
* main's Task.ts:3594, where abort itself is a get-only accessor fed by the run-state
71+
* kernel (run-state/runState.ts:188). Writing task.abort from a test still compiles
72+
* against this branch's Task and stops compiling against main's, which is the difference
73+
* the merged-tree typecheck sees; calling the same entry point production calls does not
74+
* depend on which of the two shapes the tree carries. abandoned is carried only so the
75+
* method matches the one it replaces - nothing here asserts it, and guardedWrite.ts:420
76+
* reads only abort.
6777
*/
6878
function createMockTask(options: MockTaskOptions = {}): Task {
6979
const task = {
7080
cwd: options.cwd ?? WORKSPACE,
7181
observationRegistry: options.observationRegistry ?? new ObservationRegistry(),
7282
abort: false,
83+
abandoned: false,
84+
requestAbort(isAbandoned: boolean): void {
85+
this.abandoned = isAbandoned === true
86+
this.abort = true
87+
},
7388
}
7489
return task as unknown as Task
7590
}
@@ -737,12 +752,12 @@ describe("guardedWrite (S4a, epic #1375)", () => {
737752
const first = guardedWrite(task, "queued.txt", "first", "create")
738753
const second = guardedWrite(task, "queued.txt", "second", "create")
739754
// Let the first link enter the publish (the chain runs on microtasks) before the
740-
// disposal lands: Task.dispose() sets task.abort while the second write is still
741-
// queued behind it.
755+
// cancellation lands: an aborted task carries the flag guardedWrite consults, and
756+
// the second write is still queued behind the first at that point.
742757
await new Promise(function (resolve) {
743758
setImmediate(resolve)
744759
})
745-
task.abort = true
760+
task.requestAbort(false)
746761
releaseFirst()
747762
await first
748763
await expect(second).rejects.toThrow(/was cancelled/)
@@ -756,7 +771,7 @@ describe("guardedWrite (S4a, epic #1375)", () => {
756771

757772
it("refuses an already-cancelled task's write before any I/O", async () => {
758773
const task = createMockTask()
759-
task.abort = true
774+
task.requestAbort(false)
760775
await expect(guardedWrite(task, "gone.txt", "x", "create")).rejects.toThrow(/was cancelled/)
761776
expect(mockedSafeWriteText).not.toHaveBeenCalled()
762777
expect(mockedFsAccess).not.toHaveBeenCalled()

0 commit comments

Comments
 (0)