From 1a6d69d125d154cd66c89c2d55e31c6fe150d597 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Tue, 20 Oct 2026 13:23:37 -0400 Subject: [PATCH 1/4] test: reproduce cross-project topic proposal context rejection --- .../communication-source-context.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts b/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts index 9d8ef66d4..bda13f998 100644 --- a/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts +++ b/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts @@ -1,11 +1,18 @@ +import { createHash } from 'node:crypto'; import { createServer } from 'node:http'; import { describe, expect, it, vi } from 'vitest'; import { agentDefinitionSchema } from '@treeseed/sdk/agent-capacity'; import { workItemContext } from '../../../../../../../src/api/capacity/services/build/ready-execution-node.ts'; import { projectCommunicationInvocations } from '../../../../../../../src/api/capacity/policy/execution/communication-execution-projector.ts'; import { ControlPlaneStore } from '../../../../../../../src/api/persistence/store.ts'; +import { readyProposal } from '../../../../governance/proposals/architecture/ready-proposal-fixture.ts'; import { postgresGraph } from '../../graph/architecture/living/living-postgres-fixture.ts'; +const gateway = vi.hoisted(() => ({ readRepositoryFile: vi.fn() })); +vi.mock('../../../../../../../src/api/knowledge/gateway-treedx-connection.ts', () => ({ + resolveKnowledgeGatewayConnection: vi.fn(async () => ({ repositoryId: 'proposal-library', client: gateway })), +})); + const commit = 'a'.repeat(40); const repository = { id: 'software', role: 'software', provider: 'github', owner: 'example', name: 'project', currentBranch: 'staging' }; function communication(tools = ['discussion', 'source.read']) { @@ -21,6 +28,30 @@ function communication(tools = ['discussion', 'source.read']) { const sourceRef = { store: 'git', model: 'repository', id: 'software', repository: 'example/project', commit }; describe('standalone communication exact source admission', () => { + it('reads an attached same-team proposal across topic project streams without granting foreign writes or following later versions', async () => { + const bytes = 'exact attached Admin proposal', digest = createHash('sha256').update(bytes).digest('hex'); + const proposal = readyProposal(), sharedGit = { store: 'git', model: 'repository', id: 'producer', repository: 'example/producer', commit: 'c'.repeat(40) }; + Object.assign(proposal, { id: 'shared', projectId: 'proposal-project', groupIds: ['portfolio-atlas'] }); + Object.assign(proposal.executionPlan.workItems[0]!, { contextRefs: [sharedGit] }); + gateway.readRepositoryFile.mockResolvedValue({ resolvedRef: 'b'.repeat(40), file: { content: bytes, frontmatter: proposal } }); + const frozen = { kind: 'proposal', id: 'shared', projectId: 'proposal-project', immutableRef: 'b'.repeat(40), path: 'proposals/shared.mdx', digest: `sha256:${digest}` }; + for (const [teamId, owner] of [['team', 'proposal-project'], ['foreign', 'proposal-project'], ['team', 'forged-project']]) { + const node = communication(), held = structuredClone(node), ref = structuredClone(frozen); + const store = { config: { fetchImpl: vi.fn(async () => new Response(commit)) }, all: async () => [], + listHubRepositories: async () => [repository], getProjectTreeDxLibrary: async () => ({ repositoryId: 'proposal-library' }), + getGovernanceProposal: async () => ({ id: 'shared', teamId, projectId: owner, activeVersion: 2, + metadata: { contentProvenance: { repositoryId: 'proposal-library', commitSha: 'f'.repeat(40), contentPath: frozen.path, digest: 'f'.repeat(64) } } }), + first: async (sql: string) => sql.includes('governance_proposal_versions') ? { version: 1 } + : { content_refs_json: [frozen], requested_at: '2026-10-20T12:00:00.000Z' } }; + const read = workItemContext(store, node); + if (teamId === 'team' && owner === frozen.projectId) expect(await read).toEqual([ + expect.objectContaining({ store: 'treedx', model: 'proposal', id: 'shared', repository: 'proposal-library', commit: frozen.immutableRef, revision: 1 }), sharedGit, sourceRef, + ]); + else await expect(read).rejects.toMatchObject({ code: 'communication_proposal_context_stale' }); + expect(node).toEqual(held); expect(frozen).toEqual(ref); + } + }); + it('freezes configured source-read Git context before issuing an arbitrary configured chat assignment', async () => { const node = communication(), before = structuredClone(node); const fetchImpl = vi.fn(async () => new Response(commit)); From 4f7b07b3a979cf4cc8d54608c9eb06af387a6743 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Tue, 20 Oct 2026 13:24:39 -0400 Subject: [PATCH 2/4] test: retain team denial while admitting shared proposal discussion streams --- .../planning-and-estimate-operations.test.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/unit/control-plane/capacity/planning-and-estimate-operations.test.ts b/tests/unit/control-plane/capacity/planning-and-estimate-operations.test.ts index d319b2650..7d326facb 100644 --- a/tests/unit/control-plane/capacity/planning-and-estimate-operations.test.ts +++ b/tests/unit/control-plane/capacity/planning-and-estimate-operations.test.ts @@ -8,6 +8,11 @@ vi.mock('../../../../src/api/discussions/content.ts', async original => ({ loadDiscussions: vi.fn(async () => ({ messages: [] })), })); +const exactProposal = vi.hoisted(() => vi.fn()); +vi.mock('../../../../src/api/governance/executable-proposal.ts', async original => ({ + ...await original(), readExactProposal: exactProposal, +})); + const principal = { id: 'user-a', roles: [], permissions: [] }; function store(overrides: Record = {}) { @@ -102,11 +107,29 @@ describe('communication catalog operations', () => { expect(create.mock.calls.map((call) => call[1].projectId).sort()).toEqual(['project-api', 'project-sdk']); }); - it('fails closed when a communication proposal is outside the addressed project', async () => { - const create = vi.fn(); + it('attaches one owning-project proposal unchanged to another same-team project discussion stream', async () => { + const create = vi.fn(async () => ({ invocations: [] })); + const ref = { store: 'treedx', model: 'proposal', id: 'proposal-a', repository: 'library-b', commit: 'b'.repeat(40), path: 'proposals/a.mdx', digest: `sha256:${'c'.repeat(64)}` }; + exactProposal.mockResolvedValue({ ref }); const service = createCommunicationService(store({ async getProjectDetails() { return { project: { id: 'project-a', slug: 'sdk', teamId: 'team-a' } }; }, async getGovernanceProposal() { return { id: 'proposal-a', teamId: 'team-a', projectId: 'project-b', status: 'open' }; }, + async first(query: string) { + if (query.includes('communication_discussion_topics')) return { id: 'topic-a', slug: 'agent-chat', status: 'active' }; + if (query.includes('communication_discussion_streams')) return { id: 'stream-a', discussion_id: 'agent-chat' }; + return null; + }, async run() {}, async all() { return []; }, + }), { create }); + await expect(service.send(principal, 'team-a', 'Agent Chat', { message: '@sdk/architect Review the shared proposal.', proposalId: 'proposal-a' }, 'request-a')) + .rejects.toMatchObject({ code: 'communication_send_not_found' }); + expect(create).toHaveBeenCalledWith(principal, expect.objectContaining({ projectId: 'project-a', contextRefs: [{ kind: 'proposal', id: 'proposal-a', projectId: 'project-b', immutableRef: ref.commit, path: ref.path, digest: ref.digest }] }), 'request-a:project-a'); + }); + + it('fails closed when a communication proposal belongs to another team', async () => { + const create = vi.fn(); + const service = createCommunicationService(store({ + async getProjectDetails() { return { project: { id: 'project-a', slug: 'sdk', teamId: 'team-a' } }; }, + async getGovernanceProposal() { return { id: 'proposal-a', teamId: 'foreign-team', projectId: 'project-b', status: 'open' }; }, async first(query: string) { if (query.includes('communication_discussion_topics')) return { id: 'topic-a', slug: 'agent-chat', status: 'active' }; return null; From 17982511458ee322a51b2e1974ec26cb9f29b0ed Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Tue, 20 Oct 2026 13:25:42 -0400 Subject: [PATCH 3/4] test: reproduce shared proposal replay and owning context validation --- .../completion/proposal-replay.test.ts | 21 ++++++++++++++++--- .../communication-source-context.test.ts | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/unit/control-plane/capacity/communication/completion/proposal-replay.test.ts b/tests/unit/control-plane/capacity/communication/completion/proposal-replay.test.ts index 78c2c8869..9e35c7d7b 100644 --- a/tests/unit/control-plane/capacity/communication/completion/proposal-replay.test.ts +++ b/tests/unit/control-plane/capacity/communication/completion/proposal-replay.test.ts @@ -1,5 +1,6 @@ import { createHash } from 'node:crypto'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { validateDiscussionContextRefs } from '../../../../../../src/api/discussions/content.ts'; import { createCommunicationService } from '../../../../../../src/api/control-plane/repositories/capacity/communication-service.ts'; const mocks = vi.hoisted(() => ({ history: vi.fn(), exact: vi.fn(), targets: vi.fn() })); @@ -41,11 +42,12 @@ beforeEach(() => { }); describe('communication recovery after a proposal edit', () => { - it('reuses the committed message context instead of resolving the moved proposal', async () => { - const f = fixture(); + it.each(['project', 'producer-project'])('reuses committed %s context instead of resolving the moved proposal', async owner => { + const f = fixture(), heldRef = { ...originalRef, projectId: owner }; + mocks.history.mockResolvedValue({ messages: [{ ...source, frontmatter: { ...source.frontmatter, contextRefs: [heldRef] } }] }); const result = await f.service.send(principal, 'team', 'mcp-coordination', { message, proposalId: 'proposal' }, key); expect(result.replayed).toBe(true); - expect(f.created).toHaveBeenCalledWith(principal, expect.objectContaining({ contextRefs: [originalRef] }), `${key}:project`); + expect(f.created).toHaveBeenCalledWith(principal, expect.objectContaining({ contextRefs: [heldRef] }), `${key}:project`); expect(f.store.getGovernanceProposal).not.toHaveBeenCalled(); expect(mocks.exact).not.toHaveBeenCalled(); expect(mocks.history).toHaveBeenCalledWith(expect.objectContaining({ exactMessageIds: [messageId] })); @@ -64,6 +66,19 @@ describe('communication recovery after a proposal edit', () => { }); describe('discussion replay before invocation admission', () => { + it('validates a shared proposal by its owning project and team while other foreign context remains denied', async () => { + const ref = { ...originalRef, projectId: 'producer-project' }; + for (const visible of [true, false]) { + const store = { first: vi.fn(async () => visible ? { id: 'proposal' } : null) }; + const read = validateDiscussionContextRefs({ store, teamId: 'team', projectId: 'project', values: [ref] }); + if (visible) expect(await read).toEqual([ref]); + else await expect(read).rejects.toMatchObject({ code: 'discussion_context_proposal_stale' }); + expect(store.first).toHaveBeenCalledWith(expect.stringContaining('team_id = ? AND project_id = ?'), ['proposal', 'team', 'producer-project']); + await expect(validateDiscussionContextRefs({ store, teamId: 'team', projectId: 'project', values: [{ ...ref, kind: 'assignment' }] })) + .rejects.toMatchObject({ code: 'discussion_context_project_forbidden' }); + } + }); + it('rejects changed exact context when a concurrent writer already committed the message', async () => { const { createDiscussionService } = await import('../../../../../../src/api/discussions/discussion-service.ts'); const f = fixture(); diff --git a/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts b/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts index bda13f998..1c2a36bed 100644 --- a/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts +++ b/tests/unit/control-plane/capacity/execution/architecture/communication/communication-source-context.test.ts @@ -28,7 +28,7 @@ function communication(tools = ['discussion', 'source.read']) { const sourceRef = { store: 'git', model: 'repository', id: 'software', repository: 'example/project', commit }; describe('standalone communication exact source admission', () => { - it('reads an attached same-team proposal across topic project streams without granting foreign writes or following later versions', async () => { + it('reads an attached same-team proposal across topic project streams with contributor source context and the pinned version', async () => { const bytes = 'exact attached Admin proposal', digest = createHash('sha256').update(bytes).digest('hex'); const proposal = readyProposal(), sharedGit = { store: 'git', model: 'repository', id: 'producer', repository: 'example/producer', commit: 'c'.repeat(40) }; Object.assign(proposal, { id: 'shared', projectId: 'proposal-project', groupIds: ['portfolio-atlas'] }); From 297951515ecc1a226c7dd7e09a26d6e2df7f3835 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Tue, 20 Oct 2026 13:35:02 -0400 Subject: [PATCH 4/4] fix: attach shared proposal context across same-team topic streams --- .../services/build/ready-execution-node.ts | 8 ++++---- .../capacity/communication-service.ts | 8 ++++---- src/api/discussions/content.ts | 4 ++-- .../completion/proposal-replay-native.test.ts | 18 ++++++++++-------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/api/capacity/services/build/ready-execution-node.ts b/src/api/capacity/services/build/ready-execution-node.ts index b3bf4c625..5a71ec16f 100644 --- a/src/api/capacity/services/build/ready-execution-node.ts +++ b/src/api/capacity/services/build/ready-execution-node.ts @@ -154,12 +154,12 @@ export async function workItemContext(store: any, node: ExecutionNode): Promise< continue; } const reference = record(value); - if (reference.kind !== 'proposal' || text(reference.projectId) !== node.projectId || !text(reference.id)) { + if (reference.kind !== 'proposal' || !text(reference.projectId) || !text(reference.id)) { throw new CapacityGovernanceError('communication_context_reference_invalid', `Communication node ${node.id} contains an unsupported context reference.`, 409); } const proposal = await store.getGovernanceProposal(text(reference.id)); if (!proposal || text(proposal.teamId ?? proposal.team_id) !== node.teamId - || text(proposal.projectId ?? proposal.project_id) !== node.projectId) { + || text(proposal.projectId ?? proposal.project_id) !== text(reference.projectId)) { throw new CapacityGovernanceError('communication_proposal_context_stale', `Communication node ${node.id} references an unavailable proposal.`, 409); } const digest = text(reference.digest).replace(/^sha256:/u, ''); @@ -173,12 +173,12 @@ export async function workItemContext(store: any, node: ExecutionNode): Promise< // invalidating already-admitted planning conversation. const exact = await readExactProposal(store, proposal, { store: 'treedx', model: 'proposal', id: text(reference.id), revision: Number(version.version), - repository: source.repository, commit: text(reference.immutableRef), + repository: text(record(record(proposal.metadata ?? proposal.metadata_json).contentProvenance).repositoryId, source.repository), commit: text(reference.immutableRef), path: text(reference.path), digest: text(reference.digest), }); const workItemRefs = array(record(exact.definition.executionPlan).workItems) .flatMap((workItem) => array(record(workItem).contextRefs)); - resolved.push(exact.ref, ...await canonicalProposalContextRefs(store, node, workItemRefs)); + resolved.push(exact.ref, ...await canonicalProposalContextRefs(store, { ...node, projectId: text(reference.projectId) }, workItemRefs)); } return projectSourceContext(store, node, resolved); diff --git a/src/api/control-plane/repositories/capacity/communication-service.ts b/src/api/control-plane/repositories/capacity/communication-service.ts index 55e5af756..c2084b7fd 100644 --- a/src/api/control-plane/repositories/capacity/communication-service.ts +++ b/src/api/control-plane/repositories/capacity/communication-service.ts @@ -317,21 +317,21 @@ export function createCommunicationService(store: any, discussions?: { create(pr if (text(original.body) !== text(body.message) || text(frontmatter.parentWorkdayId) !== text(body.parentWorkdayId) || JSON.stringify(recipients) !== JSON.stringify(projectTargets.map(target => target.agentSlug).sort()) - || (proposalId ? proposals.length !== 1 || proposals[0]!.id !== proposalId || proposals[0]!.projectId !== projectId : proposals.length !== 0)) { + || (proposalId ? proposals.length !== 1 || proposals[0]!.id !== proposalId || !text(proposals[0]!.projectId) : proposals.length !== 0)) { throw new CapacityOperationError(409, 'discussion_idempotency_conflict', 'The idempotency key is already bound to another discussion request.'); } contextRefs.push(...frozen); } else if (proposalId) { const proposal = await store.getGovernanceProposal(proposalId); if (!proposal || text(proposal.teamId, text(proposal.team_id)) !== teamId - || text(proposal.projectId, text(proposal.project_id)) !== projectId) { - throw new CapacityOperationError(404, 'communication_proposal_not_found', 'The selected proposal does not belong to this team and project.'); + || !text(proposal.projectId, text(proposal.project_id))) { + throw new CapacityOperationError(404, 'communication_proposal_not_found', 'The selected proposal does not belong to this team.'); } if (!['open', 'discussing'].includes(text(proposal.status))) { throw new CapacityOperationError(409, 'communication_proposal_not_open', 'Discussion context requires an open proposal.'); } const exact = await readExactProposal(store, proposal); - contextRefs.push({ kind: 'proposal', id: proposalId, projectId, + contextRefs.push({ kind: 'proposal', id: proposalId, projectId: text(proposal.projectId, text(proposal.project_id)), immutableRef: exact.ref.commit, path: exact.ref.path, digest: exact.ref.digest }); } const streamId = `stream-${stableId(text(topic.id), projectId)}`; diff --git a/src/api/discussions/content.ts b/src/api/discussions/content.ts index 18791e22c..d472c4946 100644 --- a/src/api/discussions/content.ts +++ b/src/api/discussions/content.ts @@ -320,7 +320,7 @@ export async function validateDiscussionContextRefs(input: { store: any; teamId: const values = Array.isArray(input.values) ? input.values.slice(0, 24) : []; if (values.some((value) => !isAgentAtlasContextReference(value))) throw Object.assign(new Error('Discussion context contains an invalid Atlas reference.'), { status: 422, code: 'discussion_context_invalid' }); const references = values as AgentAtlasContextReference[]; - if (references.some((reference) => reference.projectId !== input.projectId)) throw Object.assign(new Error('Discussion context must belong to the selected project.'), { status: 403, code: 'discussion_context_project_forbidden' }); + if (references.some((reference) => reference.kind !== 'proposal' && reference.projectId !== input.projectId)) throw Object.assign(new Error('Discussion context must belong to the selected project.'), { status: 403, code: 'discussion_context_project_forbidden' }); const workdays = new Map>(); for (const reference of references) { if (reference.workdayId) { @@ -350,7 +350,7 @@ export async function validateDiscussionContextRefs(input: { store: any; teamId: if (!assignment) throw Object.assign(new Error('Discussion context references an unknown assignment.'), { status: 409, code: 'discussion_context_assignment_stale' }); } if (reference.kind === 'proposal') { - const proposal = await input.store.first('SELECT id FROM governance_proposals WHERE id = ? AND team_id = ? AND project_id = ? LIMIT 1', [reference.id, input.teamId, input.projectId]); + const proposal = await input.store.first('SELECT id FROM governance_proposals WHERE id = ? AND team_id = ? AND project_id = ? LIMIT 1', [reference.id, input.teamId, reference.projectId]); if (!proposal) throw Object.assign(new Error('Discussion context references an unknown proposal.'), { status: 409, code: 'discussion_context_proposal_stale' }); } if (reference.kind === 'decision') { diff --git a/tests/unit/control-plane/capacity/communication/completion/proposal-replay-native.test.ts b/tests/unit/control-plane/capacity/communication/completion/proposal-replay-native.test.ts index e55c38bd4..dac940ec1 100644 --- a/tests/unit/control-plane/capacity/communication/completion/proposal-replay-native.test.ts +++ b/tests/unit/control-plane/capacity/communication/completion/proposal-replay-native.test.ts @@ -17,19 +17,20 @@ import { OperationRegistry } from '../../../../../../src/api/control-plane/catal import { installControlPlaneProtocolRoutes } from '../../../../../../src/api/control-plane/http/protocol-routes.ts'; describe('native communication proposal recovery', () => { - it('keeps the original context and one invocation after real proposal authoring, concurrent retries and interrupted admission', async () => { + it.each([false, true])('keeps the original context and one invocation across project streams=%s after proposal edits and interrupted admission', async crossProject => { // Original owning services, PostgreSQL pools and disposable signed TreeDX. // The principal/profile/proposal are controlled inputs, not agent results, // OAuth, provider execution or a full objective workflow acceptance claim. const f = await relationAuthoringDatabase(true), owned = new Set(); try { const projectId = f.sources[0]!.projectId, repository = f.sources[0]!.repository; + const discussionProjectId = f.sources[crossProject ? 1 : 0]!.projectId; const now = new Date().toISOString(); - const agent = { schemaVersion: 'treeseed.agent/v1', id: `${projectId}/researcher`, name: 'Researcher', agentClass: 'researcher', + const agent = { schemaVersion: 'treeseed.agent/v1', id: `${discussionProjectId}/researcher`, name: 'Researcher', agentClass: 'researcher', purpose: 'Inspect the retry boundary.', responsibilities: ['Inspect the retry boundary.'], capabilities: ['repository-analysis'], context: { include: ['project-objectives'] }, activityProfiles: { chat: { handler: 'writer', permissions: { content: { read: ['knowledge', 'discussion'], write: ['discussion'] }, tools: ['discussion', 'source.read'] }, prompt: { system: 'Answer with exact source evidence.' } } } }; await f.query(`INSERT INTO project_agent_classes (id,team_id,project_id,slug,name,handler_refs_json,metadata_json,created_at,updated_at) - VALUES (?,?,?,?,?,?,?,?,?)`, ['recovery-researcher', 'team', projectId, 'researcher', 'Researcher', JSON.stringify({ agents: [agent] }), '{}', now, now]); + VALUES (?,?,?,?,?,?,?,?,?)`, ['recovery-researcher', 'team', discussionProjectId, 'researcher', 'Researcher', JSON.stringify({ agents: [agent] }), '{}', now, now]); const declaration = { schemaVersion: 'treeseed.proposal/v1', id: 'recover-mcp', projectId, title: 'Recover meaningful MCP work', request: 'Keep the original invocation when retrying project work after proposal edits.', summary: 'Repair the observed retry-context drift.', status: 'draft' }; const path = 'proposals/governance/recover-mcp.mdx', source = `---\n${stringify(declaration)}---\n`; @@ -59,7 +60,7 @@ describe('native communication proposal recovery', () => { const id = String(result.authoringReceipt.changeset.workspaceId ?? ''); assert.ok(id); owned.add(id); await f.client.workspaces.close(id); expect(object(await f.client.workspaces.get(id)).status).toBe('closed'); owned.delete(id); }; - const body = { message: `@${projectId}/researcher Trace the MCP retry-context boundary.`, proposalId: declaration.id }; + const body = { message: `@${discussionProjectId}/researcher Trace the MCP retry-context boundary.`, proposalId: declaration.id }; const send = (key = 'same-project-work', input = body) => communication.send(f.principal, 'team', 'mcp-recovery', input, key); // The original official client must compile the actual receipt schema and // validate real owning-service output; no relaxed validator or fake receipt. @@ -79,7 +80,8 @@ describe('native communication proposal recovery', () => { path: { teamId: 'team', channel: 'mcp-recovery' }, body, headers: { 'Idempotency-Key': 'same-project-work' } } }); expect(result.isError).not.toBe(true); first = binding.schema.output.parse(result.structuredContent); } finally { await client.close(); } - expect(first.targets).toHaveLength(1); + expect(first.targets).toHaveLength(1); expect(first.targets[0]!.projectId).toBe(discussionProjectId); + if (crossProject) expect((await loadDiscussions({ store: f.store, projectId, discussionId: 'mcp-recovery', collection: 'messages' })).messages).toHaveLength(0); const snapshots = async () => ({ invocations: await f.store.all('SELECT * FROM agent_invocation_requests ORDER BY id'), assignments: await f.store.all('SELECT * FROM capacity_provider_assignments ORDER BY id'), ledger: await f.store.all('SELECT * FROM capacity_ledger_entries ORDER BY id'), events: await f.store.all('SELECT * FROM communication_topic_events ORDER BY sequence') }); @@ -104,16 +106,16 @@ describe('native communication proposal recovery', () => { CREATE TRIGGER reject_recovery_admission BEFORE INSERT ON agent_invocation_requests FOR EACH ROW EXECUTE FUNCTION reject_recovery_admission();`); try { await expect(send('interrupted-project-work')).rejects.toMatchObject({ status: 409, code: 'P0001' }); } finally { await f.db.exec('DROP TRIGGER reject_recovery_admission ON agent_invocation_requests; DROP FUNCTION reject_recovery_admission();'); } - const interrupted = await loadDiscussions({ store: f.store, projectId, discussionId: 'mcp-recovery', collection: 'messages' }); + const interrupted = await loadDiscussions({ store: f.store, projectId: discussionProjectId, discussionId: 'mcp-recovery', collection: 'messages' }); const frozen = interrupted.messages.find(message => message.id !== first.projectStreams[0]!.messageRef.split('/').at(-1)?.replace('.mdx','')); assert.ok(frozen); await edit('Recover the interrupted admission against its original proposal.', 'Exercise the observed authoring-before-admission interruption.', '2'); const moved = await f.store.getGovernanceProposal(declaration.id); assert.ok(moved); const exact = await readExactProposal(f.store, moved), interruptedState = await snapshots(); // This is the state a racing caller reaches after its first lookup missed // the message but another writer committed before its own create call. - await expect(discussion.create(f.principal, { teamId: 'team', projectId, discussionId: 'mcp-recovery', body: body.message, + await expect(discussion.create(f.principal, { teamId: 'team', projectId: discussionProjectId, discussionId: 'mcp-recovery', body: body.message, contextRefs: [{ kind: 'proposal', id: declaration.id, projectId, immutableRef: exact.ref.commit, path: exact.ref.path, digest: exact.ref.digest }], - recipients: ['researcher'] }, `interrupted-project-work:${projectId}`)) + recipients: ['researcher'] }, `interrupted-project-work:${discussionProjectId}`)) .rejects.toMatchObject({ status: 409, code: 'discussion_idempotency_conflict' }); expect(await snapshots()).toEqual(interruptedState); const recovered = await send('interrupted-project-work'); expect(recovered.replayed).toBe(true);