test(cli): pin the daemon's reduction of the server podIds union (TASK-019) - #1760
Merged
Merged
Conversation
samxu01
force-pushed
the
kai/task019-daemon-pod-binding
branch
from
September 18, 2026 17:43
1f34cb9 to
5cea73b
Compare
…K-019) The server's /assigned row carries podIds as the union of the pods a seat's owner installed it into (backend/routes/agentBinding.ts); a token record holds one pod, so the daemon reduces the union at daemon-supervisor.js:290 with row.podIds?.[0] || null. Only the single-element case was covered. Two tests, each mutation-proven on its own: the reducer takes the FIRST declared pod (binding the last instead reds only this test, since single-element fixtures cannot tell), and a seat the server declares no pod for binds null for both shapes of 'declares none' (defaulting to a pod instead of null reds only this one). Deliberately NOT pinned: which pod the server puts first. That ordering is the projection's decision - the find at :394 carries no sort and taking the first non-empty value per field is per-field - so this is a witness for whichever change makes the projection deterministic rather than a lock on the accident. cli 39 suites / 539 passed / 10 skipped, lint:cli clean. Test-only, so no version bump: the package-version guard watches cli/src.
lilyshen0722
force-pushed
the
kai/task019-daemon-pod-binding
branch
from
September 19, 2026 15:56
5cea73b to
1ffb750
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The third piece of the same row, on the side the row is actually named for. TASK-019 is "Daemon multi-installation projection" — and until now the daemon's own reduction of the server's multi-installation list was untested.
The server's
/assignedrow carriespodIdsas the union of the pods a seat's owner installed it into (backend/routes/agentBinding.ts:417pushes one per installation). A token record can hold exactly one pod, so the daemon reduces the union atcli/src/lib/daemon-supervisor.js:290:The only fixture in
cli/__tests__/daemon-supervisor.test.mjsdeclarespodIds: ['pod-1']— a one-element union, where the reducer and the accident agree. Two tests added:binds the local record to the first pod of the server-side union, not the smallest id (TASK-019)— a three-element union in deliberately non-alphabetical order (['pod-b','pod-a','pod-c'], expectspod-b); the record binds the first element it is given.binds null when the server declares no pod for the seat— both shapes of "declares none" (empty array, absent field).Mutation proof, one mutation per test
daemon-supervisor.js:290row.podIds?.[0]→row.podIds?.at(-1)|| null→|| 'pod-last-resort'|| nulldropped, so the field isundefinedtoHaveProperty('podId', null)is doing the work)row.podIds?.[0]→[...(row.podIds || [])].sort()[0]All four restored; the diff is the test file alone.
The first version of test 1 survived a client-side sort. With the original alphabetical fixture (
['pod-oldest','pod-second','pod-third'])podIds[0]andsort()[0]agree, so[...podIds].sort()[0]passed 28/28 — found by @vera while gating this head. The fixture is now out of order and expectspod-b, so a daemon-side re-ordering reds the test. That matters beyond the mutation: if the ruling lands as a server-side ascending sort, a client-side sort would be a second ordering decision, and the alphabetical fixture could not tell the two apart. A sort makes an invention repeatable, not correct — and the daemon takes the first element it is handed.What this deliberately does NOT pin
Which pod the server puts first. That is the projection's decision, and on main it is an accident: the find at
agentBinding.ts:394carries no.sort(), and:418/:419take the first non-empty value per field, so runtime and environment can come from different installations. Pinningpod-oldestas "correct" would freeze today's insertion order.The reducer is the contract; the ordering is the open question. That question is unchanged by this PR and is on the row with a ruling pending: runtime+environment should move as a pair from a single installation (a sorted mix is a repeatable invention —
environmentForoverlays runtimemodel/effortinto a declared environment while the adapter comes fromruntime.adapter, so per-field mixing runs A's adapter with B's model),podIdsstays a union, and the sort at:394is needed either way.This PR is a witness for whichever change makes the projection deterministic — merge it before or after that fix and it holds.
A second consumer, found while gating this head (adds a requirement to the fix)
@vera traced
podIdsand the runtime/environment pair acrosscli/,commonly-mcp/andbackend/routes|servicesand found one more consumer, which inverts the precedence in this same file:environmentFor(:136-143) treats a declared environment'smodelauthoritative and overlaysruntime.modelonly where the environment is silent.:357-359setsseat.model = row.runtime?.model || localToken?.environment?.model || …, i.e. runtime wins, andagentStates()(:73) ships that on the machine heartbeat (:381).Both verified on the tree at
5cea73bd. So with a single installation,runtime.model='opus'+environment.model='sonnet'spawns sonnet and reports opus — the per-field mix makes it worse, but the inversion does not need multiple installations to exist. Consequence for the fix shape: pair-atomic projection is necessary and not sufficient — the spawn path and the status path need one shared precedence rule, or the heartbeat keeps advertising a model that is not running.Checked and clear, same trace:
grantServersForIdentities(agentBinding.ts:189/209) usespodIdsas a set (membership /$in), so order does not matter there; the per-grant MCP entries attach to whichever install's environment won, so they follow the pair ruling;cli/src/commands/dev.js:356is an unrelated local-accountpodIds; no other CLI or MCP reader of/assigned. Scope limit: that was agit grep, not a live-cluster read.Why the union is load-bearing at all
Because of this consumer.
daemon-supervisor.js:290collapsing the union by index is the measured reason a whole-row atomicity design (taking everything from one installation) would silently drop a seat's other pods: a seat installed in three of its owner's pods would bind whichever one survived. Verified on main, cited from the TASK-019 row.Verification
cli: 39 suites / 539 passed / 10 skipped, green.lint:cli: clean.node --checkon the test file: clean.cli/src— which this does not touch.origin/main(ac544d5c) withgit log --oneline origin/main..HEADrun before opening: one commit, one file.Honest limits
/assignedresponse — this is a unit-level pin of the reducer with the harness's injected client, same as every other test in this file.kubectlfails:gke-gcloud-auth-plugin not found).bootstrapAgentRecordFromEnv, theagent runpath) and test(agent-binding): pin the daemon work-list projection #1759 (server projection contracts).