@jc_stack/ez-agents 0.1.0-beta.47 → 0.1.0-beta.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +21 -0
- package/Dockerfile +5 -1
- package/THIRD_PARTY_NOTICES.md +6 -3
- package/bin/ez +7 -0
- package/docker/broker-readiness.mjs +54 -0
- package/docker/broker-update-smoke.mjs +86 -0
- package/docker/run.ts +1 -1
- package/docs/application-channel.md +32 -1
- package/docs/architecture/ai-selection.md +18 -3
- package/docs/docker-runtime.md +29 -0
- package/docs/isolated-plugin-update-readiness.md +66 -0
- package/docs/releasing.md +4 -0
- package/docs/scheduling.md +100 -6
- package/docs/selective-monitoring.md +13 -0
- package/docs/upgrades.md +35 -0
- package/docs/work-preflight.md +9 -0
- package/package.json +1 -1
- package/src/ai.ts +22 -3
- package/src/application-channel.ts +43 -3
- package/src/application-cli.ts +7 -1
- package/src/application-origin.ts +2 -0
- package/src/audio.ts +37 -22
- package/src/client-defaults.ts +3 -2
- package/src/config.ts +9 -0
- package/src/control-state.ts +3 -1
- package/src/delivery-socket-client.d.mts +2 -0
- package/src/delivery-socket-client.mjs +2 -0
- package/src/delivery-socket.ts +24 -1
- package/src/event-sources.ts +2 -2
- package/src/executor.ts +72 -1
- package/src/files.ts +47 -3
- package/src/host-executor.ts +38 -29
- package/src/index.ts +41 -18
- package/src/install-tools.mjs +8 -5
- package/src/menu.ts +4 -2
- package/src/plugin-broker.mjs +47 -4
- package/src/plugins/manager.mjs +11 -4
- package/src/run-evidence.ts +78 -0
- package/src/runs.ts +12 -1
- package/src/schedule-cli.ts +69 -8
- package/src/schedule-preflight.ts +23 -0
- package/src/scheduled-tasks.ts +16 -12
- package/src/scheduler.ts +79 -18
- package/src/scripts.ts +167 -0
- package/src/setup.ts +2 -2
- package/src/task-capability.ts +17 -1
- package/src/task-executor.ts +10 -7
- package/src/task-mcp.ts +20 -8
- package/src/tasks.ts +89 -9
- package/src/updates/runtime.mjs +46 -7
- package/test/ai.test.ts +40 -0
- package/test/application-channel.test.ts +16 -1
- package/test/audio.test.ts +19 -0
- package/test/config.test.ts +8 -0
- package/test/executor.test.ts +37 -0
- package/test/files.test.ts +44 -0
- package/test/host-executor.test.ts +45 -2
- package/test/install-json.test.mjs +21 -0
- package/test/plugin-broker.test.mjs +64 -0
- package/test/plugin-manager.test.mjs +20 -0
- package/test/private-speech-config.test.ts +22 -0
- package/test/run-evidence.test.ts +78 -0
- package/test/schedule-preflight.test.ts +49 -0
- package/test/scheduler-host.test.ts +7 -0
- package/test/scheduler-relay.test.ts +38 -3
- package/test/scheduler.test.ts +1 -1
- package/test/script-schedules.test.ts +219 -0
- package/test/task-native.test.ts +50 -12
- package/test/tasks.test.ts +97 -4
- package/test/updates.test.mjs +45 -9
package/test/tasks.test.ts
CHANGED
|
@@ -15,16 +15,17 @@ import { requireOwnerExecution } from '../src/execution-authority.js'
|
|
|
15
15
|
import { execFile } from 'node:child_process'
|
|
16
16
|
import { promisify } from 'node:util'
|
|
17
17
|
|
|
18
|
-
async function fixture(t: test.TestContext, channelOwner = false) {
|
|
18
|
+
async function fixture(t: test.TestContext, channelOwner = false, speech?: (text:string) => Promise<{buffer:Buffer;mimeType:string}>) {
|
|
19
19
|
const dir = await mkdtemp('/tmp/ez-task-test-'), socket = join(dir, 's.sock')
|
|
20
20
|
let accountId = 'account-a', events: SourceEvent[] = [], uncertain = false
|
|
21
21
|
const sends: any[] = [], watches: any[] = []
|
|
22
22
|
const server = createServer(async (req, res) => {
|
|
23
23
|
let text = ''; for await (const chunk of req) text += chunk
|
|
24
24
|
const { command, args } = JSON.parse(text)
|
|
25
|
-
const data = command === 'events-head' ? { cursor: 0, accountId, taskProtocol: 'message-v1', persistentWatch: true, wildcardWatch: true }
|
|
25
|
+
const data = command === 'events-head' ? { cursor: 0, accountId, taskProtocol: 'message-v1', persistentWatch: true, wildcardWatch: true, taskVoice: true }
|
|
26
26
|
: command === 'task-watch' ? (watches.push(args), { watching: args.conversationId })
|
|
27
27
|
: command === 'events-check' ? { events: events.filter(e => args.ids.includes(e.id)) }
|
|
28
|
+
: command === 'task-document' ? {name:'shop.txt',data:Buffer.from('Shop opens at 7am.').toString('base64'),sha256:createHash('sha256').update('Shop opens at 7am.').digest('hex')}
|
|
28
29
|
: command === 'task-send' ? (sends.push(args), { ...args, state: uncertain ? 'uncertain' : 'accepted', receiptId: 'provider-1' }) : {}
|
|
29
30
|
res.end(JSON.stringify({ ok: true, data }))
|
|
30
31
|
})
|
|
@@ -35,7 +36,7 @@ async function fixture(t: test.TestContext, channelOwner = false) {
|
|
|
35
36
|
await runs.create({ id: 'owner', ownerId: ownerId(owner), ownerEpoch: ownerEpoch(owner), texts: ['Own this conversation'] })
|
|
36
37
|
await runs.patch('owner', { status: 'running' })
|
|
37
38
|
} else await ownerRun(dir, 'owner')
|
|
38
|
-
const control = new ControlStore(dir, 900000), sources = new EventSources(dir), runs = new RunStore(dir), tasks = new Tasks(dir)
|
|
39
|
+
const control = new ControlStore(dir, 900000), sources = new EventSources(dir), runs = new RunStore(dir), tasks = new Tasks(dir,speech)
|
|
39
40
|
await sources.register('generic', socket, (await control.status()).owner!)
|
|
40
41
|
t.after(async () => { server.closeAllConnections(); await new Promise<void>(r => server.close(() => r())); await rm(dir, { recursive: true, force: true }) })
|
|
41
42
|
async function proposal() {
|
|
@@ -247,7 +248,7 @@ test('approved initial task crosses the real host file client and uses a fresh r
|
|
|
247
248
|
const { isHostRunId } = await import('../src/host-executor-protocol.js')
|
|
248
249
|
const f = await fixture(t), { run } = await f.activate()
|
|
249
250
|
assert.ok(isHostRunId(run.id))
|
|
250
|
-
await writeFile(join(f.dir, 'codex'), `#!${process.execPath}\nif(process.argv[2]==='--version')console.log('codex-cli 0.160.
|
|
251
|
+
await writeFile(join(f.dir, 'codex'), `#!${process.execPath}\nif(process.argv[2]==='--version')console.log('codex-cli 0.160.1');else if(process.argv[2]==='debug')console.log(JSON.stringify({models:[{slug:'fixture',tool_mode:'code_mode_only',apply_patch_tool_type:'freeform',multi_agent_version:'v2'}]}));else console.log(JSON.stringify({cwd:process.cwd(),args:process.argv.slice(2),control:process.env.EZ_CONTROL_DIR}));`, { mode: 0o700 })
|
|
251
252
|
const priorPath = process.env.PATH
|
|
252
253
|
process.env.PATH = `${f.dir}:${priorPath}`
|
|
253
254
|
const abort = new AbortController(), host = serveHostExecutor({ cli: 'codex', agents: [{ name: 'test', workspace: f.dir, controlDir: f.dir, binDir: f.dir }] }, abort.signal)
|
|
@@ -341,3 +342,95 @@ test('public grants enforce persisted hourly and cumulative admission budgets',a
|
|
|
341
342
|
assert.equal(await f.tasks.admitPublic(proposal.id),'revoked')
|
|
342
343
|
assert.equal((await f.tasks.get(proposal.id))?.state,'revoked')
|
|
343
344
|
})
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
test('voice reuses task send authority and receipt keys, rechecks the account after synthesis, and never falls back to text', async t => {
|
|
348
|
+
let generated = 0, changed = false
|
|
349
|
+
let changeAccount: (value:string) => void
|
|
350
|
+
const f = await fixture(t,false,async text => {
|
|
351
|
+
generated++; assert.equal(text, 'Spoken fixture')
|
|
352
|
+
if(changed)changeAccount('replaced')
|
|
353
|
+
return {buffer:Buffer.from('OggSfixture OpusHead'),mimeType:'audio/ogg'}
|
|
354
|
+
})
|
|
355
|
+
changeAccount = f.account
|
|
356
|
+
const {run} = await f.activate()
|
|
357
|
+
const sent = await f.tasks.workerCall(run.id,'send',{text:'Spoken fixture',key:'spoken',voice:true})
|
|
358
|
+
assert.ok('state' in sent);assert.equal(sent.state,'accepted');assert.equal(generated,1);assert.equal(f.sends.length,1)
|
|
359
|
+
assert.equal(Buffer.from(f.sends[0].audio.data,'base64').toString(),'OggSfixture OpusHead')
|
|
360
|
+
await f.tasks.workerCall(run.id,'send',{text:'Spoken fixture',key:'spoken',voice:true})
|
|
361
|
+
assert.equal(generated,1);assert.equal(f.sends.length,1)
|
|
362
|
+
await assert.rejects(f.tasks.workerCall(run.id,'send',{text:'Spoken fixture',key:'spoken'}), /different text/)
|
|
363
|
+
changed=true
|
|
364
|
+
assert.equal((await f.tasks.workerCall(run.id,'send',{text:'Spoken fixture',key:'changed',voice:true}) as {state:string}).state,'uncertain')
|
|
365
|
+
assert.equal(f.sends.length,1)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
test('voice generation failure is a definite non-send: no text, no stored operation, same key retryable', async t => {
|
|
369
|
+
let generated=0
|
|
370
|
+
const f=await fixture(t,false,async () => { if(++generated===1)throw new Error('provider failed'); return {buffer:Buffer.from('OggSfixture OpusHead'),mimeType:'audio/ogg'} })
|
|
371
|
+
const {run, taskId}=await f.activate()
|
|
372
|
+
await assert.rejects(f.tasks.workerCall(run.id,'send',{text:'Spoken fixture',key:'failed',voice:true}),/provider failed/)
|
|
373
|
+
assert.equal(f.sends.length,0);assert.equal(Object.hasOwn((await f.tasks.get(taskId))!.operations,'failed'),false)
|
|
374
|
+
assert.equal((await f.tasks.workerCall(run.id,'send',{text:'Spoken fixture',key:'failed',voice:true}) as {state:string}).state,'accepted')
|
|
375
|
+
assert.equal(generated,2);assert.equal(f.sends.length,1)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
test('a cancelled worker cannot dispatch a voice generated after stop', async t => {
|
|
380
|
+
let stop: () => Promise<unknown>
|
|
381
|
+
const f=await fixture(t,false,async () => { await stop(); return {buffer:Buffer.from('OggSfixture OpusHead'),mimeType:'audio/ogg'} })
|
|
382
|
+
const {run}=await f.activate();stop=()=>f.runs.patch(run.id,{status:'cancelled'})
|
|
383
|
+
assert.equal((await f.tasks.workerCall(run.id,'send',{text:'Spoken fixture',key:'stopped',voice:true}) as {state:string}).state,'uncertain')
|
|
384
|
+
assert.equal(f.sends.length,0)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
test('incoming documents use shared staging and never expose another event or owner path', async t => {
|
|
388
|
+
const f=await fixture(t), {taskId}=await f.activate(), task=(await f.tasks.get(taskId))!
|
|
389
|
+
const row={id:'1',conversationId:'contact-a',receivedAt:Date.now(),text:'Document attached'};f.rows([row])
|
|
390
|
+
const run=await f.runs.create({id:'event_document',taskId,chatId:101,telegramUserId:101,texts:[],external:{sourceId:'generic',bindingId:task.bindingId,eventIds:['1']}})
|
|
391
|
+
await f.runs.patch(run.id,{status:'running'})
|
|
392
|
+
const document:any=await f.tasks.workerCall(run.id,'read_attachment',{incomingId:'1'})
|
|
393
|
+
assert.equal(document.text,'Shop opens at 7am.');assert.equal(document.name,'shop.txt');assert.equal(document.type,'text')
|
|
394
|
+
await assert.rejects(f.tasks.workerCall(run.id,'read_attachment',{incomingId:'2'}),/outside/)
|
|
395
|
+
await assert.rejects(f.tasks.workerCall(run.id,'read_attachment',{incomingId:'/etc/passwd'}),/outside/)
|
|
396
|
+
f.rows([{...row,conversationId:'contact-b'}]);await assert.rejects(f.tasks.workerCall(run.id,'read_attachment',{incomingId:'1'}),/correspondence/)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('explicit application launch continues restricted task; cross-task, owner mode, revocation and replay changes fail closed',async t=>{
|
|
400
|
+
const {ApplicationBindings,ApplicationChannel}=await import('../src/application-channel.js')
|
|
401
|
+
const {initialPreset}=await import('../src/ai.js')
|
|
402
|
+
const f=await fixture(t), {taskId,run}=await f.activate(), owner=(await f.control.status()).owner!
|
|
403
|
+
const bindings=new ApplicationBindings(f.dir), binding=(await bindings.register('browser','x'.repeat(48),owner))!
|
|
404
|
+
const token='a'.repeat(64),ticketHash=createHash('sha256').update(token).digest('hex')
|
|
405
|
+
await assert.rejects(f.tasks.workerCall(run.id,'application_launch',{application:'browser',ticketHash}),/not authorized/)
|
|
406
|
+
await bindings.taskLaunch('browser',{command:'fixture',args:['launch','--task'],tasks:[taskId]})
|
|
407
|
+
const grant=await f.tasks.workerCall(run.id,'application_launch',{application:'browser',ticketHash}) as {taskId:string;expiresAt:number}
|
|
408
|
+
assert.equal(grant.taskId,taskId)
|
|
409
|
+
assert(!(await readFile(join(f.dir,'task-applications',ticketHash+'.json'),'utf8')).includes(token))
|
|
410
|
+
const admission=await f.tasks.authorizeApplicationLaunch(run.id,ticketHash)
|
|
411
|
+
assert.deepEqual(admission.launch,{command:'fixture',args:['launch','--task'],tasks:[taskId]})
|
|
412
|
+
await assert.rejects(f.tasks.authorizeApplicationLaunch('owner',ticketHash))
|
|
413
|
+
const app=new ApplicationChannel({controlDir:f.dir,initial:initialPreset('codex'),wake(){},async cancel(){}})
|
|
414
|
+
const input={requestId:'voice-test',scope:'task:'+taskId,text:'When is the booking?',taskToken:token}
|
|
415
|
+
await assert.rejects(app.submit(binding.bindingId,{...input,followOwner:true}),/Invalid application task/)
|
|
416
|
+
await assert.rejects(app.submit(binding.bindingId,{...input,scope:'task:other'}),/scope/)
|
|
417
|
+
const delegated=await app.submit(binding.bindingId,input)
|
|
418
|
+
assert.equal(delegated.taskId,taskId);assert.equal(delegated.version,2)
|
|
419
|
+
assert.equal((await app.submit(binding.bindingId,input)).id,delegated.id)
|
|
420
|
+
await assert.rejects(app.submit(binding.bindingId,{...input,text:'Changed request'}),/conflicts/)
|
|
421
|
+
await f.runs.patch(delegated.id,{status:'running'})
|
|
422
|
+
const context=await f.tasks.workerCall(delegated.id,'context',{}) as {notes:string[];request:string;context:string}
|
|
423
|
+
assert.match(context.context,/Two people/);assert.equal(context.request,input.text)
|
|
424
|
+
await f.tasks.workerCall(delegated.id,'note',{text:'Discussion-only voice result'})
|
|
425
|
+
const reply=await f.tasks.workerCall(delegated.id,'send',{key:'answer',text:'7pm'}) as {receipt:{queued:string}}
|
|
426
|
+
assert.equal(f.sends.length,0)
|
|
427
|
+
const outbox=(await f.runs.pendingOutbox()).find(x=>x.id===reply.receipt.queued)!
|
|
428
|
+
await app.deliver(delegated,outbox)
|
|
429
|
+
assert.deepEqual((await app.snapshot(binding.bindingId,delegated.id)).messages.map(m=>m.text),['7pm'])
|
|
430
|
+
const other=(await bindings.register('other','y'.repeat(48),owner))!
|
|
431
|
+
await bindings.taskLaunch('other',{command:'fixture',args:['launch'],tasks:'all'})
|
|
432
|
+
await assert.rejects(app.submit(other.bindingId,{...input,requestId:'other'}),/revoked/)
|
|
433
|
+
await bindings.taskLaunch('browser',null)
|
|
434
|
+
await assert.rejects(f.tasks.authorize(delegated),/revoked/)
|
|
435
|
+
await assert.rejects(app.snapshot(binding.bindingId,delegated.id),/revoked/)
|
|
436
|
+
})
|
package/test/updates.test.mjs
CHANGED
|
@@ -114,13 +114,16 @@ test('root preparation leaves private plugin updates readable by the installer',
|
|
|
114
114
|
await exec('setpriv',['--reuid=20001','--regid=20002','--clear-groups','--no-new-privs',process.execPath,'-e',script]);
|
|
115
115
|
});
|
|
116
116
|
function runtime(f,{fail,stopped=false,buildArgs={CODEX_CLI_VERSION:'0.156.1'},latest='0.160.0'}={}) {
|
|
117
|
-
const calls=[];let failed=false;
|
|
117
|
+
const calls=[];let failed=false,brokerImage='sha256:'+'a'.repeat(64);
|
|
118
118
|
const execute=async(command,args,opts)=>{
|
|
119
119
|
calls.push([command,...args]);if(args.includes('view'))return JSON.stringify(latest);if(fail&&!failed&&fail(command,args)){failed=true;throw Error('Synthetic failure');}
|
|
120
120
|
if(args.at(-1)==='--version')return '10.30.3';
|
|
121
121
|
if(args.includes('config'))return JSON.stringify({services:{relay:{build:{args:buildArgs}}}});
|
|
122
|
-
if(args.includes('ps'))return stopped?'':'
|
|
123
|
-
if(args
|
|
122
|
+
if(args.includes('ps'))return stopped?'':'a'.repeat(64);
|
|
123
|
+
if(args.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED'))) {const [hostSha256,registrySha256,configSha256]=args.slice(-3);return JSON.stringify({version:1,ready:true,hostSha256,registrySha256,configSha256,plugins:[]});}
|
|
124
|
+
if(args[0]==='image'&&args[1]==='inspect')return 'sha256:'+'b'.repeat(64);
|
|
125
|
+
if(args.includes('--force-recreate'))brokerImage=(await fs.readFile(args[args.lastIndexOf('--env-file')+1],'utf8')).trim().split('=')[1];
|
|
126
|
+
if(args[0]==='inspect')return brokerImage;
|
|
124
127
|
if(args[0]==='volume'&&args[1]==='ls')return 'existing';
|
|
125
128
|
if(args[0]==='volume')return JSON.stringify([{Name:args[2]}]);
|
|
126
129
|
if(args[0]==='run')await fs.writeFile(opts.outputFile,'retained-private-state',{mode:0o600});
|
|
@@ -384,22 +387,55 @@ test('plugin updates register additive command routes on an unchanged deployment
|
|
|
384
387
|
const dispatch=await prepareCommand(f.home,'query',[]);assert.deepEqual(dispatch.argv.slice(-4),['node','sample','/app/bin/example.mjs','query']);
|
|
385
388
|
assert(r.calls.some(call=>call.includes('up')));
|
|
386
389
|
});
|
|
387
|
-
test('isolated plugin replacement
|
|
390
|
+
test('isolated plugin replacement recreates only the pinned broker and verifies activation and rollback',async t=>{
|
|
388
391
|
const f=await fixture(t,'plugin');
|
|
389
392
|
await fs.appendFile(path.join(f.config.deploymentDir,'docker.env'),'EZ_EXECUTOR_TRANSPORT=local\n');
|
|
390
393
|
const job=await queued(f),r=runtime(f),execute=r.execute;
|
|
391
394
|
let observed;
|
|
392
395
|
r.execute=async(command,args,options)=>{
|
|
393
|
-
if(args.includes('
|
|
396
|
+
if(args.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED'))) observed=(await read(path.join(f.home,'registry.json'))).plugins.sample.manifest.version;
|
|
397
|
+
if(args.includes('--force-recreate')) {
|
|
398
|
+
assert.equal(args.at(-1),'plugin-broker');
|
|
399
|
+
for(const flag of ['--no-deps','--no-build','--wait'])assert(args.includes(flag));
|
|
400
|
+
assert.equal(args[args.indexOf('--pull')+1],'never');
|
|
401
|
+
assert.equal(await fs.readFile(args[args.lastIndexOf('--env-file')+1],'utf8'),'EZ_RELAY_IMAGE=sha256:'+ 'a'.repeat(64)+'\n');
|
|
402
|
+
}
|
|
394
403
|
return execute(command,args,options);
|
|
395
404
|
};
|
|
396
405
|
assert.equal((await perform(f.home,job,r)).status,'completed');
|
|
397
|
-
assert.equal(observed,'0.1.1');
|
|
398
|
-
assert.equal(r.calls.filter(call=>call.includes('restart')).length,1);
|
|
406
|
+
assert.equal(observed,'0.1.1');assert.equal(job.brokerReadiness.ready,true);
|
|
399
407
|
const interrupted=await read(path.join(jobPath(f.home,job.id),'job.json'));interrupted.status='applying';
|
|
400
408
|
assert.equal((await perform(f.home,interrupted,r)).status,'rolled-back');
|
|
401
|
-
assert.equal(observed,'0.1.0');
|
|
402
|
-
assert.equal(r.calls.filter(call=>call.includes('
|
|
409
|
+
assert.equal(observed,'0.1.0');assert.equal(interrupted.rollbackBrokerReadiness.ready,true);
|
|
410
|
+
assert.equal(r.calls.filter(call=>call.includes('--force-recreate')).length,2);
|
|
411
|
+
assert(!r.calls.some(call=>call.includes('restart')||call[0]==='stopHost'));
|
|
412
|
+
});
|
|
413
|
+
for(const boundary of ['recreate','probe','readback'])test(`isolated broker ${boundary} failure cannot complete an update`,async t=>{
|
|
414
|
+
const f=await fixture(t,'plugin');await fs.appendFile(path.join(f.config.deploymentDir,'docker.env'),'EZ_EXECUTOR_TRANSPORT=local\n');
|
|
415
|
+
const job=await queued(f),r=runtime(f),base=r.execute;
|
|
416
|
+
r.execute=async(c,a,o)=>{
|
|
417
|
+
if(boundary==='recreate'&&a.includes('--force-recreate')||boundary==='probe'&&a.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED')))throw Error('Synthetic broker failure');
|
|
418
|
+
if(boundary==='readback'&&a.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED')))return JSON.stringify({version:1,ready:true,hostSha256:'0'.repeat(64)});
|
|
419
|
+
return base(c,a,o);
|
|
420
|
+
};
|
|
421
|
+
const result=await perform(f.home,job,r);
|
|
422
|
+
assert.equal(result.status,'recovery-required');assert(result.error);assert(result.recoveryError);
|
|
423
|
+
assert.equal((await read(path.join(f.home,'registry.json'))).plugins.sample.manifest.version,'0.1.0');
|
|
424
|
+
});
|
|
425
|
+
test('main isolated activation and retained-relay rollback also refresh the broker',async t=>{
|
|
426
|
+
const f=await fixture(t);await fs.appendFile(path.join(f.config.deploymentDir,'docker.env'),'EZ_EXECUTOR_TRANSPORT=local\n');
|
|
427
|
+
const job=await queued(f),r=runtime(f);
|
|
428
|
+
assert.equal((await perform(f.home,job,r)).status,'completed');assert.equal(job.brokerReadiness.ready,true);
|
|
429
|
+
const base=r.execute;r.execute=async(c,a,o)=>a[0]==='inspect'&&a.includes('{{json .}}')?JSON.stringify({Image:'sha256:'+'a'.repeat(64),State:{Running:true,Health:{Status:'healthy'}}}):base(c,a,o);
|
|
430
|
+
const interrupted=await read(path.join(jobPath(f.home,job.id),'job.json'));interrupted.status='applying';
|
|
431
|
+
assert.equal((await perform(f.home,interrupted,r)).status,'rolled-back');
|
|
432
|
+
assert.equal(interrupted.rollbackBrokerReadiness.ready,true);
|
|
433
|
+
assert.equal(r.calls.filter(call=>call.includes('--force-recreate')).length,2);
|
|
434
|
+
assert.equal(r.calls.filter(call=>call.includes('up')&&call.at(-1)==='relay').length,1);
|
|
435
|
+
});
|
|
436
|
+
test('host-capable updates never inspect, recreate or probe an isolated broker',async t=>{
|
|
437
|
+
const f=await fixture(t,'plugin'),r=runtime(f);assert.equal((await perform(f.home,await queued(f),r)).status,'completed');
|
|
438
|
+
assert(!r.calls.some(call=>call.includes('plugin-broker')||call.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED'))));
|
|
403
439
|
});
|
|
404
440
|
test('plugin update preserves a created but never running service as stopped',async t=>{
|
|
405
441
|
const f=await fixture(t,'plugin'),job=await queued(f),r=runtime(f),execute=r.execute;
|