diff --git a/guarantees/agent/golden/scenes/component-boundaries.scene.yaml b/guarantees/agent/golden/scenes/component-boundaries.scene.yaml index 94d15837..fba7dd7c 100644 --- a/guarantees/agent/golden/scenes/component-boundaries.scene.yaml +++ b/guarantees/agent/golden/scenes/component-boundaries.scene.yaml @@ -3,6 +3,12 @@ id: agent.golden.component-boundaries title: Golden execution component boundaries scope: local-component-tests workflow: + - id: governed-profile-order-unit + action: { verifier: agent.golden.governed-profile-order-unit } + expect: { status: passed } + - id: governed-profile-order-native + action: { verifier: agent.golden.governed-profile-order-native } + expect: { status: passed } - id: campaign-fresh-estimates-unit action: { verifier: agent.golden.campaign-fresh-estimates-unit } expect: { status: passed } diff --git a/guarantees/agent/golden/sdk-complete.guarantee.yaml b/guarantees/agent/golden/sdk-complete.guarantee.yaml index 5b7c71bd..46731109 100644 --- a/guarantees/agent/golden/sdk-complete.guarantee.yaml +++ b/guarantees/agent/golden/sdk-complete.guarantee.yaml @@ -11,6 +11,8 @@ scene: required: true manifest: guarantees/agent/golden/scenes/sdk-complete.scene.yaml acceptanceCriteria: + - criterion: a6f86094d3c2dc802a54319c3b55c1fb50fa3f1bdc5f9ee831088347c22732bb + verifierRefs: [agent.golden.architecture-profile-custody-live-1, sdk.golden.live.product] - criterion: e2bf7c70f63b2f4d1532a2d9c62fd7d5b4305151bcadc9c650a3a48bcd8649e7 verifierRefs: [sdk.golden.live.product] - criterion: 47b521b0831f30babf7664f8e12de9b9336a9c949de98473e1767212d7a76cd2 @@ -80,7 +82,7 @@ acceptanceCriteria: - criterion: 80496650a6ed5888fbda21ddcb15047d221874adb709887306dede9e3e93ff55 verifierRefs: [agent.golden.live.results] - criterion: 6d6749cd8c2676c6c50c009b55025ec3b951589db602440f63b76ba021159f20 - verifierRefs: [agent.golden.live.graph] + verifierRefs: [agent.golden.live.graph, sdk.golden.live.product] - criterion: 3bf7f808c3baf5cc63fc5e8e8c065bc2cd87dea9e2df9aae4d1924565af6acd8 verifierRefs: [agent.golden.live.reporter] - criterion: 9a9b626b666f555139baf8aabcead659ca30e7f88ad8002f73f2b35ddf542ad4 diff --git a/guarantees/verifiers/golden.verifiers.yaml b/guarantees/verifiers/golden.verifiers.yaml index d4080b69..d8a807dd 100644 --- a/guarantees/verifiers/golden.verifiers.yaml +++ b/guarantees/verifiers/golden.verifiers.yaml @@ -1,6 +1,16 @@ schemaVersion: treeseed.guarantee-verifiers/v1 ownerPackage: "@treeseed/agent" verifiers: + agent.golden.governed-profile-order-unit: + kind: vitestCase + ownerPackage: "@treeseed/agent" + testFile: tests/modern/kernel/architecture/custody/profile-custody-readback.test.ts + testName: "rejects graph dependencies that differ from independently read renamed governed profile bytes without rewriting authority" + agent.golden.governed-profile-order-native: + kind: vitestCase + ownerPackage: "@treeseed/agent" + testFile: tests/modern/kernel/architecture/custody/graph/living-graph-native.test.ts + testName: "native exact Git profile and public SDK graph reads reject changed governed dependencies and retain denied observations before unchanged retry" agent.golden.workday-start-receipt-unit: kind: vitestCase ownerPackage: "@treeseed/agent" diff --git a/tests/acceptance/workday/profile-custody.test.ts b/tests/acceptance/workday/profile-custody.test.ts index e94526a8..f5906351 100644 --- a/tests/acceptance/workday/profile-custody.test.ts +++ b/tests/acceptance/workday/profile-custody.test.ts @@ -49,12 +49,13 @@ test('Actual completed project-owned handler retains its exact governed selectio // source-to-selected-guest-build hash or executable handler-code observation. }); test('Every actual assignment matches independently read exact governed profile bytes handler prompt parameters and narrower grants', { timeout: 120_000 }, () => { - const f = evidence(), cache = new Map(); + const f = evidence(), cache = new Map(), graph = read(['execution', 'graph', '--server', 'local'], f.team); for (const item of f.assignments) { const attempt = assignmentAttemptSchema.parse(item.assignmentAttempt); const file = readGovernedContentFile(attempt.effectiveProfile.profileRef, attempt.projectId, f.team, cache, 'ACCEPTANCE_PROFILE'); - verifyGovernedProfile(item, file); + verifyGovernedProfile(item, file, graph); } + assert.deepEqual(read(['execution', 'graph', '--server', 'local'], f.team), graph); assert.deepEqual(readWorkdayAssignments(f.id, String(f.run.startedAt), f.team), f.assignments); }); test('Public assignment views preserve whole canonical attempts results and original authority across repeated readback', { timeout: 120_000 }, () => { diff --git a/tests/acceptance/workday/support/assignment-authority.ts b/tests/acceptance/workday/support/assignment-authority.ts index 6251eb9c..ebe36663 100644 --- a/tests/acceptance/workday/support/assignment-authority.ts +++ b/tests/acceptance/workday/support/assignment-authority.ts @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { assignmentWorkspaceSchema, effectiveActivityProfileSchema, exactGrantSchema, exactEntityReferenceSchema, assignmentAttemptSchema, assignmentReferenceSchema, assignmentResultSchema, verificationRecordSchema, validateAgentDefinitionModel } from '@treeseed/sdk/agent-capacity'; +import { assignmentWorkspaceSchema, effectiveActivityProfileSchema, exactGrantSchema, exactEntityReferenceSchema, assignmentAttemptSchema, assignmentReferenceSchema, assignmentResultSchema, verificationRecordSchema, validateAgentDefinitionModel, executionEdgeSchema } from '@treeseed/sdk/agent-capacity'; import { createHash } from 'node:crypto'; import { isDeepStrictEqual } from 'node:util'; import { parse } from 'yaml'; @@ -89,7 +89,7 @@ export function verifyModelClockEvidence(item: Row, event: Row): void { assert.equal(ids.size, actual.completedChecks); assert.equal(pending.size, 0, 'ACCEPTANCE_MODEL_CLOCK_PENDING'); } -export function verifyGovernedProfile(item: Row, file: Row): void { +export function verifyGovernedProfile(item: Row, file: Row, graph?: Row): void { const attempt = assignmentAttemptSchema.parse(item.assignmentAttempt), profile = attempt.effectiveProfile; assert.equal(file.path, profile.profileRef.path, 'ACCEPTANCE_PROFILE_PATH: Exact governed path required'); assert.equal(typeof file.content, 'string', 'ACCEPTANCE_PROFILE_BYTES: Original governed bytes required'); @@ -110,6 +110,41 @@ export function verifyGovernedProfile(item: Row, file: Row): void { assert.deepEqual(profile.parameters, activity.parameters); assert.deepEqual(profile.additionalContext, activity.additionalContext); assert.deepEqual(profile.permissionCeiling, activity.permissions); verifyAssignmentAuthority(item); + if (graph && profile.activity === 'acting') { + const code = 'ACCEPTANCE_PROFILE_DEPENDENCY'; + assert.ok(graph.teamId === attempt.teamId && Array.isArray(graph.nodes) && Array.isArray(graph.edges), `${code}: Complete owning graph required`); + const nodes: Row[] = graph.nodes; + assert.ok(nodes.every(node => node && typeof node === 'object' && !Array.isArray(node)) + && new Set(nodes.map(node => node.id)).size === nodes.length, `${code}: Unique original graph nodes required`); + const scope = (node: Row) => node.teamId === attempt.teamId && node.projectId === attempt.projectId && isDeepStrictEqual(node.sourceRef, attempt.sourceRef); + const selected = nodes.filter(node => node.id === attempt.nodeId); + assert.ok(selected.length === 1 && scope(selected[0]!) && selected[0]!.pairRole === 'actor' + && selected[0]!.agentClass === attempt.agentClass && selected[0]!.workItemId === attempt.workItemId, `${code}: Exact original Actor required`); + const expected: string[] = []; + for (const agentClass of activity.dependsOn?.agents ?? []) { + const predecessors = nodes.filter(node => scope(node) && node.pairRole === 'actor' && node.agentClass === agentClass); + if (predecessors.length) for (const predecessor of predecessors) { + if (predecessor.id === attempt.nodeId) continue; + const paired = nodes.filter(node => scope(node) && node.workItemId === predecessor.workItemId && node.pairRole === 'reviewer'); + assert.ok(paired.length <= 1, `${code}: Ambiguous reviewed predecessor`); + expected.push(String(paired[0]?.id ?? predecessor.id)); + } else { + const conditions = nodes.filter(node => scope(node) && node.kind === 'condition' && row(node.condition).conditionType === 'external' + && row(node.condition).expectedState === `agent-class:${agentClass}` && isDeepStrictEqual(row(node.condition).subjectRef, attempt.sourceRef)); + assert.equal(conditions.length, 1, `${code}: Exact missing-class condition required`); expected.push(String(conditions[0]!.id)); + } + } + const edges = (graph.edges as unknown[]).map(value => { const checked = executionEdgeSchema.safeParse(value); + assert.ok(checked.success, `${code}: Canonical original graph edge required`); return checked.data; }); + assert.equal(new Set(edges.map(edge => edge.id)).size, edges.length, `${code}: Duplicate edge identity`); + const incoming = edges.filter(edge => edge.toNodeId === attempt.nodeId && edge.provenance === 'profile-agent'); + assert.equal(incoming.length, expected.length, `${code}: Graph must retain every governed dependency and no substitute`); + for (const id of expected) { + const matching = incoming.filter(edge => edge.fromNodeId === id); + assert.ok(matching.length === 1 && matching[0]!.teamId === attempt.teamId && matching[0]!.graphRevisionRemoved === undefined + && isDeepStrictEqual(matching[0]!.sourceRef, attempt.sourceRef), `${code}: Exact original dependency edge required`); + } + } } export function verifyHandlerInspection(item: Row, catalog: Row, selected: Row): void { const attempt = assignmentAttemptSchema.parse(item.assignmentAttempt), profile = attempt.effectiveProfile; diff --git a/tests/modern/guarantees/bindings.test.ts b/tests/modern/guarantees/bindings.test.ts index 6114db6f..1d626f90 100644 --- a/tests/modern/guarantees/bindings.test.ts +++ b/tests/modern/guarantees/bindings.test.ts @@ -31,7 +31,6 @@ describe('capacity-provider guarantee execution bindings', () => { const scene = parse(readFileSync(resolve(root, manifest.scene.manifest), 'utf8')); const refs: string[] = scene.workflow.map((step: { action: { verifier: string } }) => step.action.verifier); for (const [criterion, ref, testName] of [ - ['6d6749cd8c2676c6c50c009b55025ec3b951589db602440f63b76ba021159f20', 'agent.golden.live.graph', 'Golden runtime graph evidence satisfies its acceptance boundary'], ['3bf7f808c3baf5cc63fc5e8e8c065bc2cd87dea9e2df9aae4d1924565af6acd8', 'agent.golden.live.reporter', 'Golden runtime reporter evidence satisfies its acceptance boundary'], ]) { expect(manifest.acceptanceCriteria.filter((value: { criterion: string }) => value.criterion === criterion)) @@ -50,6 +49,20 @@ describe('capacity-provider guarantee execution bindings', () => { expect(registry.verifiers[noop]).toMatchObject({ kind: 'nodeTestCase', testFile: 'tests/acceptance/workday/living-graph.test.ts', testName: 'Living graph identical reconciliation and node inspection preserve assignments usage and every public graph record' }); const product = 'sdk.golden.live.product'; + const profile = 'agent.golden.architecture-profile-custody-live-1'; + expect(manifest.acceptanceCriteria.filter((value: { criterion: string }) => value.criterion === 'a6f86094d3c2dc802a54319c3b55c1fb50fa3f1bdc5f9ee831088347c22732bb')) + .toEqual([{ criterion: 'a6f86094d3c2dc802a54319c3b55c1fb50fa3f1bdc5f9ee831088347c22732bb', verifierRefs: [profile, product] }]); + expect(refs.filter(value => value === profile)).toHaveLength(1); + expect(refs.indexOf(profile)).toBeGreaterThan(refs.indexOf('agent.golden.live.campaign')); + expect(refs.indexOf(profile)).toBeLessThan(refs.indexOf(product)); + expect(registry.verifiers[profile]).toMatchObject({ kind: 'nodeTestCase', testFile: 'tests/acceptance/workday/profile-custody.test.ts', + testName: 'Every actual assignment matches independently read exact governed profile bytes handler prompt parameters and narrower grants' }); + expect(manifest.acceptanceCriteria.filter((value: { criterion: string }) => value.criterion === '6d6749cd8c2676c6c50c009b55025ec3b951589db602440f63b76ba021159f20')) + .toEqual([{ criterion: '6d6749cd8c2676c6c50c009b55025ec3b951589db602440f63b76ba021159f20', verifierRefs: ['agent.golden.live.graph', product] }]); + expect(refs.filter(value => value === 'agent.golden.live.graph')).toHaveLength(1); + expect(refs.indexOf('agent.golden.live.graph')).toBeGreaterThan(refs.indexOf('agent.golden.live.campaign')); + expect(registry.verifiers['agent.golden.live.graph']).toMatchObject({ kind: 'nodeTestCase', testFile: 'tests/acceptance/sdk-runtime-golden.test.ts', + testName: 'Golden runtime graph evidence satisfies its acceptance boundary' }); for (const criterion of ['9a9b626b666f555139baf8aabcead659ca30e7f88ad8002f73f2b35ddf542ad4', '8fc0d803455b8e437a458732d5b6ba575f885eb35ba387e93e14230dd40d27b7', ...sdkFixedGraphCriteria, @@ -120,13 +133,14 @@ describe('capacity-provider guarantee execution bindings', () => { 'b918b519ac545fdd61faab208687ccc64e1f3d03c9a84dfc57a5cfee6190f3f3', ].map(criterion => ({ criterion, verifierRefs: ['agent.golden.live.campaign-freeze'] })); expect(manifest.acceptanceCriteria).toEqual([ + { criterion: 'a6f86094d3c2dc802a54319c3b55c1fb50fa3f1bdc5f9ee831088347c22732bb', verifierRefs: ['agent.golden.architecture-profile-custody-live-1', 'sdk.golden.live.product'] }, ...sdkFixedGraphCriteria.map(criterion => ({ criterion, verifierRefs: ['sdk.golden.live.product'] })), { criterion: 'b635827ed7862d56b8764dfad30114b9ee18d02b4aa709a257841e18a1bd0f0d', verifierRefs: ['agent.golden.execution-clock-observation-live-1', 'api.golden.execution-protected-observation-live-1'] }, { criterion: '7eaf9fdc1c264e2d0b28d2fc751da33a2ffcf98ca0d6446ac6e8672503dfa533', verifierRefs: ['agent.golden.live.reporter', 'agent.golden.live.settlement'] }, ...preRunBindings, { criterion: '80496650a6ed5888fbda21ddcb15047d221874adb709887306dede9e3e93ff55', verifierRefs: ['agent.golden.live.results'] }, - { criterion: '6d6749cd8c2676c6c50c009b55025ec3b951589db602440f63b76ba021159f20', verifierRefs: ['agent.golden.live.graph'] }, + { criterion: '6d6749cd8c2676c6c50c009b55025ec3b951589db602440f63b76ba021159f20', verifierRefs: ['agent.golden.live.graph', 'sdk.golden.live.product'] }, { criterion: '3bf7f808c3baf5cc63fc5e8e8c065bc2cd87dea9e2df9aae4d1924565af6acd8', verifierRefs: ['agent.golden.live.reporter'] }, { criterion: '9a9b626b666f555139baf8aabcead659ca30e7f88ad8002f73f2b35ddf542ad4', verifierRefs: ['sdk.golden.live.product'] }, { criterion: '8fc0d803455b8e437a458732d5b6ba575f885eb35ba387e93e14230dd40d27b7', verifierRefs: ['sdk.golden.live.product'] }, diff --git a/tests/modern/kernel/architecture/custody/graph/living-graph-native.test.ts b/tests/modern/kernel/architecture/custody/graph/living-graph-native.test.ts index be0d4cdf..e667ad3a 100644 --- a/tests/modern/kernel/architecture/custody/graph/living-graph-native.test.ts +++ b/tests/modern/kernel/architecture/custody/graph/living-graph-native.test.ts @@ -5,6 +5,67 @@ import { ControlPlaneClient, defaultLocalControlPlaneServer } from '@treeseed/sd import { controlPlaneOperation } from '@treeseed/sdk/operator-contracts'; import { graphRevisionSchema } from '@treeseed/sdk/agent-capacity'; import { verifyGraphNoopPlan } from '../../../../../acceptance/workday/support/evidence-pages.ts'; +import { verifyGovernedProfile } from '../../../../../acceptance/workday/support/assignment-authority.ts'; +import { profileOrderFixture, profileOrderFailures, mutateProfileOrder } from './profile-order-fixture.ts'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +it('native exact Git profile and public SDK graph reads reject changed governed dependencies and retain denied observations before unchanged retry', async () => { + const f = profileOrderFixture(), root = mkdtempSync(join(tmpdir(), 'agent-profile-order-')); + const git = (...args: string[]) => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); + const requests: Array<{ path?: string; body: unknown }> = [], failures: Array<{ mode: string; error: string }> = []; + let mode = 'exact'; + const server = createServer((request, response) => { + let bytes = ''; request.setEncoding('utf8'); request.on('data', value => { bytes += String(value); }); + request.on('end', () => { + const body = bytes ? JSON.parse(bytes) as { ref: string; paths: string[] } : undefined; + requests.push({ path: request.url, body }); response.setHeader('content-type', 'application/json'); + if (mode === 'interrupted') { request.socket.destroy(); return; } + if (mode === 'denied' || mode === 'unavailable') { response.writeHead(mode === 'denied' ? 403 : 503).end(JSON.stringify({ title: 'Controlled native denial' })); return; } + if (mode === 'json') { response.end('{'); return; } + if (body) { + assert.equal(body.ref, f.item.assignmentAttempt.effectiveProfile.profileRef.commit); assert.deepEqual(body.paths, [f.file.path]); + const content = execFileSync('git', ['show', `${body.ref}:${body.paths[0]}`], { cwd: root, encoding: 'utf8' }); + response.end(JSON.stringify({ data: { resolvedRef: body.ref, files: [{ path: f.file.path, content: mode === 'bytes' ? `${content}changed` : content }] } })); return; + } + const graph = structuredClone(f.graph); mutateProfileOrder(graph, mode); response.end(JSON.stringify({ data: graph })); + }); + }); + try { + git('init', '-b', 'fixture-base'); git('config', 'user.name', 'Native Profile Fixture'); git('config', 'user.email', 'profile@example.invalid'); + mkdirSync(join(root, 'agents')); writeFileSync(join(root, f.file.path), f.file.content); git('add', '.'); git('commit', '-m', 'original governed profile'); + const commit = git('rev-parse', 'HEAD'); f.item.assignmentAttempt.effectiveProfile.profileRef.commit = commit; + const held = structuredClone(f); + await new Promise((accept, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', accept); }); + const address = server.address(); assert.ok(address && typeof address !== 'string'); + const client = new ControlPlaneClient({ profile: defaultLocalControlPlaneServer({ TREESEED_API_BASE_URL: `http://127.0.0.1:${address.port}` }), accessToken: 'controlled-native-profile' }); + const observe = async () => { + const profile = f.item.assignmentAttempt.effectiveProfile.profileRef; + const content = (await client.invoke(controlPlaneOperation('treedx.repositories.files.read'), { path: { projectId: f.item.assignmentAttempt.projectId, repoId: profile.repository! }, query: {}, + body: { ref: profile.commit, paths: [profile.path!], encoding: 'utf8', parseFrontmatter: true } })).data as { resolvedRef: string; files: Array> }; + assert.equal(content.resolvedRef, profile.commit); assert.equal(content.files.length, 1); + const graph = (await client.invoke(controlPlaneOperation('execution.graph.show'), { path: { teamId: f.graph.teamId }, query: {}, body: undefined })).data; + assert.ok(graph && typeof graph === 'object'); verifyGovernedProfile(f.item, content.files[0]!, graph as Record); + }; + await Promise.all([observe(), observe()]); + for (const fault of [...profileOrderFailures, 'bytes', 'denied', 'unavailable', 'json', 'interrupted']) { + mode = fault; + try { await observe(); failures.push({ mode, error: 'unexpected pass' }); } + catch (failure) { failures.push({ mode, error: failure instanceof Error ? failure.message : 'Native profile failure' }); } + } + expect(failures).toHaveLength(16); expect(failures.every(value => value.error !== 'unexpected pass')).toBe(true); + const retained = structuredClone(failures); mode = 'exact'; await observe(); + expect(failures).toEqual(retained); expect(f).toEqual(held); expect(git('rev-parse', 'HEAD')).toBe(commit); + expect(requests.filter(value => value.body)).toHaveLength(19); expect(requests.filter(value => !value.body)).toHaveLength(15); + // Actual Git bytes and public SDK/HTTP; upstream graph is controlled, not an API producer or managed passing receipt. + } finally { + server.closeAllConnections(); if (server.listening) await new Promise((accept, reject) => server.close(error => error ? reject(error) : accept())); + rmSync(root, { recursive: true, force: true }); + } + expect(server.listening).toBe(false); +}, 30_000); it('native public reconciliation denies changed previews before actual commands and retains original receipts through concurrent unchanged retry', async () => { const graph = { teamId: 'native-team', revision: 1, digest: `sha256:${'d'.repeat(64)}`, nodes: [], edges: [] }; diff --git a/tests/modern/kernel/architecture/custody/graph/profile-order-fixture.ts b/tests/modern/kernel/architecture/custody/graph/profile-order-fixture.ts new file mode 100644 index 00000000..3936d9f5 --- /dev/null +++ b/tests/modern/kernel/architecture/custody/graph/profile-order-fixture.ts @@ -0,0 +1,39 @@ +import { createHash } from 'node:crypto'; +import { stringify } from 'yaml'; +import { assignmentAttemptSchema } from '@treeseed/sdk/agent-capacity'; +import { request } from '../../../provider-kernel-fixture.ts'; +import { portableProfile } from '../../portable/portable-kernel-fixture.ts'; + +export const profileOrderFailures = ['missing', 'extra', 'duplicate', 'actor', 'class', 'source', 'team', 'project', 'removed', 'foreign', 'work-item'] as const; +export function profileOrderFixture(dependencies = ['earlier-configured-class']) { + const definition = portableProfile(), activity = definition.activityProfiles.acting!; + activity.dependsOn = { agents: dependencies }; + const content = stringify(definition), path = 'agents/renamed-author.yaml'; + const attempt = assignmentAttemptSchema.parse({ ...assignmentAttemptSchema.parse(request().assignment.assignmentAttempt), agentClass: definition.agentClass, + effectiveProfile: { profileRef: { store: 'treedx', model: 'agent', id: definition.id, revision: 1, + digest: `sha256:${createHash('sha256').update(content).digest('hex')}`, repository: 'native-library', commit: 'a'.repeat(40), path }, + activity: 'acting', handler: activity.handler, handlerOrigin: 'agent-package', prompt: activity.prompt, + parameters: activity.parameters, permissionCeiling: activity.permissions } }); + const actor = { id: attempt.nodeId, teamId: attempt.teamId, projectId: attempt.projectId, workItemId: attempt.workItemId, + sourceRef: attempt.sourceRef, agentClass: attempt.agentClass, pairRole: 'actor' }; + const predecessor = { ...actor, id: 'earlier-actor', workItemId: 'earlier-work', agentClass: 'earlier-configured-class' }; + const reviewer = { ...predecessor, id: 'earlier-reviewer', agentClass: 'reviewer', pairRole: 'reviewer' }; + const edge = { schemaVersion: 'treeseed.execution-edge/v1', id: 'profile-edge', teamId: attempt.teamId, + fromNodeId: reviewer.id, toNodeId: actor.id, provenance: 'profile-agent', sourceRef: attempt.sourceRef, graphRevisionCreated: 1 }; + return { item: { id: attempt.id, assignmentAttempt: attempt }, file: { path, content }, + graph: { teamId: attempt.teamId, nodes: [actor, predecessor, reviewer], edges: [edge] } }; +} +export function mutateProfileOrder(graph: ReturnType['graph'], mode: string): void { + const edge = graph.edges[0]!; + if (mode === 'missing') graph.edges = []; + else if (mode === 'extra') graph.edges.push({ ...edge, id: 'extra-edge', fromNodeId: 'unrelated-reviewer' }); + else if (mode === 'duplicate') graph.edges.push(structuredClone(edge)); + else if (mode === 'actor') edge.fromNodeId = 'earlier-actor'; + else if (mode === 'class') graph.nodes[1]!.agentClass = 'foreign-class'; + else if (mode === 'source') edge.sourceRef = { ...edge.sourceRef, revision: 2 }; + else if (mode === 'team') edge.teamId = 'foreign-team'; + else if (mode === 'project') graph.nodes[1]!.projectId = 'foreign-project'; + else if (mode === 'removed') Reflect.set(edge, 'graphRevisionRemoved', 2); + else if (mode === 'foreign') edge.fromNodeId = 'foreign-node'; + else if (mode === 'work-item') edge.provenance = 'work-item'; +} diff --git a/tests/modern/kernel/architecture/custody/profile-custody-readback.test.ts b/tests/modern/kernel/architecture/custody/profile-custody-readback.test.ts index 778d09d9..56fd0752 100644 --- a/tests/modern/kernel/architecture/custody/profile-custody-readback.test.ts +++ b/tests/modern/kernel/architecture/custody/profile-custody-readback.test.ts @@ -8,6 +8,7 @@ import { portableProfile } from '../portable/portable-kernel-fixture.ts'; import { verifyGovernedProfile, verifyHandlerInspection } from '../../../../acceptance/workday/support/assignment-authority.ts'; import { inspectLiveAssignmentProfile } from '../../../../acceptance/workday/support/monitoring/live-assignment-records.ts'; import { liveAssignmentRecord } from '../../../guarantees/monitoring/live-assignment-fixture.ts'; +import { profileOrderFixture, profileOrderFailures, mutateProfileOrder } from './graph/profile-order-fixture.ts'; await import('../../../../acceptance/workday/profile-custody.test.ts'); function input() { @@ -21,6 +22,24 @@ function input() { return { item: { id: attempt.id, assignmentAttempt: attempt }, file: { path, content } }; } describe('independent governed profile assertion contract', () => { + it('rejects graph dependencies that differ from independently read renamed governed profile bytes without rewriting authority', () => { + const f = profileOrderFixture(), inspect = (value: typeof f) => Reflect.apply(verifyGovernedProfile, undefined, [value.item, value.file, value.graph]); + const before = structuredClone(f); expect(() => inspect(f)).not.toThrow(); expect(f).toEqual(before); + for (const mode of profileOrderFailures) { + const changed = structuredClone(f); mutateProfileOrder(changed.graph, mode); const held = structuredClone(changed); + expect.soft(() => inspect(changed), mode).toThrow('ACCEPTANCE_PROFILE_DEPENDENCY'); expect(changed).toEqual(held); + } + expect(() => inspect(f)).not.toThrow(); expect(f).toEqual(before); + const independent = profileOrderFixture([]); independent.graph.edges = []; + expect(() => inspect(independent)).not.toThrow(); + const unreviewed = profileOrderFixture(); unreviewed.graph.nodes.pop(); unreviewed.graph.edges[0]!.fromNodeId = 'earlier-actor'; + expect(() => inspect(unreviewed)).not.toThrow(); + const external = profileOrderFixture(), attempt = external.item.assignmentAttempt; + Reflect.set(external.graph, 'nodes', [external.graph.nodes[0], { id: 'missing-class', teamId: attempt.teamId, projectId: attempt.projectId, + sourceRef: attempt.sourceRef, kind: 'condition', condition: { conditionType: 'external', expectedState: 'agent-class:earlier-configured-class', subjectRef: attempt.sourceRef } }]); + external.graph.edges[0]!.fromNodeId = 'missing-class'; expect(() => inspect(external)).not.toThrow(); + Reflect.set(external.graph, 'nodes', [external.graph.nodes[0]]); expect(() => inspect(external)).toThrow('ACCEPTANCE_PROFILE_DEPENDENCY'); + }); it('live profile inspection reads the independent canonical assignment exact governed bytes and public handler boundary before accepting a new observation', () => { const f = input(), item = liveAssignmentRecord(f.item.assignmentAttempt), attempt = f.item.assignmentAttempt, profile = attempt.effectiveProfile; const handler = { id: profile.handler, origin: profile.handlerOrigin };