Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ id: cli.golden.component-boundaries
title: Golden operator component boundaries
scope: local-component-tests
workflow:
- id: restart-current-custody-unit
action: { verifier: cli.golden.restart-current-custody-unit }
expect: { status: passed }
- id: restart-refresh-denial-unit
action: { verifier: cli.golden.restart-refresh-denial-unit }
expect: { status: passed }
- id: restart-current-custody-native
action: { verifier: cli.golden.restart-current-custody-native }
expect: { status: passed }
- id: execution-diagnostics-read-unit
action: { verifier: cli.golden.execution-diagnostics-read-unit }
expect: { status: passed }
Expand Down
15 changes: 15 additions & 0 deletions guarantees/verifiers/golden.verifiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -811,3 +811,18 @@ verifiers:
ownerPackage: "@treeseed/cli"
testFile: tests/integration/development-rebuild.test.ts
testName: "native public rebuild and restart retain exact candidate output without transient released execution"
cli.golden.restart-current-custody-unit:
kind: nodeTestCase
ownerPackage: "@treeseed/cli"
testFile: tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts
testName: restart refreshes current manager custody before handoff and omits direct host readiness for a managed endpoint
cli.golden.restart-refresh-denial-unit:
kind: nodeTestCase
ownerPackage: "@treeseed/cli"
testFile: tests/unit/command-boundary/development/lifecycle-entrypoints.test.ts
testName: refused restart refresh performs no build start release or route mutation
cli.golden.restart-current-custody-native:
kind: nodeTestCase
ownerPackage: "@treeseed/cli"
testFile: tests/integration/development-rebuild.test.ts
testName: native restart refreshes changed caller recipe and retains one exact build and endpoint through retry
6 changes: 4 additions & 2 deletions src/cli/commands/development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,10 @@ async function restartConsumer(input: { state: LocalSessionState; runtime: Devel
}
async function restart(invocation: ParsedInvocation, context: CommandContext, state: LocalSessionState, sessionId: string) {
const selection = parseSelection(`${invocation.arguments[0]}=candidate`);
const runtimes = (await loadDevelopmentRuntimes(state.manifest)).map(({ runtime }) => runtime);
if (invocation.options.plan !== true) await invoke(context, 'local.dev.session.refresh', { sessionId, runtimes });
const status = await invoke(context, 'local.dev.status', { sessionId, all: false }) as DevelopmentStatusRecord;
const record = { ...status, runtimes: (await loadDevelopmentRuntimes(state.manifest)).map(({ runtime }) => runtime) };
const record = { ...status, runtimes };
const selected = record.session.targets.find((entry) => entry.projectId === selection.projectId && entry.targetId === selection.targetId);
if (!selected || selected.mode === 'released') throw new Error(`${selection.projectId}.${selection.targetId} is not selected for local development.`);
const { runtime, target } = selectedTarget(record, selection.projectId, selection.targetId);
Expand All @@ -325,7 +327,7 @@ async function restart(invocation: ParsedInvocation, context: CommandContext, st
}
if (target.kind === 'package-watch') throw new Error('Package-watch targets rebuild atomically and do not support restart.');
await restartConsumer({ state, runtime, target, worktree: repository.worktree, mode: selected.mode as 'candidate' | 'live', context, recordGeneration: false });
await invoke(context, 'local.dev.use', { sessionId, projectId: selection.projectId, targetId: selection.targetId, mode: selected.mode, ...(target.endpoints[0] ? { port: target.endpoints[0].port } : {}) });
await invoke(context, 'local.dev.use', { sessionId, projectId: selection.projectId, targetId: selection.targetId, mode: selected.mode, ...(!usesManagerBuild(target) && target.endpoints[0] ? { port: target.endpoints[0].port } : {}) });
saveState(state, context.env);
return { sessionId, target: `${selection.projectId}.${selection.targetId}`, restarted: true, record: await invoke(context, 'local.dev.status', { sessionId, all: false }) };
}
Expand Down
1 change: 1 addition & 0 deletions tests/contract/package/thin-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ test('guarantee metadata binds owner tests without adding a second CLI implement
'cli.golden.freeze-earlier-artifact': {testFile:'tests/integration/development-freeze.test.ts',testName:'native freeze rejects earlier artifact mutation by a later owner build'},
'cli.golden.freeze-repeat': {testFile:'tests/integration/development-freeze.test.ts',testName:'native freeze preserves repeated declared outputs and dirty source without claiming a verification pass'},
'cli.golden.freeze-failure': {testFile:'tests/integration/development-freeze.test.ts',testName:'native freeze failure retains partial outputs without persisting or registering a candidate'},
'cli.golden.restart-current-custody-native': {testFile:'tests/integration/development-rebuild.test.ts',testName:'native restart refreshes changed caller recipe and retains one exact build and endpoint through retry'},
'cli.golden.freeze-wildcard': {testFile:'tests/integration/development-freeze.test.ts',testName:'native freeze admits new wildcard output directories without treating generated bytes as source'},
'cli.golden.freeze-empty-target': {testFile:'tests/integration/development-freeze.test.ts',testName:'native freeze blocks a target with no declared outputs before executing a later build'},
'cli.golden.source-parent': {testFile:'tests/unit/command-boundary/development/source-closure.test.ts',testName:'development closure rejects a selected directory borrowing its parent Git root'},
Expand Down
24 changes: 24 additions & 0 deletions tests/integration/development-rebuild.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ import {resolve} from 'node:path';
import type {DevelopmentRuntime} from '@treeseed/sdk/development';
import {rebuildFixture} from '../support/development-rebuild.ts';

test('native restart refreshes changed caller recipe and retains one exact build and endpoint through retry', async()=>{
const f=await rebuildFixture();try{
const file=resolve(f.root,'treeseed.package.yaml');
const document=JSON.parse(readFileSync(file,'utf8')) as {development:DevelopmentRuntime};
const target=document.development.targets[0]!;
target.endpoints=[{id:'http',protocol:'http',port:4321,visibility:'host',canonicalAlias:'specimen.treeseed.localhost',authentication:'application'}];
target.operations.build!.environment={FAIL_AT:'1'};
writeFileSync(file,JSON.stringify(document));const source=readFileSync(file);
const build=readFileSync(resolve(f.root,'scripts/build.ts'));
assert.equal(await f.invoke('restart'),1,f.output.join(''));
assert.deepEqual(f.managerRuntimes(),[document.development]);
assert.deepEqual(f.calls.slice(0,2),['local.dev.session.refresh','local.dev.status']);
assert.deepEqual(f.builds(),['1']);assert.deepEqual(f.selectedBytes,[]);assert.deepEqual([...f.uses],[]);
const failed=f.output.join('');assert.match(failed,/failed/i);
assert.equal(f.mode(),'candidate');assert.equal(f.calls.includes('local.dev.container:stop'),false);
assert.equal(await f.invoke('restart'),0,f.output.join(''));
assert.deepEqual(f.builds(),['1','2']);assert.deepEqual(f.selectedBytes,['built-2']);
assert.equal(f.uses.length,1);assert.equal(f.uses[0]!.port,4321);
assert.equal(readFileSync(resolve(f.root,'candidate.bin'),'utf8'),'built-2');
assert.equal(f.output.join('').startsWith(failed),true);
assert.deepEqual(readFileSync(file),source);assert.deepEqual(readFileSync(resolve(f.root,'scripts/build.ts')),build);
}finally{f.close();}
});

// 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.
Expand Down
15 changes: 10 additions & 5 deletions tests/support/development-rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;rejectStart?:boolean;missingBuild?:boolean}={}) {
export async function rebuildFixture(options:{managerOwned?:boolean;failAt?:number;rejectStop?:boolean;rejectStart?:boolean;missingBuild?:boolean;rejectRefresh?: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;
Expand All @@ -26,9 +26,14 @@ export async function rebuildFixture(options:{managerOwned?:boolean;failAt?:numb
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[]=[],selections:string[]=[];
const uses:Array<{mode:string;port?:number}>=[];
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.session.start'){record={session:payload.session,runtimes:structuredClone(payload.runtimes)};return record;}
if(input.handlerId==='local.dev.session.refresh'){
if(options.rejectRefresh)throw new Error('Fixture refuses runtime refresh.');
record.runtimes=structuredClone(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.');
Expand All @@ -41,15 +46,15 @@ export async function rebuildFixture(options:{managerOwned?:boolean;failAt?:numb
}
return {};
}
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;
if(input.handlerId==='local.dev.use'){uses.push(payload);selections.push(payload.mode);record.session.targets[0]!.mode=payload.mode;return record;}
if(input.handlerId==='local.dev.status'||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;selections.length=0;
}catch(error){rmSync(root,{recursive:true,force:true});rmSync(state,{recursive:true,force:true});throw error;}
return {root,calls,output,selectedBytes,selections,
return {root,calls,output,selectedBytes,selections,uses,managerRuntimes:()=>structuredClone(record.runtimes),
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,43 @@ import type { DevelopmentRuntime, DevelopmentTarget } from '@treeseed/sdk/develo
import { startPackageSynchronizer, stopProcess, waitForPackageOverlay } from '../../../../src/cli/commands/development-support/overlays.ts';
import { rebuildFixture, rebuildTarget } from '../../../support/development-rebuild.ts';

test('restart refreshes current manager custody before handoff and omits direct host readiness for a managed endpoint', async () => {
const f = await rebuildFixture({ managerOwned: true });
try {
const file = resolve(f.root, 'treeseed.package.yaml');
const document = JSON.parse(readFileSync(file, 'utf8')) as { development: DevelopmentRuntime };
document.development.targets[0]!.endpoints = [{ id: 'http', protocol: 'http', port: 4000,
visibility: 'host', canonicalAlias: 'specimen.treeseed.localhost', authentication: 'application' }];
writeFileSync(file, JSON.stringify(document));
const before = readFileSync(file), stored = f.managerRuntimes();
assert.equal(await f.invoke('restart', true), 0, f.output.join(''));
assert.deepEqual(f.calls, ['local.dev.status']);
assert.deepEqual(f.managerRuntimes(), stored);
f.calls.length = 0;
assert.equal(await f.invoke('restart'), 0, f.output.join(''));
assert.deepEqual(f.calls.slice(0, 2), ['local.dev.session.refresh', 'local.dev.status']);
assert.deepEqual(f.managerRuntimes(), [document.development]);
assert.equal(Object.hasOwn(f.uses[0]!, 'port'), false);
assert.equal(f.mode(), 'candidate');
assert.deepEqual(f.selectedBytes, ['manager-owned']);
assert.deepEqual(f.builds(), []);
assert.deepEqual(readFileSync(file), before);
} finally { f.close(); }
});

test('refused restart refresh performs no build start release or route mutation', async () => {
const f = await rebuildFixture({ managerOwned: true, rejectRefresh: true });
try {
const before = f.managerRuntimes();
assert.equal(await f.invoke('restart'), 1, f.output.join(''));
assert.match(f.output.join(''), /refuses runtime refresh/);
assert.deepEqual(f.calls, ['local.dev.session.refresh']);
assert.deepEqual(f.managerRuntimes(), before);
assert.deepEqual(f.builds(), []); assert.deepEqual(f.selectedBytes, []);
assert.deepEqual(f.uses, []); assert.equal(f.mode(), 'candidate');
} finally { f.close(); }
});

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 });
Expand Down Expand Up @@ -95,8 +132,8 @@ test('boot resume and manual use re-read state under the same lifecycle lock', {
operations: { start: { command: 'manager-runtime' } }, dependencies: [], endpoints: [{ id: 'http', protocol: 'http', port: 4000 }], ready: { kind: 'process', graceSeconds: 0 } }] }],
};
let started = false, starts = 0;
const context = {
cwd: root, env,
const context: CommandContext = {
cwd: root, env, interactiveUi: false, outputFormat: 'json', write() {},
hostInvoke: async (request: { handlerId: string; options: { payload?: unknown } }) => {
const payload = request.options.payload ? JSON.parse(String(request.options.payload)) : {};
if (request.handlerId === 'local.dev.status') return payload.all ? { sessions: [record] } : record;
Expand All @@ -120,8 +157,10 @@ test('boot resume and manual use re-read state under the same lifecycle lock', {
if (request.handlerId === 'local.host.start') return { state: 'running', changed: true };
throw new Error(`Unexpected operation ${request.handlerId}`);
},
} as CommandContext;
const invocation = { command: { name: 'dev use' }, arguments: ['api.operations-runner=candidate'], options: { session: sessionId } } as ParsedInvocation;
};
const selectedCommand = resolveCommand(['dev', 'use', 'api.operations-runner=candidate', '--session', sessionId]);
assert.ok(selectedCommand);
const invocation = parseInvocation(selectedCommand.command, selectedCommand.rest);
try {
await Promise.all([resumeDevelopmentSession(sessionId, context), runDevelopment(invocation, context)]);
assert.equal(starts, 1);
Expand Down
Loading