Repository navigation
fix(registry): read a seat's runtime chip from its newest install in any pod (TASK-569) #2795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ec51be7
fix(registry): read a seat's runtime chip from its newest install in …
lilyshen0722 f3631b8
fix(registry): order a seat's installs by an identity-only clock, not…
lilyshen0722 db28dad
fix(registry): a re-attach through upsert moves the identity clock (T…
lilyshen0722 448990a
test(registry): both pod-agent routes serve the seat's newest runtime…
lilyshen0722 196d860
test(registry): the reactivation case can fail, and the stamps no lon…
lilyshen0722 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
79 changes: 79 additions & 0 deletions
79
backend/__tests__/unit/routes/registry.pod-agents-seat-runtime.test.js
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| // TASK-569: both pod-agent read routes must hand the seat's newest identity to | ||
| // the payload. The helper suite (registry.seat-runtime-identity.test.js) proves | ||
| // the resolver; this one fails if either route stops passing it through. | ||
| const request = require('supertest'); | ||
| const express = require('express'); | ||
| const mongoose = require('mongoose'); | ||
|
|
||
| const mockViewer = '6a0000000000000000000001'; | ||
| jest.mock('../../../middleware/auth', () => (req, res, next) => { | ||
| req.userId = mockViewer; | ||
| req.user = { id: mockViewer }; | ||
| next(); | ||
| }); | ||
| jest.mock('../../../services/dmService', () => ({ canViewPod: jest.fn().mockResolvedValue(true) })); | ||
|
|
||
| const { setupMongoDb, closeMongoDb, clearMongoDb } = require('../../utils/testUtils'); | ||
| const { AgentInstallation } = require('../../../models/AgentRegistry'); | ||
| const Pod = require('../../../models/Pod'); | ||
| const podAgentsRouter = require('../../../routes/registry/pod-agents'); | ||
|
|
||
| const app = express(); | ||
| app.use(express.json()); | ||
| app.use('/api/registry', podAgentsRouter); | ||
|
|
||
| const viewer = new mongoose.Types.ObjectId(mockViewer); | ||
| const podA = new mongoose.Types.ObjectId(); | ||
| const podB = new mongoose.Types.ObjectId(); | ||
|
|
||
| const install = (podId, runtime, at) => AgentInstallation.create({ | ||
| agentName: 'sprint-review', | ||
| instanceId: 'default', | ||
| podId, | ||
| version: '1.0.0', | ||
| installedBy: viewer, | ||
| status: 'active', | ||
| config: runtime ? { runtime } : {}, | ||
| }).then(async (doc) => { | ||
| if (at) await AgentInstallation.collection.updateOne({ _id: doc._id }, { $set: { runtimeIdentityAt: new Date(at) } }); | ||
| return doc; | ||
| }); | ||
|
|
||
| const IDENTITY = { | ||
| runtimeType: 'webhook', host: 'byo', adapter: 'claude', wrappedCli: 'claude', | ||
| }; | ||
| // sanitizeRuntimeConfig does not echo wrappedCli on this shape; the other three | ||
| // keys reach the payload, and none of pod A's stale codex group survives. | ||
| const expectNewest = (runtime) => { | ||
| expect(runtime).toMatchObject({ runtimeType: 'webhook', host: 'byo', adapter: 'claude' }); | ||
| expect(JSON.stringify(runtime)).not.toContain('codex'); | ||
| }; | ||
|
|
||
| beforeAll(setupMongoDb); | ||
| afterAll(closeMongoDb); | ||
| beforeEach(async () => { | ||
| await Pod.collection.insertMany([podA, podB].map((_id) => ({ | ||
| _id, name: 'p', type: 'chat', createdBy: viewer, members: [viewer], | ||
| }))); | ||
| // Older identity in pod A, newer one in pod B: pod A must read pod B's. | ||
| await install(podA, { | ||
| runtimeType: 'codex', host: 'cloud', adapter: 'codex', wrappedCli: 'codex', | ||
| }, '2026-10-20T00:00:00Z'); | ||
| await install(podB, IDENTITY, '2026-10-25T00:00:00Z'); | ||
| }); | ||
| afterEach(clearMongoDb); | ||
|
|
||
| describe('pod-agent routes read the seat\'s newest runtime identity', () => { | ||
| it('GET /pods/:podId/agents', async () => { | ||
| const res = await request(app).get(`/api/registry/pods/${podA}/agents`); | ||
| expect(res.status).toBe(200); | ||
| const agent = res.body.agents.find((a) => a.name === 'sprint-review' || a.agentName === 'sprint-review'); | ||
| expectNewest(agent.runtime); | ||
| }); | ||
|
|
||
| it('GET /pods/:podId/agents/:name', async () => { | ||
| const res = await request(app).get(`/api/registry/pods/${podA}/agents/sprint-review`); | ||
| expect(res.status).toBe(200); | ||
| expectNewest(res.body.agent.runtime); | ||
| }); | ||
| }); |
187 changes: 187 additions & 0 deletions
187
backend/__tests__/unit/routes/registry.seat-runtime-identity.test.js
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| // TASK-569 (D55): attach writes a seat's runtime identity only onto the install | ||
| // where it ran, so the same seat showed a chip in one pod and none in another. | ||
| // Every pod now reads the seat's identity from its most recently attached | ||
| // install, never the first match, so a harness swap shows everywhere. "Most | ||
| // recent" is runtimeIdentityAt, which only identity writes move: updatedAt | ||
| // moved on a skill-load report and revived a stale harness (Sable on #2795). | ||
| const mongoose = require('mongoose'); | ||
| const { setupMongoDb, closeMongoDb, clearMongoDb } = require('../../utils/testUtils'); | ||
| const { AgentInstallation } = require('../../../models/AgentRegistry'); | ||
| const { | ||
| resolveSeatRuntimeIdentities, | ||
| buildAgentInstallationPayload, | ||
| } = require('../../../routes/registry/helpers'); | ||
|
|
||
| const owner = new mongoose.Types.ObjectId(); | ||
| const podA = new mongoose.Types.ObjectId(); | ||
| const podB = new mongoose.Types.ObjectId(); | ||
| const podC = new mongoose.Types.ObjectId(); | ||
|
|
||
| const install = (podId, runtime, extra = {}) => AgentInstallation.create({ | ||
| agentName: 'sprint-review', | ||
| instanceId: 'default', | ||
| podId, | ||
| version: '1.0.0', | ||
| installedBy: owner, | ||
| status: 'active', | ||
| config: runtime ? { runtime } : {}, | ||
| ...extra, | ||
| }); | ||
|
|
||
| // Rewrites the identity clock directly so the order of attaches is explicit. | ||
| const stamp = (doc, iso) => AgentInstallation.collection.updateOne( | ||
| { _id: doc._id }, { $set: { runtimeIdentityAt: new Date(iso) } }, | ||
| ); | ||
| const raw = (doc) => AgentInstallation.collection.findOne({ _id: doc._id }); | ||
|
|
||
| const runtimeIn = async (doc) => { | ||
| const fresh = await AgentInstallation.findById(doc._id).lean(); | ||
| const identities = await resolveSeatRuntimeIdentities([fresh]); | ||
| return buildAgentInstallationPayload(fresh, { | ||
| runtimeIdentity: identities.get('sprint-review:default') || null, | ||
| }).runtime; | ||
| }; | ||
|
|
||
| beforeAll(setupMongoDb); | ||
| afterAll(closeMongoDb); | ||
| afterEach(clearMongoDb); | ||
|
|
||
| describe('seat runtime identity across pods', () => { | ||
| it('fills an install that declares no runtime from the seat\'s other install', async () => { | ||
| const declared = await install(podA, { runtimeType: 'claude-code', host: 'byo', adapter: 'claude' }); | ||
| const bare = await install(podB, null); | ||
| expect(await runtimeIn(bare)).toMatchObject({ runtimeType: 'claude-code', host: 'byo', adapter: 'claude' }); | ||
| expect(await runtimeIn(declared)).toMatchObject({ runtimeType: 'claude-code', adapter: 'claude' }); | ||
| }); | ||
|
|
||
| it('a seat attached as codex in pod A, then re-attached as claude in pod B, shows claude in both', async () => { | ||
| const a = await install(podA, { runtimeType: 'webhook', host: 'byo', adapter: 'codex' }); | ||
| const b = await install(podB, { runtimeType: 'claude-code', host: 'byo' }); | ||
| await stamp(a, '2020-01-20T00:00:00Z'); | ||
| await stamp(b, '2020-01-25T00:00:00Z'); | ||
| const inA = await runtimeIn(a); | ||
| const inB = await runtimeIn(b); | ||
| // The group moves whole: pod A's older adapter does not outlive pod B's type. | ||
| expect(inA.runtimeType).toBe('claude-code'); | ||
| expect(inA.adapter).toBeUndefined(); | ||
| expect(inB.runtimeType).toBe('claude-code'); | ||
| }); | ||
|
|
||
| it('picks the most recent install, not the first one created', async () => { | ||
| const first = await install(podA, { runtimeType: 'claude-code', host: 'byo' }); | ||
| const second = await install(podB, { runtimeType: 'codex', host: 'byo' }); | ||
| const bare = await install(podC, null); | ||
| await stamp(first, '2020-01-25T00:00:00Z'); | ||
| await stamp(second, '2020-01-21T00:00:00Z'); | ||
| expect((await runtimeIn(bare)).runtimeType).toBe('claude-code'); | ||
| expect((await runtimeIn(second)).runtimeType).toBe('claude-code'); | ||
| }); | ||
|
|
||
| it('ignores uninstalled installs and installs of another instance', async () => { | ||
| await install(podA, { runtimeType: 'codex', host: 'byo' }, { status: 'uninstalled' }); | ||
| await install(podB, { runtimeType: 'codex', host: 'byo' }, { instanceId: 'other' }); | ||
| const bare = await install(podC, null); | ||
| const identities = await resolveSeatRuntimeIdentities([bare]); | ||
| expect(identities.size).toBe(0); | ||
| }); | ||
|
|
||
| it('keeps per-install fields and the hosted type', async () => { | ||
| const hosted = await install(podA, { runtimeType: 'hosted', host: 'cloud', model: 'm-1' }); | ||
| const bare = await install(podB, { model: 'm-2' }); | ||
| await stamp(hosted, '2020-01-25T00:00:00Z'); | ||
| const runtime = await runtimeIn(bare); | ||
| expect(runtime).toMatchObject({ runtimeType: 'hosted', host: 'cloud', model: 'm-2' }); | ||
| }); | ||
|
|
||
| it('a skill-load report on a stale install does not bring its old harness back', async () => { | ||
| const a = await install(podA, { runtimeType: 'webhook', host: 'byo', adapter: 'codex' }); | ||
| const b = await install(podB, { runtimeType: 'claude-code', host: 'byo' }); | ||
| await stamp(a, '2020-01-20T00:00:00Z'); | ||
| await stamp(b, '2020-01-25T00:00:00Z'); | ||
| // The exact write agentsRuntime.ts makes for POST .../skill-load. | ||
| await AgentInstallation.updateOne( | ||
| { _id: a._id, status: 'active' }, | ||
| { $set: { skillLoad: { at: new Date(), mounted: ['x'], conflicted: [], status: 'available' } } }, | ||
| ); | ||
| expect((await runtimeIn(a)).runtimeType).toBe('claude-code'); | ||
| expect((await runtimeIn(b)).runtimeType).toBe('claude-code'); | ||
| }); | ||
|
|
||
| it('a settings save that keeps the identity does not move the clock', async () => { | ||
| const a = await install(podA, { runtimeType: 'webhook', host: 'byo', adapter: 'codex' }); | ||
| const b = await install(podB, { runtimeType: 'claude-code', host: 'byo' }); | ||
| await stamp(a, '2020-01-20T00:00:00Z'); | ||
| await stamp(b, '2020-01-25T00:00:00Z'); | ||
| // The manage-settings.ts shape: the whole config map rewritten, runtime unchanged. | ||
| const doc = await AgentInstallation.findById(a._id); | ||
| const config = Object.fromEntries(doc.config); | ||
| doc.config = new Map(Object.entries({ ...config, wakeOnMessage: { enabled: true } })); | ||
| await doc.save(); | ||
| expect((await raw(a)).runtimeIdentityAt.toISOString()).toBe('2020-01-20T00:00:00.000Z'); | ||
| expect((await runtimeIn(b)).runtimeType).toBe('claude-code'); | ||
| }); | ||
|
|
||
| it('changing an install\'s identity, or reactivating it, makes it the newest', async () => { | ||
| const a = await install(podA, { runtimeType: 'webhook', host: 'byo', adapter: 'codex' }); | ||
| const b = await install(podB, { runtimeType: 'claude-code', host: 'byo' }); | ||
| await stamp(a, '2020-01-20T00:00:00Z'); | ||
| await stamp(b, '2020-01-25T00:00:00Z'); | ||
| const doc = await AgentInstallation.findById(a._id); | ||
| doc.config = new Map(Object.entries({ runtime: { runtimeType: 'codex', host: 'byo' } })); | ||
| await doc.save(); | ||
| expect((await runtimeIn(b)).runtimeType).toBe('codex'); | ||
|
|
||
| // b starts older than a, so only the reactivation stamp can make it win. | ||
| await stamp(a, '2020-01-20T00:00:00Z'); | ||
| await stamp(b, '2020-01-10T00:00:00Z'); | ||
| expect((await runtimeIn(a)).runtimeType).toBe('codex'); | ||
| await AgentInstallation.updateOne({ _id: b._id }, { $set: { status: 'uninstalled' } }); | ||
| const back = await AgentInstallation.findById(b._id); | ||
| back.status = 'active'; | ||
| await back.save(); | ||
| expect((await runtimeIn(a)).runtimeType).toBe('claude-code'); | ||
| }); | ||
|
|
||
| it('stamps the clock when an install is created with an identity, and not without one', async () => { | ||
| const declared = await install(podA, { runtimeType: 'claude-code', host: 'byo' }); | ||
| const bare = await install(podB, null); | ||
| expect((await raw(declared)).runtimeIdentityAt).toBeInstanceOf(Date); | ||
| expect((await raw(bare)).runtimeIdentityAt).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('a stamped install beats rows from before the clock, which fall back to creation order', async () => { | ||
| const legacyOld = await install(podA, { runtimeType: 'codex', host: 'byo' }); | ||
| const legacyNew = await install(podB, { runtimeType: 'webhook', host: 'byo', adapter: 'codex' }); | ||
| await AgentInstallation.collection.updateMany({}, { $unset: { runtimeIdentityAt: '' } }); | ||
| await AgentInstallation.collection.updateOne({ _id: legacyOld._id }, { $set: { createdAt: new Date('2026-09-01T00:00:00Z') } }); | ||
| await AgentInstallation.collection.updateOne({ _id: legacyNew._id }, { $set: { createdAt: new Date('2026-09-02T00:00:00Z') } }); | ||
| expect((await runtimeIn(legacyOld)).adapter).toBe('codex'); | ||
| expect((await runtimeIn(legacyOld)).runtimeType).toBe('webhook'); | ||
| const attached = await install(podC, { runtimeType: 'claude-code', host: 'byo' }); | ||
| await AgentInstallation.collection.updateOne({ _id: attached._id }, { $set: { createdAt: new Date('2026-08-01T00:00:00Z') } }); | ||
| expect((await runtimeIn(legacyNew)).runtimeType).toBe('claude-code'); | ||
| }); | ||
|
|
||
| it('a re-attach through upsert onto an existing row makes it the newest', async () => { | ||
| const a = await install(podA, { runtimeType: 'claude-code', host: 'byo' }); | ||
| const b = await install(podB, { runtimeType: 'webhook', host: 'byo', adapter: 'codex' }); | ||
| await stamp(a, '2020-01-20T00:00:00Z'); | ||
| await stamp(b, '2020-01-25T00:00:00Z'); | ||
| await AgentInstallation.upsert('sprint-review', podA, { | ||
| version: '1.0.0', installedBy: owner, config: { runtime: { runtimeType: 'claude-code', host: 'byo' } }, | ||
| }); | ||
| expect((await runtimeIn(a)).runtimeType).toBe('claude-code'); | ||
| expect((await runtimeIn(b)).runtimeType).toBe('claude-code'); | ||
| }); | ||
|
|
||
| it('an upsert with no runtime (mention auto-join) leaves the clock alone', async () => { | ||
| const a = await install(podA, { runtimeType: 'claude-code', host: 'byo' }); | ||
| await stamp(a, '2020-01-20T00:00:00Z'); | ||
| await AgentInstallation.upsert('sprint-review', podB, { | ||
| version: '1.0.0', installedBy: owner, config: { heartbeat: { enabled: false }, autoJoinSource: 'mention-resolution' }, | ||
| }); | ||
| const joined = await AgentInstallation.findOne({ podId: podB }).lean(); | ||
| expect(joined.runtimeIdentityAt).toBeUndefined(); | ||
| expect((await runtimeIn(joined)).runtimeType).toBe('claude-code'); | ||
| }); | ||
| }); | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔎 Supported by static analysis
Script executed:
Repository: Team-Commonly/commonly
Length of output: 15923
Script executed:
Repository: Team-Commonly/commonly
Length of output: 10185
Script executed:
Repository: Team-Commonly/commonly
Length of output: 45654
Script executed:
Repository: Team-Commonly/commonly
Length of output: 43043
Add cross-pod response tests for both GET routes.
The helper tests do not call either route. Add tests for
GET /pods/:podId/agentsandGET /pods/:podId/agents/:namewith a newer same-seat identity in another pod. Assert all four runtime identity fields in each response. This ensures the tests fail if either route stops passingruntimeIdentity.