test(gardening): cover the node factories and pipeline file events (36 cases, 4 new files) - #2625
test(gardening): cover the node factories and pipeline file events (36 cases, 4 new files)#2625camielvs wants to merge 6 commits into
Conversation
12 cases: node-id prefixing, editor.position parsing (present / absent / malformed JSON), zIndex (default 0, number, numeric string, fractional rounding, non-numeric fallback), taskSpec identity, highlighted=false, callback wrapping (and the empty object when no nodeCallbacks are given), and readOnly precedence. Churn 9 over the sampled window and no dedicated test file. The existing createNodesFromComponentSpec.test.ts covers this factory only transitively and never asserts zIndex or readOnly. Ref: vitest-testing#utility--pure-function-tests
8 cases including the two spread-order behaviours that are easy to break silently: nodeData wins over a colliding input-spec field, and the readOnly argument wins over nodeData.readOnly. Verified by mutation: swapping the spread order fails exactly the collision test and nothing else. Ref: vitest-testing#utility--pure-function-tests
7 cases mirroring createInputNode: id prefixing, label, position, zIndex, spec field pass-through, name/annotations exclusion, readOnly precedence. Ref: vitest-testing#utility--pure-function-tests
9 cases over the pub/sub surface and the per-source write-time bookkeeping: delivery, unsubscribe, multi-listener fan-out, and getLastForeignWriteTime (unknown key, own-source-only, foreign write, latest-per-source, own write not masking a foreign one, per-key isolation). Uses fake timers so the Date.now() stamps are deterministic, and a distinct storage key per test because the module holds its state in module scope. Ref: vitest-testing#utility--pure-function-tests
🎩 PreviewA preview build has been created at: |
…-safe
createTaskNode.test: the callbacks test only asserted expect.any(Function),
so a regression that forwarded the raw nodeCallbacks instead of wrapping
them stayed green. Adds a case that invokes two wrapped callbacks and
asserts the spies received { taskId, nodeId } plus the forwarded argument.
Confirmed it fails against a mutant that skips the injection.
pipelineFileEvents.test: unsubscribe() was the last statement of each
listener test, so a failing assertion leaked a listener into the
module-scoped EventTarget and cascaded into later tests. Registers the
teardown at subscribe time and drains it in afterEach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`createTaskNode`, `createInputNode` and `createOutputNode` each took
`readOnly: boolean = false` and spread it after `...nodeData`, so
`createTaskNode(task, { readOnly: true })` produced a node with
`data.readOnly === false`. The spread order is defensible on its own —
an explicit argument should beat a shared `nodeData` bag — but the
default made an *omitted* argument indistinguishable from an explicit
`false`, so the phantom default silently outranked a real
`nodeData.readOnly`.
Drop the default and resolve explicitly:
readOnly: readOnly ?? nodeData.readOnly ?? false
An explicit argument still wins, an omitted one now inherits from
`nodeData`, and the `?? false` keeps `data.readOnly` a boolean so
nothing downstream shifts.
This matters for `duplicateNodes`, the one caller that omits the
argument: it passes `originalNodeData` for all three node types, so
duplicates were flagged editable regardless of the original. Not
reachable today — every route into `duplicateNodes` is gated on
`readOnly` upstream (the selection toolbar and paste handler in
`FlowCanvas`, and `TaskDetails/Actions.tsx`) — but `data.readOnly` does
drive read-only UI via `TaskNodeProvider` and `IONode`, so the
inheritance should be correct rather than incidentally unused.
`createFlexNode` keeps its `= false` default: it takes no `nodeData`,
so it has no collision to resolve.
Updates the three assertions this PR had pinned on the old behavior, and
adds a `duplicateNodes` case covering the inheritance across all three
node types. Reverting the three factories alone fails all four.
Part of B5 of #2626.
morgan-wowk
left a comment
There was a problem hiding this comment.
🤖 Agent review. The new tests are strong — real factory/event assertions, no snapshots, no flaky (time/random/order) patterns. One thing to flag: despite the test(...) title, this isn't test-only (inline).
| highlighted: false, | ||
| callbacks: dynamicCallbacks, // Use these callbacks internally within the node | ||
| readOnly, | ||
| readOnly: readOnly ?? nodeData.readOnly ?? false, |
There was a problem hiding this comment.
🤖 This is a runtime behaviour change, not just tests: duplicated nodes now inherit readOnly from the original (readOnly ?? nodeData.readOnly ?? false) instead of always being false. The only caller that omits the arg is duplicateNodes (createNodesFromComponentSpec always passes it explicitly, so its branch is unchanged). This is safe only if every duplicateNodes entry point is genuinely gated on readOnly upstream — please confirm that invariant, since data.readOnly drives the read-only UI. Also worth retitling off test(...) since it carries a source behaviour change. (Same pattern in createInputNode.ts / createOutputNode.ts.)
pnpm run test:coveragedoes not run onmasterThe pillar's primary signal is coverage × churn. It was unavailable:
@vitest/coverage-v8@4.1.10is installed againstvitest@3.2.6— a major-version mismatch, so nocoverage report can be produced at all. Two separate defects, neither introduced here:
pnpm run test:coveragefails on a cleanmastercheckout.run."test:coverage": "pnpm vitest --coverage"starts watch mode; in CIit would hang rather than exit. Compare
"test": "vitest run".Update: both are fixed in #2632, which upgrades
vitestto^4.1.10to match the installed@vitest/coverage-v8, addsrunto the script, and setscoverage.include(v4 otherwise reports onlyfiles loaded during the run). Coverage should be available as a ranking signal for the next pass.
This run substituted churn × no-co-located-test-file over
src/{utils,hooks,lib,services}as the ranking signal, which found 13 candidate files. That substitutionis a weaker signal than real coverage — it cannot see partially-covered files — and is disclosed here
rather than presented as equivalent.
What was added
src/utils/nodes/createTaskNode.tscreateTaskNode.test.tssrc/utils/nodes/createInputNode.tscreateInputNode.test.tssrc/utils/nodes/createOutputNode.tscreateOutputNode.test.tssrc/services/pipelineStorage/pipelineFileEvents.tspipelineFileEvents.test.tsThe three node factories were not untested in the coverage sense —
createNodesFromComponentSpec.test.tsdrives them indirectly. But that test asserts only
id/position/type/taskId/taskSpec, andnever touches
zIndexorreadOnly. The new files cover what it does not:zIndexextraction — default (0for all three types, fromZ_INDEX_RANGES), numeric annotation,numeric string annotation, fractional value (rounded), and non-numeric string (falls back to default).
{ x: 0, y: 0 }.createInputNode/createOutputNodebuilddataas{...rest, ...nodeData, label, readOnly}, sonodeDatawins over a colliding input-spec field. (ThereadOnlyassertions originally in thisgroup have been rewritten — see the ruling section below.)
createTaskNodelikewise always forceshighlighted: false.nameandannotationsare deliberately not copied into node data (they are destructured out).pipelineFileEvents— delivery, unsubscribe, multi-listener fan-out, and every branch ofgetLastForeignWriteTime: unknown key, own-source-only, a foreign write, latest-per-source, an ownwrite not masking an earlier foreign one, and per-key isolation.
The assertions were checked for teeth, not just green
createInputNode's spread order was mutated in place ({...rest, ...nodeData}→{...nodeData, ...rest})and the suite re-run:
Exactly one test failed, and it was the right one. The mutation was reverted.
The
readOnlyprecedence ruling (B5)The checklist item below asked for a decision. It is a bug, and the fix is now the last commit here.
All three factories declared
readOnly: boolean = falseand spread it after...nodeData, socreateTaskNode(task, { readOnly: true })yieldeddata.readOnly === false. The spread order isdefensible — an explicit argument should beat a shared
nodeDatabag — but the default is not: it madean omitted argument indistinguishable from an explicit
false, so a phantom default outranked a real value.An explicit argument still wins, an omitted one now inherits from
nodeData, and?? falsekeepsdata.readOnlya boolean so nothing downstream shifts.Why it matters:
duplicateNodesis the one caller that omits the argument — it passes
originalNodeDatafor all three node types — soduplicates were flagged editable regardless of the original. Not reachable today: every route into
duplicateNodesis gated onreadOnlyupstream (selection toolbar atFlowCanvas.tsx:1226, paste at:1088, node action atTaskDetails/Actions.tsx:64). Butdata.readOnlydoes drive read-only UI throughTaskNodeProvider.tsx:189andIONode.tsx:92, so the inheritance should be correct rather thanincidentally unused.
createFlexNodekeeps its= falsedefault — it takes nonodeData, so it has nocollision to resolve.
Test changes: the three pinned assertions now read argument →
nodeData→falseinstead ofdocumenting the clobber, and a new
duplicateNodescase covers inheritance across all three node types.Reverting the three factories alone fails exactly those four tests and nothing else.
duplicateNodes.test.ts)typecheck·lint·knip·prettier✅ — 195 files / 2,004 tests (was 191 / 1,966)0.85(tests, config)Reviewer checklist
(mandatory:
requiresBehaviorReview: true)The engine requires every PR to survive the
reviewskill first, but that skill isdisable-model-invocationand only a human can run it.Decide whether thereadOnlyprecedence I pinned down is the behavior you want.Resolved: ruled a bug. The source is fixed and the three assertions rewritten in this PR — see
the ruling section. Review that commit on its own merits;
it is the only source change here.
Deliberately not done
it.todos inhydrateComponentReference.test.ts(:892,:979) are left alone. Their owntext says the behavior is undecided — "todo: decide if this case is valid" and "todo: decide how we
should handle this". Implementing them means inventing intended behavior for
text-vs-spec conflicts and for invalid-YAML-with-valid-spec. Queued for a human decision instead.
tests/e2e/aggregator.spec.ts:49test.skip("should add dynamic inputs when connection is madeto add-input handle") is left skipped. Un-skipping it without being able to run Playwright here would
be shipping an unverified spec.
pnpm run test:e2e:cineeds browsers and a dev server that this environment doesnot provide, so per the pillar I am not claiming a pass — and I would rather add none than add specs I
could not execute.
generateDynamicNodeCallbacks(churn 3) has no new test, and the reason is a type bug worthfixing first: it is declared to return
NodeCallbacks, whose members take(ids: NodeAndTaskId, ...args),but the values it actually returns are wrappers that take only the trailing args (they supply
idsthemselves). Calling the result the way its own type describes would pass the ids object twice, so an
honest test needs an
as unknown asdouble cast — whichtypescript-standards#avoid-unsafe-type-castingrules out. Queued as a source fix; the call-through path is already covered indirectly by
createNodesFromComponentSpec.test.ts:176.Update: fixed in fix: correct generateDynamicNodeCallbacks return type #2633, which corrects the return type to
TaskNodeCallbacksand adds the 6 teststhis pillar could not write without the cast. No change needed here.
src/services/googleDrive/*andpipelineStorage/{db,createDriver,pipelineRegistry}.tswereranked but skipped this run: they need IndexedDB / Google API mock scaffolding, which is a larger
design decision than a gardening pass should make unilaterally.
Method notes
master, not as the earlier pillars' branches left it; noneof the 4 files it touched overlaps the 46 files claimed by PRs garden(comments): weekly groundskeeping — 2026-W33 #2620–chore(gardening): adopt UI primitives + tone prop where the class delta is provably zero (18 swaps) #2624.