diff --git a/guarantees/agent/golden/scenes/component-boundaries.scene.yaml b/guarantees/agent/golden/scenes/component-boundaries.scene.yaml index 2424b9574..c13d86ee3 100644 --- a/guarantees/agent/golden/scenes/component-boundaries.scene.yaml +++ b/guarantees/agent/golden/scenes/component-boundaries.scene.yaml @@ -3,6 +3,9 @@ id: sdk.golden.component-boundaries title: sdk golden component boundaries scope: local-component-tests workflow: + - id: frozen-draft-unit + action: { verifier: sdk.golden.frozen-draft-unit } + expect: { status: passed } - id: proposal-identity-unit action: { verifier: sdk.golden.proposal-identity-unit } expect: { status: passed } diff --git a/guarantees/verifiers/golden.verifiers.yaml b/guarantees/verifiers/golden.verifiers.yaml index 0dfdf3713..90a10f2bf 100644 --- a/guarantees/verifiers/golden.verifiers.yaml +++ b/guarantees/verifiers/golden.verifiers.yaml @@ -1,6 +1,11 @@ schemaVersion: treeseed.guarantee-verifiers/v1 ownerPackage: "@treeseed/sdk" verifiers: + sdk.golden.frozen-draft-unit: + kind: vitestCase + ownerPackage: "@treeseed/sdk" + testFile: tests/unit/operator-contracts/acceptance/workday-start.test.ts + testName: "reads only the original frozen draft request and rejects missing ambiguous changed foreign or redirected input without repairing it" sdk.golden.proposal-identity-unit: kind: vitestCase ownerPackage: "@treeseed/sdk" diff --git a/tests/acceptance/golden-product.ts b/tests/acceptance/golden-product.ts index 9714477c8..b72fb8b80 100644 --- a/tests/acceptance/golden-product.ts +++ b/tests/acceptance/golden-product.ts @@ -71,6 +71,46 @@ export function sdkGoldenProposalDocument(storedProposal: unknown, original: Sdk 'ACCEPTANCE_SDK_PROPOSAL: Governance index differs from its original content'); return proposal; } +function verifySdkGoldenAuthoredWork(proposal: Row, draft = false): void { + const sourceCode = draft ? 'ACCEPTANCE_SDK_FROZEN_REQUEST' : 'ACCEPTANCE_SDK_PROPOSAL'; + const graphCode = draft ? sourceCode : 'ACCEPTANCE_SDK_GRAPH'; + assert.ok(Array.isArray(proposal.evidenceRefs), `${sourceCode}: Original source issue evidence required`); + const sourceEvidence = proposal.evidenceRefs.filter((ref: unknown) => ref && typeof ref === 'object' + && !Array.isArray(ref) && (ref as Row).url === 'https://github.com/treeseed-ai/sdk/issues/299'); + assert.equal(sourceEvidence.length, 1, `${sourceCode}: One unambiguous original SDK source issue required`); + const issue = exactEntityReferenceSchema.safeParse(sourceEvidence[0]); + assert.ok(issue.success && issue.data.store === 'url' && issue.data.model === 'issue', + `${sourceCode}: Canonical original SDK source issue reference required`); + const authored = proposal.executionPlan?.workItems; + const fixedRows = [ + ['research-context', 'researcher', 'treedx'], ['architecture-contract', 'architect', 'treedx'], + ['tests-first', 'tester', 'git'], ['implement-change', 'engineer', 'git'], + ['document-change', 'technical-writer', 'git'], ['simulate-release', 'releaser', 'git'], + ] as const; + assert.ok(Array.isArray(authored) && authored.length === fixedRows.length + && authored.every(item => item && typeof item === 'object' && !Array.isArray(item)) + && new Set(authored.map(item => item.id)).size === fixedRows.length, + `${graphCode}: Six unique original authored work items required`); + for (const [id, agentClass, workspace] of fixedRows) { + const item = authored.find(row => row.id === id); + assert.ok(item && item.agentClass === agentClass && item.workspace === workspace + && item.activity === 'acting' && item.review === 'required' && item.maximumReviewCycles === 2 + && Array.isArray(item.dependsOn) && item.dependsOn.length === 0 + && ['objective', 'contextRefs', 'requestedPermissions', 'requiredCapabilities', 'acceptanceCriteria', + ...(draft ? [] : ['estimate', 'reviewEstimate'])].every(field => Object.hasOwn(item, field)), + `${graphCode}: Original ${id} class workspace dependencies and review policy required`); + if (draft) assert.ok(['estimate', 'reviewEstimate', 'ownerEstimate', 'reviewerEstimate'].every(field => !Object.hasOwn(item, field)) + && typeof item.objective === 'string' && item.objective.trim() + && Array.isArray(item.contextRefs) && item.contextRefs.length > 0 + && item.contextRefs.every((ref: unknown) => exactEntityReferenceSchema.safeParse(ref).success) + && item.requestedPermissions && typeof item.requestedPermissions === 'object' && !Array.isArray(item.requestedPermissions) + && Array.isArray(item.requiredCapabilities) && item.requiredCapabilities.every((value: unknown) => typeof value === 'string' && value.trim()) + && Array.isArray(item.acceptanceCriteria) && item.acceptanceCriteria.length > 0 + && item.acceptanceCriteria.every((value: unknown) => typeof value === 'string' && value.trim()), + `${graphCode}: Explicit estimate-free original work required`); + } +} + export function verifySdkGoldenProduct(assignments: Row[], sourceBase: string, storedProposal: unknown, original?: SdkGoldenOriginal): void { const actors = assignments.filter(item => item.status === 'completed' @@ -96,37 +136,20 @@ export function verifySdkGoldenProduct(assignments: Row[], sourceBase: string, s && typeof original.initialProposal.request === 'string' && original.initialProposal.request.trim() && proposal.request === original.initialProposal.request, 'ACCEPTANCE_SDK_FROZEN_REQUEST: Ready proposal must retain its original frozen request; source issue is evidence only'); - assert.ok(Array.isArray(proposal.evidenceRefs), 'ACCEPTANCE_SDK_PROPOSAL: Original source issue evidence required'); - const sourceEvidence = proposal.evidenceRefs.filter((ref: unknown) => ref && typeof ref === 'object' - && !Array.isArray(ref) && (ref as Row).url === 'https://github.com/treeseed-ai/sdk/issues/299'); - assert.equal(sourceEvidence.length, 1, 'ACCEPTANCE_SDK_PROPOSAL: One unambiguous original SDK source issue required'); - const issue = exactEntityReferenceSchema.safeParse(sourceEvidence[0]); - assert.ok(issue.success && issue.data.store === 'url' && issue.data.model === 'issue', - 'ACCEPTANCE_SDK_PROPOSAL: Canonical original SDK source issue reference required'); // Check the original public proposal, not the derived graph node counts. // Genuine estimate provenance and generated review readiness have separate gates. - const authored = proposal.executionPlan?.workItems; - const fixedRows = [ - ['research-context', 'researcher', 'treedx'], ['architecture-contract', 'architect', 'treedx'], - ['tests-first', 'tester', 'git'], ['implement-change', 'engineer', 'git'], - ['document-change', 'technical-writer', 'git'], ['simulate-release', 'releaser', 'git'], - ] as const; - assert.ok(Array.isArray(authored) && authored.length === fixedRows.length - && authored.every(item => item && typeof item === 'object' && !Array.isArray(item)) - && new Set(authored.map(item => item.id)).size === fixedRows.length, - 'ACCEPTANCE_SDK_GRAPH: Six unique original authored work items required'); - for (const [id, agentClass, workspace] of fixedRows) { - const item = authored.find(row => row.id === id); - assert.ok(item && item.agentClass === agentClass && item.workspace === workspace - && item.activity === 'acting' && item.review === 'required' && item.maximumReviewCycles === 2 - && Array.isArray(item.dependsOn) && item.dependsOn.length === 0 - && ['objective', 'estimate', 'reviewEstimate', 'contextRefs', 'requestedPermissions', 'requiredCapabilities', 'acceptanceCriteria'] - .every(field => Object.hasOwn(item, field)), - `ACCEPTANCE_SDK_GRAPH: Original ${id} class workspace dependencies and review policy required`); - } + verifySdkGoldenAuthoredWork(proposal); const expanded = validatePortableContentData('proposal', proposal); assert.ok(expanded.ok, 'ACCEPTANCE_SDK_GRAPH: Complete original canonical Proposal and WorkItems required'); assert.deepEqual(expanded.data, proposal, 'ACCEPTANCE_SDK_GRAPH: Original configuration cannot inherit defaults or discard shorthand'); + verifySdkGoldenAuthoredWork(original.initialProposal, true); + for (const field of ['title', 'summary', 'evidenceRefs']) assert.deepEqual(proposal[field], original.initialProposal[field], + 'ACCEPTANCE_SDK_FROZEN_REQUEST: Fixed original objective and evidence changed'); + for (const item of proposal.executionPlan.workItems) { + const { estimate, reviewEstimate, ...fixed } = item; + assert.deepEqual(fixed, original.initialProposal.executionPlan.workItems.find((row: Row) => row.id === item.id), + 'ACCEPTANCE_SDK_FROZEN_REQUEST: Fixed original work changed while becoming ready'); + } verifySdkGoldenProposalReferences(proposal, original!, release.assignmentAttempt); assert.ok(sourceRef.revision === governance.activeVersion && sourceRef.digest === `sha256:${provenance.digest}` && sourceRef.repository === provenance.repositoryId && sourceRef.commit === provenance.commitSha @@ -194,7 +217,7 @@ export function verifySdkGoldenProposalReferences(proposal: Row, original: SdkGo const ref = exactEntityReferenceSchema.parse(value), model = index < proposal.objectiveRefs.length ? 'objective' : 'discussion'; assert.ok(ref.store === 'treedx' && ref.model === model && ref.repository && ref.path && ref.commit && ref.digest, `${code}: Complete exact governed reference required`); - const matches = original.referenceFiles.filter(read => read.repoId === ref.repository && read.resolvedRef === ref.commit + const matches: Row[] = original.referenceFiles.filter(read => read.repoId === ref.repository && read.resolvedRef === ref.commit && Array.isArray(read.files) && read.files.length === 1 && read.files[0]?.path === ref.path); assert.equal(matches.length, 1, `${code}: One unambiguous native read required`); const read = matches[0]!; assert.ok(!consumed.has(read), `${code}: Reused reference read`); consumed.add(read); @@ -250,6 +273,11 @@ export function sdkGoldenFrozenProposal(freeze: Row): Row { && proposal.projectId === freeze.request.body.projects[0] && typeof proposal.request === 'string' && proposal.request.trim(), 'ACCEPTANCE_SDK_FROZEN_REQUEST: Exact initial draft identity project and request required'); + assert.ok(typeof proposal.title === 'string' && proposal.title.trim() && typeof proposal.summary === 'string' && proposal.summary.trim() + && Array.isArray(proposal.objectiveRefs) && proposal.objectiveRefs.length >= 2 + && proposal.objectiveRefs.every((ref: unknown) => exactEntityReferenceSchema.safeParse(ref).success), + 'ACCEPTANCE_SDK_FROZEN_REQUEST: Complete original objective authority required'); + verifySdkGoldenAuthoredWork(proposal, true); assert.deepEqual(retainedBytes(path), bytes, 'ACCEPTANCE_SDK_FROZEN_REQUEST: Original proposal changed during read'); return proposal; } diff --git a/tests/unit/operator-contracts/acceptance/golden-product-fixture.ts b/tests/unit/operator-contracts/acceptance/golden-product-fixture.ts index 6da316f7a..40d140fd1 100644 --- a/tests/unit/operator-contracts/acceptance/golden-product-fixture.ts +++ b/tests/unit/operator-contracts/acceptance/golden-product-fixture.ts @@ -53,9 +53,11 @@ export function proposalReadback(...input: [unknown?, number?, (boolean | 'alias const base = `---\n${encoded}---\n`; const content = base + ' '.repeat(Math.max(0, (input[1] ?? 0) - Buffer.byteLength(base))); const digest = createHash('sha256').update(content).digest('hex'), repositoryId = 'repo_sdk_fixture', commitSha = 'c'.repeat(40), contentPath = 'proposals/governance/sdk-proposal.mdx'; + const initialProposal = structuredClone({ ...proposal, status: 'draft' }); + for (const item of initialProposal.executionPlan.workItems) { Reflect.deleteProperty(item, 'estimate'); Reflect.deleteProperty(item, 'reviewEstimate'); } return { governance: { id: fields.id, projectId: fields.projectId, title: fields.title, summary: fields.summary, body: fields.request, activeVersion: 1, activeContentHash: digest, metadata: { contentProvenance: { repositoryId, commitSha, contentPath, digest } } }, - original: { initialProposal: structuredClone({ ...proposal, status: 'draft' }), binding: { repositoryId }, + original: { initialProposal, binding: { repositoryId }, teamLibrary: { project: { id: 'team-project', slug: 'team', teamId: 'sdk-team' }, library: { projectId: 'team-project', teamId: 'sdk-team', repositoryId: 'repo_team_fixture' } }, referenceFiles: structuredClone(referenced.map(item => item.read)), files: { ok: true, repoId: repositoryId, resolvedRef: commitSha, files: [{ path: contentPath, content, frontmatter: parse(encoded) as unknown }] } } }; diff --git a/tests/unit/operator-contracts/acceptance/golden-proposal-native.test.ts b/tests/unit/operator-contracts/acceptance/golden-proposal-native.test.ts index a83675464..4aa19456c 100644 --- a/tests/unit/operator-contracts/acceptance/golden-proposal-native.test.ts +++ b/tests/unit/operator-contracts/acceptance/golden-proposal-native.test.ts @@ -2,7 +2,7 @@ import { createServer } from 'node:http'; import { spawn, type ChildProcess } from 'node:child_process'; import { createRequire } from 'node:module'; import { once } from 'node:events'; -import { writeFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { createHash } from 'node:crypto'; import { parse } from 'yaml'; @@ -17,7 +17,9 @@ it('native public SDK proposal reads in isolated product verification reject cha : kind === 'reference-path' ? bytes.replace('path: proposals/governance/sdk-proposal.mdx', 'path: proposals/foreign.mdx') : bytes; const original = structuredClone({ release, review, proposal }); const retainedInput = workdayStartFixture(), inputPath = resolve(retainedInput.root, 'proposal-input.json'); - const inputBytes = JSON.stringify({ ...proposal, status: 'draft' }); writeFileSync(inputPath, inputBytes); + const initial = { ...structuredClone(proposal), status: 'draft' }; + for (const item of initial.executionPlan.workItems) { Reflect.deleteProperty(item, 'estimate'); Reflect.deleteProperty(item, 'reviewEstimate'); } + const inputBytes = JSON.stringify(initial); writeFileSync(inputPath, inputBytes); const freeze = { proposal: { id: proposal.id }, request: { body: { projects: [proposal.projectId] } }, receipts: { [inputPath]: `sha256:${createHash('sha256').update(inputBytes).digest('hex')}` } }; let worker: ChildProcess | undefined, sequence = 0, stderrBytes = 0; @@ -112,12 +114,12 @@ it('native public SDK proposal reads in isolated product verification reject cha import{controlPlaneOperation}from ${JSON.stringify(operations)}; import{verifySdkGoldenProduct,sdkGoldenProposalProvenance,sdkGoldenFrozenProposal,sdkGoldenProposalDocument,sdkGoldenLibraryFiles}from ${JSON.stringify(helper)}; const client=new ControlPlaneClient({profile:defaultLocalControlPlaneServer({TREESEED_API_BASE_URL:${JSON.stringify(`http://127.0.0.1:${address.port}`)}}),accessToken:'controlled-native-input'}); -process.on('message',async ({id,actors})=>{try{ +process.on('message',async ({id,actors,frozen})=>{try{ const observed=await client.invoke(controlPlaneOperation('governance.proposals.show'),{path:{projectId:'sdk-project',proposalId:'sdk-proposal'},query:{},body:undefined}); const provenance=sdkGoldenProposalProvenance(observed.data); const binding=await client.invoke(controlPlaneOperation('treedx.library.show'),{path:{projectId:'sdk-project'},query:{},body:undefined}); const files=await client.invoke(controlPlaneOperation('treedx.repositories.files.read'),{path:{projectId:'sdk-project',repoId:provenance.repositoryId},query:{},body:{ref:provenance.commitSha,paths:[provenance.contentPath],encoding:'utf8',parseFrontmatter:true}}); -const original={binding:binding.data,files:sdkGoldenLibraryFiles({result:files.data,receipt:{operation:'read'}}),initialProposal:sdkGoldenFrozenProposal(${JSON.stringify(freeze)})}; +const original={binding:binding.data,files:sdkGoldenLibraryFiles({result:files.data,receipt:{operation:'read'}}),initialProposal:sdkGoldenFrozenProposal(frozen)}; const document=sdkGoldenProposalDocument(observed.data,original); const teamProject=await client.invoke(controlPlaneOperation('projects.show'),{path:{projectId:'team-project'},query:{},body:undefined}); const teamBinding=await client.invoke(controlPlaneOperation('treedx.library.show'),{path:{projectId:'team-project'},query:{},body:undefined}); @@ -140,10 +142,10 @@ Reflect.apply(verifySdkGoldenProduct,undefined,[actors,${JSON.stringify(commit)} const item = pending.get(value.id)!; pending.delete(value.id); clearTimeout(item.timer); if (value.passed) item.resolve({ stdout: 'verified' }); else item.reject(new Error(String(value.error))); }); - const observe = (actors: unknown[] = [release, review]) => new Promise<{ stdout: string }>((resolve, reject) => { + const observe = (actors: unknown[] = [release, review], frozen = freeze) => new Promise<{ stdout: string }>((resolve, reject) => { if (!worker?.connected) { reject(new Error('Isolated verifier is unavailable')); return; } const id = ++sequence, timer = setTimeout(() => { fail(new Error('Isolated verifier exceeded original five-second watchdog')); worker!.kill('SIGKILL'); }, 5_000); - pending.set(id, { resolve, reject, timer }); worker.send({ id, actors }, error => { if (error) fail(error); }); + pending.set(id, { resolve, reject, timer }); worker.send({ id, actors, frozen }, error => { if (error) fail(error); }); }); expect((await Promise.all([observe(), observe()])).map(value => value.stdout)).toEqual(['verified', 'verified']); for (const denied of ['title', 'summary', 'foreign-id', 'foreign-project', 'missing-title', 'missing-summary', @@ -205,6 +207,39 @@ Reflect.apply(verifySdkGoldenProduct,undefined,[actors,${JSON.stringify(commit)} mode = 'exact'; expect((await observe()).stdout).toBe('verified'); expect(failed).toEqual(deniedReferences); expect(referenceCalls.slice(0, retainedReferenceCalls.length)).toEqual(retainedReferenceCalls); expect(failed).toHaveLength(59); expect(calls).toHaveLength(189); + const deniedDrafts: Array<{ bytes: string; error: string }> = []; + for (const field of ['executionPlan', 'evidenceRefs', 'objectiveRefs', 'title', 'summary']) { + const changed = structuredClone(initial); Reflect.deleteProperty(changed, field); + deniedDrafts.push({ bytes: JSON.stringify(changed), error: 'ACCEPTANCE_SDK_FROZEN_REQUEST' }); + } + for (const [index] of initial.executionPlan.workItems.entries()) { + for (const field of ['objective', 'contextRefs', 'acceptanceCriteria']) { + const changed = structuredClone(initial); Reflect.deleteProperty(changed.executionPlan.workItems[index]!, field); + deniedDrafts.push({ bytes: JSON.stringify(changed), error: 'ACCEPTANCE_SDK_FROZEN_REQUEST' }); + } + } + for (const field of ['estimate', 'reviewEstimate', 'ownerEstimate', 'reviewerEstimate']) { + const changed = structuredClone(initial); Reflect.set(changed.executionPlan.workItems[0]!, field, null); + deniedDrafts.push({ bytes: JSON.stringify(changed), error: 'ACCEPTANCE_SDK_FROZEN_REQUEST' }); + } + for (const field of ['title', 'summary', 'evidenceRefs', 'objective', 'contextRefs', 'acceptanceCriteria']) { + const changed = structuredClone(initial); + if (field === 'evidenceRefs') changed.evidenceRefs[0]!.id = 'different-original-evidence'; + else if (field === 'contextRefs') changed.executionPlan.workItems[0]!.contextRefs[0]!.commit = 'd'.repeat(40); + else if (field === 'acceptanceCriteria') changed.executionPlan.workItems[0]!.acceptanceCriteria = ['Different fixed criterion.']; + else if (field === 'objective') changed.executionPlan.workItems[0]!.objective = 'Different fixed objective.'; + else Reflect.set(changed, field, 'Different fixed objective.'); + deniedDrafts.push({ bytes: JSON.stringify(changed), error: 'ACCEPTANCE_SDK_FROZEN_REQUEST' }); + } + for (const failedDraft of deniedDrafts) { + writeFileSync(inputPath, failedDraft.bytes); + const frozen = { ...freeze, receipts: { [inputPath]: `sha256:${createHash('sha256').update(failedDraft.bytes).digest('hex')}` } }; + await expect.soft(observe([release, review], frozen)).rejects.toThrow(failedDraft.error); + expect(readFileSync(inputPath, 'utf8')).toBe(failedDraft.bytes); + } + const preservedDrafts = structuredClone(deniedDrafts); writeFileSync(inputPath, inputBytes); + expect((await observe()).stdout).toBe('verified'); expect(deniedDrafts).toEqual(preservedDrafts); + expect(failed).toEqual(deniedReferences); expect(calls).toHaveLength(291); for (const file of proposalReadback().original.referenceFiles) expect(referenceCalls.some(value => value.endsWith(` ${file.files[0]!.path}`))).toBe(true); expect({ release, review, proposal }).toEqual(original); diff --git a/tests/unit/operator-contracts/acceptance/workday-start.test.ts b/tests/unit/operator-contracts/acceptance/workday-start.test.ts index 35bc16a1a..b8ac94163 100644 --- a/tests/unit/operator-contracts/acceptance/workday-start.test.ts +++ b/tests/unit/operator-contracts/acceptance/workday-start.test.ts @@ -4,16 +4,37 @@ import { createHash } from 'node:crypto'; import { resolve } from 'node:path'; import { sdkGoldenWorkdayId, sdkGoldenFrozenProposal } from '../../../acceptance/golden-product.ts'; import { workdayStartFixture } from './workday-start-fixture.ts'; +import { proposal as readyProposal } from './golden-product-fixture.ts'; it('reads only the original frozen draft request and rejects missing ambiguous changed foreign or redirected input without repairing it', () => { const f = workdayStartFixture(), path = resolve(f.root, 'proposal-input.json'); - const proposal = { id: f.freeze.proposal.id, projectId: f.freeze.request.body.projects[0], status: 'draft', request: 'Controlled unchanged SDK request' }; + const proposal = { ...structuredClone(readyProposal), id: f.freeze.proposal.id, projectId: f.freeze.request.body.projects[0], status: 'draft' }; + for (const item of proposal.executionPlan.workItems) { Reflect.deleteProperty(item, 'estimate'); Reflect.deleteProperty(item, 'reviewEstimate'); } const receipt = (bytes: string) => ({ ...structuredClone(f.freeze), receipts: { [path]: `sha256:${createHash('sha256').update(bytes).digest('hex')}` } }); const bytes = JSON.stringify(proposal), freeze = receipt(bytes); writeFileSync(path, bytes); try { expect(sdkGoldenFrozenProposal(freeze)).toEqual(proposal); const boundary = bytes + ' '.repeat(1_048_576 - Buffer.byteLength(bytes)); writeFileSync(path, boundary); expect(sdkGoldenFrozenProposal(receipt(boundary))).toEqual(proposal); writeFileSync(path, bytes); + const denied: unknown[] = []; + for (const field of ['executionPlan', 'evidenceRefs', 'objectiveRefs', 'title', 'summary']) { + for (const value of [undefined, null, '', false, [], {}]) denied.push({ ...proposal, [field]: value }); + } + for (const [index] of proposal.executionPlan.workItems.entries()) { + for (const field of ['id', 'agentClass', 'workspace', 'activity', 'review', 'maximumReviewCycles', 'objective', + 'contextRefs', 'requestedPermissions', 'requiredCapabilities', 'acceptanceCriteria']) { + const value = structuredClone(proposal); Reflect.deleteProperty(value.executionPlan.workItems[index]!, field); denied.push(value); + } + for (const field of ['estimate', 'reviewEstimate', 'ownerEstimate', 'reviewerEstimate']) { + const value = structuredClone(proposal); Reflect.set(value.executionPlan.workItems[index]!, field, null); denied.push(value); + } + } + for (const value of denied) { + const content = JSON.stringify(value); writeFileSync(path, content); + expect.soft(() => sdkGoldenFrozenProposal(receipt(content))).toThrow('ACCEPTANCE_SDK_FROZEN_REQUEST'); + expect(readFileSync(path, 'utf8')).toBe(content); + } + writeFileSync(path, bytes); for (const value of [undefined, null, [], {}, { [path]: null }, { [path]: 'invalid' }, { [path]: freeze.receipts[path], [resolve(f.root, 'other/proposal-input.json')]: freeze.receipts[path] }]) { const changed = { ...freeze, receipts: value }, held = structuredClone(changed); diff --git a/tests/unit/operator-contracts/golden-product.test.ts b/tests/unit/operator-contracts/golden-product.test.ts index 1b1b0ae3b..cc956acf7 100644 --- a/tests/unit/operator-contracts/golden-product.test.ts +++ b/tests/unit/operator-contracts/golden-product.test.ts @@ -64,6 +64,18 @@ describe('SDK golden product gate (fixtures are not acceptance)', () => { const check = (value: unknown) => { const observed = proposalReadback(value); return Reflect.apply(verifyProduct, undefined, [[release, review], commit, observed.governance, observed.original]); }; expect(() => check(proposal)).not.toThrow(); + for (const field of ['title', 'summary', 'evidenceRefs', 'objective', 'contextRefs', 'acceptanceCriteria']) { + const changed = proposalReadback(); + if (field === 'evidenceRefs') changed.original.initialProposal.evidenceRefs[0]!.id = 'another-original-evidence'; + else if (field === 'contextRefs') changed.original.initialProposal.executionPlan.workItems[0]!.contextRefs[0]!.commit = 'd'.repeat(40); + else if (field === 'acceptanceCriteria') changed.original.initialProposal.executionPlan.workItems[0]!.acceptanceCriteria = ['Different fixed criterion.']; + else if (field === 'objective') changed.original.initialProposal.executionPlan.workItems[0]!.objective = 'Different fixed objective.'; + else Reflect.set(changed.original.initialProposal, field, 'Different fixed objective.'); + const retained = structuredClone(changed); + expect(() => Reflect.apply(verifyProduct, undefined, [[release, review], commit, changed.governance, changed.original])) + .toThrow('ACCEPTANCE_SDK_FROZEN_REQUEST'); + expect(changed).toEqual(retained); + } const changedRequest = proposalReadback({ ...proposal, request: 'Publish the legacy capacity plan and deploy it upstream.' }); Reflect.set(changedRequest.original, 'initialProposal', structuredClone(proposal)); const changedActors = structuredClone([release, review]);