From 8bff508e7d8e7f7b98f530c58e072e9e3834bda1 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Thu, 8 Oct 2026 21:28:42 -0400 Subject: [PATCH 1/4] test: prohibit transient released execution during provider handoff --- .../scenes/component-boundaries.scene.yaml | 9 +++++ guarantees/verifiers/golden.verifiers.yaml | 15 +++++++++ tests/integration/development-rebuild.test.ts | 21 ++++++++++++ tests/support/development-rebuild.ts | 15 +++++---- .../development/lifecycle-entrypoints.test.ts | 33 ++++++++++++++++++- 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/guarantees/agent/golden/scenes/component-boundaries.scene.yaml b/guarantees/agent/golden/scenes/component-boundaries.scene.yaml index 234277b2..8fae27ec 100644 --- a/guarantees/agent/golden/scenes/component-boundaries.scene.yaml +++ b/guarantees/agent/golden/scenes/component-boundaries.scene.yaml @@ -480,3 +480,12 @@ workflow: - id: operator-unresolved-recovery-native action: { verifier: cli.golden.operator-unresolved-recovery-native } expect: { status: passed } + - id: provider-handoff-selection-unit + action: { verifier: cli.golden.provider-handoff-selection-unit } + expect: { status: passed } + - id: provider-handoff-denial-unit + action: { verifier: cli.golden.provider-handoff-denial-unit } + expect: { status: passed } + - id: provider-handoff-selection-native + action: { verifier: cli.golden.provider-handoff-selection-native } + expect: { status: passed } diff --git a/guarantees/verifiers/golden.verifiers.yaml b/guarantees/verifiers/golden.verifiers.yaml index 27eb465d..babfc351 100644 --- a/guarantees/verifiers/golden.verifiers.yaml +++ b/guarantees/verifiers/golden.verifiers.yaml @@ -796,3 +796,18 @@ verifiers: ownerPackage: "@treeseed/cli" testFile: tests/unit/command-boundary/assignments/recovery.test.ts testName: "packaged native recovery retains exact confirmation denial version and unresolved response through immutable retry without usage writes" + cli.golden.provider-handoff-selection-unit: + kind: nodeTestCase + ownerPackage: "@treeseed/cli" + testFile: tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts + testName: "managed rebuild and restart delegate one owning handoff without selecting or starting released code" + cli.golden.provider-handoff-denial-unit: + kind: nodeTestCase + ownerPackage: "@treeseed/cli" + testFile: tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts + testName: "managed handoff denial preserves the original selection without release fallback or a passing retry" + cli.golden.provider-handoff-selection-native: + kind: nodeTestCase + ownerPackage: "@treeseed/cli" + testFile: tests/integration/development-rebuild.test.ts + testName: "native public rebuild and restart retain exact candidate output without transient released execution" diff --git a/tests/integration/development-rebuild.test.ts b/tests/integration/development-rebuild.test.ts index ab805858..25d3cf9f 100644 --- a/tests/integration/development-rebuild.test.ts +++ b/tests/integration/development-rebuild.test.ts @@ -5,6 +5,27 @@ import {resolve} from 'node:path'; import type {DevelopmentRuntime} from '@treeseed/sdk/development'; import {rebuildFixture} from '../support/development-rebuild.ts'; +// Real public CLI, source/Git custody, native caller builds and selected file +// readback. Manager replies remain controlled inputs: not physical Docker or +// live provider claim-retention proof. +test('native public rebuild and restart retain exact candidate output without transient released execution',async()=>{ + const f=await rebuildFixture();try{ + const source=readFileSync(resolve(f.root,'treeseed.package.yaml')); + const build=readFileSync(resolve(f.root,'scripts/build.ts')); + for(const [index,command] of ['rebuild','restart','rebuild'].entries()){ + assert.equal(await f.invoke(command),0,f.output.join('')); + assert.deepEqual(f.builds(),Array.from({length:index+1},(_,n)=>String(n+1))); + assert.deepEqual(f.selectedBytes,Array.from({length:index+1},(_,n)=>`built-${n+1}`)); + assert.equal(readFileSync(resolve(f.root,'candidate.bin'),'utf8'),`built-${index+1}`); + assert.equal(f.mode(),'candidate');assert.deepEqual(f.selections,Array(index+1).fill('candidate')); + assert.equal(f.calls.includes('local.dev.container:stop'),false); + assert.equal(f.calls.filter(call=>call==='local.dev.container:start').length,index+1); + assert.deepEqual(readFileSync(resolve(f.root,'treeseed.package.yaml')),source); + assert.deepEqual(readFileSync(resolve(f.root,'scripts/build.ts')),build); + } + }finally{f.close();} +}); + test('native development rebuild executes its caller build once before selecting exact output',async()=>{ const f=await rebuildFixture();try{assert.equal(await f.invoke(),0,f.output.join(''));assert.deepEqual(f.builds(),['1']);assert.deepEqual(f.selectedBytes,['built-1']);assert.equal(f.mode(),'candidate');}finally{f.close();} }); diff --git a/tests/support/development-rebuild.ts b/tests/support/development-rebuild.ts index 949f4b1d..c0e1821b 100644 --- a/tests/support/development-rebuild.ts +++ b/tests/support/development-rebuild.ts @@ -13,7 +13,7 @@ export function rebuildTarget(managerOwned=false):DevelopmentTarget { ready:{kind:'process',graceSeconds:0},outputs:[],endpoints:[],dependencies:[],statePolicy:'stateless',migrationPolicy:'none',secretRefs:{},shutdown:{graceSeconds:1,activeWorkPolicy:'block'},resources:{},logs:[],forbiddenOperations:[],promotion:{liveAdmissible:false,candidateRequiresVerification:true}}; } -export async function rebuildFixture(options:{managerOwned?:boolean;failAt?:number;rejectStop?:boolean;missingBuild?:boolean}={}) { +export async function rebuildFixture(options:{managerOwned?:boolean;failAt?:number;rejectStop?:boolean;rejectStart?:boolean;missingBuild?:boolean}={}) { const root=mkdtempSync(resolve(tmpdir(),'cli-rebuild-source-')),state=mkdtempSync(resolve(tmpdir(),'cli-rebuild-state-')); const target=rebuildTarget(options.managerOwned); if(options.missingBuild)delete target.operations.build; @@ -25,25 +25,28 @@ export async function rebuildFixture(options:{managerOwned?:boolean;failAt?:numb writeFileSync(resolve(root,'.gitignore'),'build.log\ncandidate.bin\n'); const git=(...args:string[])=>execFileSync('git',['-C',root,...args],{encoding:'utf8'}); git('init','-b','staging');git('add','.');git('-c','user.name=Fixture','-c','user.email=fixture@example.invalid','commit','-m','fixture'); - let record:{session:DevelopmentSession;runtimes:DevelopmentRuntime[]};const calls:string[]=[],output:string[]=[],selectedBytes:string[]=[]; + let record:{session:DevelopmentSession;runtimes:DevelopmentRuntime[]};const calls:string[]=[],output:string[]=[],selectedBytes:string[]=[],selections:string[]=[]; const context:CommandContext={cwd:root,env:{...process.env,XDG_STATE_HOME:state},interactiveUi:false,outputFormat:'json',write:value=>output.push(value),hostInvoke:async input=>{ const payload=JSON.parse(String(input.options.payload));calls.push(input.handlerId+(payload.action?`:${payload.action}`:'')); if(input.handlerId==='local.dev.session.start'){record={session:payload.session,runtimes:payload.runtimes};return record;} if(input.handlerId==='local.dev.environment')return {environment:{}}; if(input.handlerId==='local.dev.container'){ if(payload.action==='stop'&&options.rejectStop)throw new Error('Fixture active claim refuses stop.'); - if(payload.action==='start')selectedBytes.push(existsSync(resolve(root,'candidate.bin'))?readFileSync(resolve(root,'candidate.bin'),'utf8'):'manager-owned'); + if(payload.action==='start'){ + if(options.rejectStart)throw new Error('Fixture owning handoff refuses active custody.'); + selectedBytes.push(existsSync(resolve(root,'candidate.bin'))?readFileSync(resolve(root,'candidate.bin'),'utf8'):'manager-owned'); + } return {}; } - if(input.handlerId==='local.dev.use'){record.session.targets[0]!.mode=payload.mode;return record;} + if(input.handlerId==='local.dev.use'){selections.push(payload.mode);record.session.targets[0]!.mode=payload.mode;return record;} if(input.handlerId==='local.dev.status'||input.handlerId==='local.dev.session.refresh'||input.handlerId==='local.dev.rebuild')return record; throw new Error(`Unexpected fixture boundary ${input.handlerId}`); }}; try { assert.equal(await runCommandLine(['dev','session','start',file,'--json'],context),0,output.join('')); - record!.session.targets[0]!.mode='candidate';calls.length=0;output.length=0; + record!.session.targets[0]!.mode='candidate';calls.length=0;output.length=0;selections.length=0; }catch(error){rmSync(root,{recursive:true,force:true});rmSync(state,{recursive:true,force:true});throw error;} - return {root,calls,output,selectedBytes, + return {root,calls,output,selectedBytes,selections, invoke:(command='rebuild',plan=false)=>runCommandLine(['dev',command,'specimen.worker',...(plan?['--plan']:[]),'--json'],context), builds:()=>existsSync(resolve(root,'build.log'))?readFileSync(resolve(root,'build.log'),'utf8').trim().split('\n'):[], mode:()=>record.session.targets[0]!.mode, diff --git a/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts b/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts index 570465aa..77c8fcb3 100644 --- a/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts +++ b/tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts @@ -10,7 +10,38 @@ import { parseInvocation } from '../../../../src/cli/parser.ts'; import { resolveCommand } from '../../../../src/cli/registry.ts'; import type { DevelopmentRuntime, DevelopmentTarget } from '@treeseed/sdk/development'; import { startPackageSynchronizer, stopProcess, waitForPackageOverlay } from '../../../../src/cli/commands/development-support/overlays.ts'; -import { rebuildTarget } from '../../../support/development-rebuild.ts'; +import { rebuildFixture, rebuildTarget } from '../../../support/development-rebuild.ts'; + +test('managed rebuild and restart delegate one owning handoff without selecting or starting released code', async () => { + for (const command of ['rebuild', 'restart']) { + const f = await rebuildFixture({ managerOwned: true }); + try { + assert.equal(await f.invoke(command), 0, f.output.join('')); + assert.equal(f.mode(), 'candidate'); + assert.deepEqual(f.selections, ['candidate']); + assert.deepEqual(f.selectedBytes, ['manager-owned']); + assert.deepEqual(f.builds(), []); + assert.equal(f.calls.filter(call => call === 'local.dev.container:start').length, 1); + assert.equal(f.calls.includes('local.dev.container:stop'), false); + } finally { f.close(); } + } +}); + +test('managed handoff denial preserves the original selection without release fallback or a passing retry', async () => { + for (const command of ['rebuild', 'restart']) { + const f = await rebuildFixture({ managerOwned: true, rejectStart: true }); + try { + assert.equal(await f.invoke(command), 1); + assert.equal(f.mode(), 'candidate'); + assert.deepEqual(f.selections, []); + assert.deepEqual(f.selectedBytes, []); + assert.deepEqual(f.builds(), []); + assert.equal(f.calls.filter(call => call === 'local.dev.container:start').length, 1); + assert.equal(f.calls.includes('local.dev.container:stop'), false); + assert.match(f.output.join(''), /owning handoff refuses active custody/); + } finally { f.close(); } + } +}); test('development freeze and verify planning returns exact nonmutating actions without manager dispatch', async () => { const root = mkdtempSync(resolve(tmpdir(), 'provider-plan-unit-')); From 6cbc1ac00685924fe68c56fd287becd6f98db1fd Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Thu, 8 Oct 2026 21:29:09 -0400 Subject: [PATCH 2/4] test: retain active-claim refusal at owning replacement boundary --- tests/integration/development-rebuild.test.ts | 2 +- tests/support/development-rebuild.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/development-rebuild.test.ts b/tests/integration/development-rebuild.test.ts index 25d3cf9f..37834a18 100644 --- a/tests/integration/development-rebuild.test.ts +++ b/tests/integration/development-rebuild.test.ts @@ -36,7 +36,7 @@ test('native development rebuild preserves selection and avoids stop after a fai const f=await rebuildFixture({failAt:1});try{assert.equal(await f.invoke(),1);assert.deepEqual(f.builds(),['1']);assert.equal(f.mode(),'candidate');assert.equal(f.calls.includes('local.dev.container:stop'),false);assert.deepEqual(f.selectedBytes,[]);}finally{f.close();} }); test('native development rebuild runs once and preserves selection when stop is refused',async()=>{ - const f=await rebuildFixture({rejectStop:true});try{assert.equal(await f.invoke(),1);assert.deepEqual(f.builds(),['1']);assert.equal(f.mode(),'candidate');assert.deepEqual(f.selectedBytes,[]);assert.equal(f.calls.filter(x=>x==='local.dev.container:stop').length,1);}finally{f.close();} + const f=await rebuildFixture({rejectStop:true});try{assert.equal(await f.invoke(),1);assert.deepEqual(f.builds(),['1']);assert.equal(f.mode(),'candidate');assert.deepEqual(f.selectedBytes,[]);assert.equal(f.calls.filter(x=>x==='local.dev.container:start').length,1);assert.equal(f.calls.includes('local.dev.container:stop'),false);assert.deepEqual(f.selections,[]);}finally{f.close();} }); test('native development rebuild plan neither builds nor refreshes manager state',async()=>{ const f=await rebuildFixture();try{assert.equal(await f.invoke('rebuild',true),0,f.output.join(''));assert.deepEqual(f.builds(),[]);assert.deepEqual(f.calls,['local.dev.status']);assert.equal(f.mode(),'candidate');assert.deepEqual(f.selectedBytes,[]);}finally{f.close();} diff --git a/tests/support/development-rebuild.ts b/tests/support/development-rebuild.ts index c0e1821b..478c7c3f 100644 --- a/tests/support/development-rebuild.ts +++ b/tests/support/development-rebuild.ts @@ -33,6 +33,9 @@ export async function rebuildFixture(options:{managerOwned?:boolean;failAt?:numb if(input.handlerId==='local.dev.container'){ if(payload.action==='stop'&&options.rejectStop)throw new Error('Fixture active claim refuses stop.'); if(payload.action==='start'){ + // The owning start boundary performs its drain/active-claim check before + // replacing a selection; refusal must not require an explicit release. + if(options.rejectStop)throw new Error('Fixture active claim refuses stop.'); if(options.rejectStart)throw new Error('Fixture owning handoff refuses active custody.'); selectedBytes.push(existsSync(resolve(root,'candidate.bin'))?readFileSync(resolve(root,'candidate.bin'),'utf8'):'manager-owned'); } From 7c0f798c8eeb533b02b1a5309a4aedb16933069f Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Thu, 8 Oct 2026 21:29:30 -0400 Subject: [PATCH 3/4] fix: replace managed candidates without released-code fallback --- src/cli/commands/development.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/development.ts b/src/cli/commands/development.ts index 18cb7422..7078a125 100644 --- a/src/cli/commands/development.ts +++ b/src/cli/commands/development.ts @@ -290,12 +290,10 @@ async function restartConsumer(input: { state: LocalSessionState; runtime: Devel const resolved = await invoke(context, 'local.dev.environment', { sessionId: state.sessionId, projectId: runtime.project.id, targetId: target.id }) as { environment?: NodeJS.ProcessEnv }; if (usesManagedContainer(target) && !usesManagerBuild(target) && target.kind === 'rebuild-restart' && target.operations.build) runOneShotOperation(state, target.operations.build, worktree, mode, context.env, resolved.environment ?? {},{runtime,target}); await stopProcess(state, key); - if (usesManagedContainer(target)) { - // Preserve the live selection if manager custody refuses an active claim. - await containerOperation(context, state.sessionId, runtime, target, 'stop'); - await invoke(context, 'local.dev.use', {sessionId:state.sessionId,projectId:runtime.project.id,targetId:target.id,mode:'released'}); - } - else if (target.operations.cleanup) runOneShotOperation(state, target.operations.cleanup, worktree, mode, context.env, { TREESEED_DEVELOPMENT_CLEANUP_SCOPE: 'runtime' }); + // Managed start owns preparation, active-work refusal and replacement. + // Explicit stop restores released code and is only for a deliberate release, + // never an intermediate step of rebuilding the selected candidate. + if (!usesManagedContainer(target) && target.operations.cleanup) runOneShotOperation(state, target.operations.cleanup, worktree, mode, context.env, { TREESEED_DEVELOPMENT_CLEANUP_SCOPE: 'runtime' }); if (target.operations.setup) runOneShotOperation(state, target.operations.setup, worktree, mode, context.env, resolved.environment ?? {}); if (!target.operations.start && target.kind === 'rebuild-restart' && target.operations.build) { runOneShotOperation(state, target.operations.build, worktree, mode, context.env, resolved.environment ?? {},{runtime,target}); From cce850d52b44570c301c12ddf115b969b6628514 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Thu, 8 Oct 2026 21:36:42 -0400 Subject: [PATCH 4/4] test(cli): retain owning replacement denial and exact scene inventory --- tests/contract/package/thin-package.test.ts | 1 + .../host/managed-development-container.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/contract/package/thin-package.test.ts b/tests/contract/package/thin-package.test.ts index aa92e6d4..4ed935a6 100644 --- a/tests/contract/package/thin-package.test.ts +++ b/tests/contract/package/thin-package.test.ts @@ -122,6 +122,7 @@ test('guarantee metadata binds owner tests without adding a second CLI implement 'cli.golden.build-docker-unit': {testFile:"tests/unit/command-boundary/development/build-custody.test.ts",testName:"development build custody recognizes a caller-built Docker runtime"}, 'cli.golden.build-direct-unit': {testFile:"tests/unit/command-boundary/development/build-custody.test.ts",testName:"development build custody leaves a direct native runtime in caller custody"}, 'cli.golden.build-once-native': {testFile:"tests/integration/development-rebuild.test.ts",testName:"native development rebuild executes its caller build once before selecting exact output"}, + 'cli.golden.provider-handoff-selection-native': {testFile:'tests/integration/development-rebuild.test.ts',testName:'native public rebuild and restart retain exact candidate output without transient released execution'}, 'cli.golden.build-non-idempotent-native': {testFile:"tests/integration/development-rebuild.test.ts",testName:"native development rebuild does not repeat a non-idempotent successful build"}, 'cli.golden.build-failure-native': {testFile:"tests/integration/development-rebuild.test.ts",testName:"native development rebuild preserves selection and avoids stop after a failed build"}, 'cli.golden.build-refused-native': {testFile:"tests/integration/development-rebuild.test.ts",testName:"native development rebuild runs once and preserves selection when stop is refused"}, diff --git a/tests/unit/command-boundary/host/managed-development-container.test.ts b/tests/unit/command-boundary/host/managed-development-container.test.ts index 6f65f817..40304277 100644 --- a/tests/unit/command-boundary/host/managed-development-container.test.ts +++ b/tests/unit/command-boundary/host/managed-development-container.test.ts @@ -24,7 +24,7 @@ test('container startup and cleanup only invoke the protected manager, including if(request.handlerId==='local.dev.environment')return {environment:{}}; if(request.handlerId==='local.dev.container'){ assert.equal(payload.sessionId,selected);actions.push(payload.action); - if(payload.action==='stop' && rejectStop)throw new Error('Active assignment prevents guest trust replacement.'); + if((payload.action==='start' || payload.action==='stop') && rejectStop)throw new Error('Active assignment prevents guest trust replacement.'); return payload.action==='status'?{registered:false,state:null}:{}; } if(request.handlerId==='local.dev.use'){ @@ -57,12 +57,12 @@ test('container startup and cleanup only invoke the protected manager, including for(const command of ['restart','rebuild']) { assert.equal(await runCommandLine(['dev',command,'api.service','--session',selected,'--json'],context),1); assert.equal(record.session.targets[0].mode,'live','A refused stop must not switch the selected runtime to released'); - assert.equal(actions.at(-1),'stop'); + assert.equal(actions.at(-1),'start','Replacement must drain through the owning start boundary without a released-code stop'); } rejectStop=false; assert.equal(await runCommandLine(['dev','restart','api.service','--session',selected,'--json'],context),0); assert.equal(await runCommandLine(['dev','use','api.service=released','--session',selected,'--json'],context),0); assert.equal(await runCommandLine(['dev','session','stop','--session',selected,'--json'],context),0); - assert.deepEqual(actions,['status','start','stop','stop','stop','start','stop','status']); + assert.deepEqual(actions,['status','start','start','start','start','stop','status']); } finally {rmSync(root,{recursive:true,force:true});} });