From fd54549353228f78a7fa81063c6a821fd1732f09 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Sun, 20 Sep 2026 15:29:50 -0400 Subject: [PATCH] fix: isolate simulation proposal revisions from workday discussion branch --- .../governance/proposal-version-content.ts | 4 +++- .../proposal-workday-custody.test.ts | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/api/control-plane/governance/proposal-version-content.ts b/src/api/control-plane/governance/proposal-version-content.ts index 77b305a8..b76db215 100644 --- a/src/api/control-plane/governance/proposal-version-content.ts +++ b/src/api/control-plane/governance/proposal-version-content.ts @@ -33,7 +33,9 @@ export async function commitProposalVersionContent(input: { store: any; proposal throw Object.assign(new Error('Proposal authoring requires an active workday bound to this project and proposal.'), { status: 409, code: 'proposal_workday_scope_invalid' }); } const simulation = run?.executionMode === 'simulation'; - const simulationRef = simulation ? `refs/heads/${run.id}` : undefined; + // Keep proposal revisions on their exact source lineage. The workday's + // discussion branch can originate before this proposal existed. + const simulationRef = simulation ? `refs/heads/${run.id}-proposal-${createHash('sha256').update(text(input.proposal.id)).digest('hex').slice(0, 16)}` : undefined; const sourceCommit = text(provenance.commitSha); if (simulation && !/^[a-f0-9]{40}$/u.test(sourceCommit)) throw Object.assign(new Error('Simulation proposal authoring requires exact existing content provenance.'), { status: 409, code: 'proposal_simulation_source_required' }); const connection = await resolveKnowledgeGatewayConnection(input.store, { projectId, write: true, relationPaths: true, authoringPaths: true, diff --git a/tests/unit/control-plane/governance/proposal-workday-custody.test.ts b/tests/unit/control-plane/governance/proposal-workday-custody.test.ts index 8574af54..dd97d407 100644 --- a/tests/unit/control-plane/governance/proposal-workday-custody.test.ts +++ b/tests/unit/control-plane/governance/proposal-workday-custody.test.ts @@ -38,12 +38,34 @@ describe('workday-scoped proposal authoring', () => { it('writes a simulation version from exact provenance to local workday custody without publication', async () => { const { input, client, base, path } = fixture(); const result = await commitProposalVersionContent(input); - expect(client.createWorkspace).toHaveBeenCalledWith(expect.objectContaining({ baseRef: base, branchName: 'refs/heads/workday-1', allowedPaths: [path] })); + const proposalBranch = `refs/heads/workday-1-proposal-${createHash('sha256').update('proposal-1').digest('hex').slice(0, 16)}`; + expect(client.createWorkspace).toHaveBeenCalledWith(expect.objectContaining({ baseRef: base, branchName: proposalBranch, allowedPaths: [path] })); expect(client.readRepositoryFiles).toHaveBeenCalledWith(expect.objectContaining({ ref: base })); expect(result.update.contentProvenance).toMatchObject({ commitSha: 'b'.repeat(40), contentPath: path }); expect(recordTreeDxAuthoringState).not.toHaveBeenCalled(); expect(projectTreeDxCommitSignals).not.toHaveBeenCalled(); expect(client.closeWorkspace).toHaveBeenCalledExactlyOnceWith('workspace-1'); }); + it('does not reuse an advanced discussion branch that lacks the proposal source', async () => { + const { input, client, base } = fixture(); + client.createWorkspace.mockImplementationOnce(async request => { + if (request.branchName === 'refs/heads/workday-1' && request.baseRef === base) { + throw new Error('Workspace branch already exists at a different commit.'); + } + return { workspaceId: 'workspace-1', baseCommitSha: base }; + }); + await expect(commitProposalVersionContent(input)).resolves.toMatchObject({ update: { contentProvenance: { commitSha: 'b'.repeat(40) } } }); + expect(client.createWorkspace.mock.calls[0]?.[0].branchName).not.toBe('refs/heads/workday-1'); + }); + it('starts a later estimate version from its exact prior proposal commit on the same simulation branch', async () => { + const { input, client } = fixture(); + input.proposal.metadata.contentProvenance.commitSha = 'b'.repeat(40); + const first = await commitProposalVersionContent(input); + const second = await commitProposalVersionContent(input); + expect(client.createWorkspace).toHaveBeenCalledTimes(2); + expect(client.createWorkspace.mock.calls[0]?.[0].branchName).toBe(client.createWorkspace.mock.calls[1]?.[0].branchName); + expect(client.createWorkspace.mock.calls[1]?.[0].baseRef).toBe('b'.repeat(40)); + expect(first.update.contentProvenance.commitSha).toBe(second.update.contentProvenance.commitSha); + }); it.each(['missing', 'terminal', 'project', 'proposal', 'expired'])('rejects %s workday scope before issuing a workspace', async invalid => { const { run, input, client } = fixture(); if (invalid === 'missing') vi.mocked(CapacityWorkdayRunRepository.prototype.get).mockResolvedValue(null);