Skip to content

test(gardening): cover the node factories and pipeline file events (36 cases, 4 new files) - #2625

Open
camielvs wants to merge 6 commits into
masterfrom
automated-gardening/tests/2026-W33
Open

test(gardening): cover the node factories and pipeline file events (36 cases, 4 new files)#2625
camielvs wants to merge 6 commits into
masterfrom
automated-gardening/tests/2026-W33

Conversation

@camielvs

@camielvs camielvs commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

🤖 Automated draft PR — opened by the gardening skill. Nothing here auto-merges.
4 new test files, 36 new cases. The pillar's rule is that a test which passes while asserting the wrong
thing is worse than no test, so every assertion here was checked against the implementation, and the
riskiest one was mutation-tested (below).

Update — the readOnly question below has been answered, and this PR now carries the fix.
The pillar flagged the readOnly precedence it had pinned and asked for a ruling. The ruling is bug,
so a source commit was added here rather than left to a follow-up. See
The readOnly precedence ruling — this PR is no longer
source-change-free
.

pnpm run test:coverage does not run on master

The pillar's primary signal is coverage × churn. It was unavailable:

SyntaxError: The requested module 'vitest/node' does not provide an export named 'BaseCoverageProvider'
  ❯ loadProvider node_modules/.../@vitest/coverage-v8/dist/load-provider-CdgAx3rL.js:4:33

@vitest/coverage-v8@4.1.10 is installed against vitest@3.2.6 — a major-version mismatch, so no
coverage report can be produced at all. Two separate defects, neither introduced here:

  1. The peer versions disagree. pnpm run test:coverage fails on a clean master checkout.
  2. The script omits run. "test:coverage": "pnpm vitest --coverage" starts watch mode; in CI
    it would hang rather than exit. Compare "test": "vitest run".

Update: both are fixed in #2632, which upgrades vitest to ^4.1.10 to match the installed
@vitest/coverage-v8, adds run to the script, and sets coverage.include (v4 otherwise reports only
files 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 substitution
is a weaker signal than real coverage — it cannot see partially-covered files — and is disclosed here
rather than presented as equivalent.

What was added

File under test Churn New file Cases
src/utils/nodes/createTaskNode.ts 9 createTaskNode.test.ts 12
src/utils/nodes/createInputNode.ts 5 createInputNode.test.ts 8
src/utils/nodes/createOutputNode.ts 5 createOutputNode.test.ts 7
src/services/pipelineStorage/pipelineFileEvents.ts 1 pipelineFileEvents.test.ts 9

The three node factories were not untested in the coverage sense — createNodesFromComponentSpec.test.ts
drives them indirectly. But that test asserts only id / position / type / taskId / taskSpec, and
never touches zIndex or readOnly. The new files cover what it does not:

  • zIndex extraction — default (0 for all three types, from Z_INDEX_RANGES), numeric annotation,
    numeric string annotation, fractional value (rounded), and non-numeric string (falls back to default).
  • Position fallbacks — absent annotation and malformed JSON both yield { x: 0, y: 0 }.
  • Spread precedence, which is the part most likely to break silently:
    • createInputNode/createOutputNode build data as {...rest, ...nodeData, label, readOnly}, so
      nodeData wins over a colliding input-spec field. (The readOnly assertions originally in this
      group have been rewritten — see the ruling section below.)
    • createTaskNode likewise always forces highlighted: false.
  • name and annotations are deliberately not copied into node data (they are destructured out).
  • pipelineFileEvents — delivery, unsubscribe, multi-listener fan-out, and every branch of
    getLastForeignWriteTime: unknown key, own-source-only, a foreign write, latest-per-source, an own
    write 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:

× createInputNode > lets nodeData win over a colliding input spec field
  Tests  1 failed | 7 passed (8)

Exactly one test failed, and it was the right one. The mutation was reverted.

The readOnly precedence 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 = false and spread it after ...nodeData, so
createTaskNode(task, { readOnly: true }) yielded data.readOnly === false. The spread order is
defensible — an explicit argument should beat a shared nodeData bag — but the default is not: it made
an omitted argument indistinguishable from an explicit false, so a phantom default outranked a real value.

readOnly?: boolean            // was: readOnly: boolean = false
// …
readOnly: readOnly ?? nodeData.readOnly ?? false

An explicit argument still wins, an omitted one now inherits from nodeData, and ?? false keeps
data.readOnly a boolean so nothing downstream shifts.

Why it matters: duplicateNodes
is 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 (selection toolbar at FlowCanvas.tsx:1226, paste at
:1088, node action at TaskDetails/Actions.tsx:64). But data.readOnly does drive read-only UI through
TaskNodeProvider.tsx:189 and IONode.tsx:92, 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.

Test changes: the three pinned assertions now read argument → nodeDatafalse instead of
documenting the clobber, and a new duplicateNodes case covers inheritance across all three node types.
Reverting the three factories alone fails exactly those four tests and nothing else.

Test files added 4
Cases added 37 (36 + 1 for the ruling)
Existing test files edited 1 (duplicateNodes.test.ts)
Source files changed 3 (the node factories — see the ruling above)
Validation typecheck · lint · knip · prettier ✅ — 195 files / 2,004 tests (was 191 / 1,966)
Confidence threshold 0.85 (tests, config)

Reviewer checklist

  • Each new/updated assertion reflects intended behavior, not merely that the test passes
    (mandatory: requiresBehaviorReview: true)
  • ⚠️ Self-review was skipped (review skill unavailable) — review this diff manually.
    The engine requires every PR to survive the review skill first, but that skill is
    disable-model-invocation and only a human can run it.
  • Decide whether the readOnly precedence 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.
  • No e2e specs were added — see below.

Deliberately not done

  • The 2 it.todos in hydrateComponentReference.test.ts (:892, :979) are left alone. Their own
    text 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:49 test.skip ("should add dynamic inputs when connection is made
    to add-input handle") is left skipped. Un-skipping it without being able to run Playwright here would
    be shipping an unverified spec.
  • No new e2e specs. pnpm run test:e2e:ci needs browsers and a dev server that this environment does
    not 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 worth
    fixing 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 ids
    themselves). Calling the result the way its own type describes would pass the ids object twice, so an
    honest test needs an as unknown as double cast — which typescript-standards#avoid-unsafe-type-casting
    rules 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 TaskNodeCallbacks and adds the 6 tests
    this pillar could not write without the cast. No change needed here.
  • src/services/googleDrive/* and pipelineStorage/{db,createDriver,pipelineRegistry}.ts were
    ranked 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

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
@camielvs camielvs added the automated-gardening Automated codebase gardening label Aug 13, 2026
@camielvs
camielvs requested a review from a team August 13, 2026 20:14
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: automated-gardening/tests/2026-W33/d1868fa

@camielvs
camielvs marked this pull request as ready for review August 13, 2026 21:29
Comment thread src/utils/nodes/createTaskNode.test.ts
Comment thread src/services/pipelineStorage/pipelineFileEvents.test.ts
camielvs and others added 2 commits August 13, 2026 15:27
…-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 morgan-wowk left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automated-gardening Automated codebase gardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants