@jc_stack/ez-agents 0.1.0-beta.36 → 0.1.0-beta.38
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 +2 -2
- package/CHANGELOG.md +37 -0
- package/Dockerfile +13 -2
- package/README.md +0 -1
- package/THIRD_PARTY_NOTICES.md +7 -4
- package/compose.yaml +1 -0
- package/docs/application-channel.md +9 -2
- package/docs/architecture/ai-selection.md +10 -2
- package/docs/docker-runtime.md +19 -0
- package/docs/upgrades.md +1 -1
- package/package.json +1 -1
- package/src/ai-cli.ts +1 -1
- package/src/ai.ts +132 -7
- package/src/application-channel.ts +1 -1
- package/src/config.ts +1 -1
- package/src/control-state.ts +1 -4
- package/src/executor.ts +62 -22
- package/src/host-executor.ts +50 -18
- package/src/index.ts +3 -2
- package/src/menu.ts +2 -2
- package/src/message-history.ts +19 -1
- package/src/message.ts +2 -2
- package/src/plugins/manager.mjs +3 -4
- package/src/setup.ts +3 -2
- package/test/ai.test.ts +185 -1
- package/test/config.test.ts +2 -2
- package/test/conversations.test.ts +2 -8
- package/test/engine-handoff.test.ts +3 -4
- package/test/executor.test.ts +112 -12
- package/test/host-executor.test.ts +168 -12
- package/test/message-history.test.ts +47 -0
- package/test/model-policy.test.ts +2 -2
- package/test/plugin-manager.test.mjs +17 -0
- package/test/setup.test.ts +7 -7
package/test/executor.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdtemp, mkdir, rm, writeFile, readFile } from 'node:fs/promises'
|
|
|
6
6
|
import { spawn } from 'node:child_process'
|
|
7
7
|
import { tmpdir } from 'node:os'
|
|
8
8
|
import path from 'node:path'
|
|
9
|
-
import { EXECUTOR_REGISTRY,
|
|
9
|
+
import { EXECUTOR_REGISTRY, executorEnvironment, grokInvocation, grokJobEnv, opencodeDataHome, opencodeInvocation, piAgentDir, resolveExecutor, resolveHostCommand, startExecutorJob, terminateJob, validateCodexProvider } from '../src/executor.js'
|
|
10
10
|
import { requireOwnerExecution } from '../src/execution-authority.js'
|
|
11
11
|
import { splitTelegramText } from '../src/reply.js'
|
|
12
12
|
import { matchingProcessIds, processSnapshot } from '../src/process-tree.js'
|
|
@@ -97,14 +97,6 @@ test('the Grok invocation is headless, workspace-scoped, and token-free', () =>
|
|
|
97
97
|
assert.equal(invocation.args.includes('TELEGRAM_BOT_TOKEN'), false)
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
test('the antigravity invocation is headless, skips permissions, and uses print mode', () => {
|
|
101
|
-
const invocation = antigravityInvocation('test prompt')
|
|
102
|
-
assert.equal(invocation.command, 'agy')
|
|
103
|
-
assert.deepEqual(invocation.args, [
|
|
104
|
-
'--dangerously-skip-permissions', '--print=test prompt',
|
|
105
|
-
])
|
|
106
|
-
})
|
|
107
|
-
|
|
108
100
|
test('the opencode invocation is headless, auto-approves, and sets model and workspace', () => {
|
|
109
101
|
const invocation = opencodeInvocation('test prompt', { workspace: '/tmp/test-ws' })
|
|
110
102
|
assert.equal(invocation.command, 'opencode')
|
|
@@ -119,15 +111,15 @@ test('the opencode invocation is headless, auto-approves, and sets model and wor
|
|
|
119
111
|
})
|
|
120
112
|
|
|
121
113
|
test('resolveExecutor correctly maps keys and aliases', () => {
|
|
122
|
-
assert.equal(resolveExecutor('agy').command, 'agy')
|
|
123
|
-
assert.equal(resolveExecutor('antigravity').command, 'agy')
|
|
124
114
|
assert.equal(resolveExecutor('claude').command, 'claude')
|
|
125
115
|
assert.equal(resolveExecutor('grok').command, 'grok')
|
|
126
116
|
assert.equal(resolveExecutor('opencode').command, 'opencode')
|
|
127
117
|
assert.equal(resolveExecutor('oc').command, 'opencode')
|
|
118
|
+
for (const cli of ['unreal-agent', 'antigravity', 'agy'])
|
|
119
|
+
assert.throws(() => resolveExecutor(cli), /Unsupported executor CLI/)
|
|
128
120
|
assert.equal(resolveExecutor('codex-gui').name, 'codex-gui')
|
|
129
121
|
assert.notEqual(resolveExecutor('codex-gui').name, resolveExecutor('codex').name)
|
|
130
|
-
assert.equal(resolveExecutor(undefined).command, '
|
|
122
|
+
assert.equal(resolveExecutor(undefined).command, 'codex')
|
|
131
123
|
assert.throws(() => resolveExecutor('claude-spark'), /Unsupported executor CLI "claude-spark"/)
|
|
132
124
|
assert.throws(() => resolveExecutor('spark'), /Unsupported executor CLI "spark"/)
|
|
133
125
|
assert.throws(() => resolveExecutor('nonexistent'), /Unsupported executor CLI "nonexistent"/)
|
|
@@ -177,6 +169,114 @@ test('Codex custom providers use declared runtime state and an environment key r
|
|
|
177
169
|
assert.throws(()=>validateCodexProvider({...provider,models:[]}),/provider models/)
|
|
178
170
|
})
|
|
179
171
|
|
|
172
|
+
test('opencode jobs use the agent-bound data home only when its auth binding exists', async t => {
|
|
173
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-opencode-auth-'));t.after(()=>rm(root,{recursive:true,force:true}))
|
|
174
|
+
const bin=path.join(root,'bin');await mkdir(bin)
|
|
175
|
+
await writeFile(path.join(bin,'opencode'),`#!${process.execPath}\nconst fs=require('fs');fs.writeFileSync(${JSON.stringify(path.join(root,'observed.json'))},JSON.stringify({args:process.argv.slice(2),xdg:process.env.XDG_DATA_HOME,telegram:process.env.TELEGRAM_BOT_TOKEN}));`,{mode:0o755})
|
|
176
|
+
assert.equal(opencodeDataHome(root),undefined)
|
|
177
|
+
await mkdir(path.join(root,'cli','opencode'),{recursive:true})
|
|
178
|
+
assert.equal(opencodeDataHome(root),undefined)
|
|
179
|
+
await writeFile(path.join(root,'cli','opencode','auth.json'),'{}',{mode:0o600})
|
|
180
|
+
assert.equal(opencodeDataHome(root),path.join(root,'cli'))
|
|
181
|
+
const prior={path:process.env.PATH,telegram:process.env.TELEGRAM_BOT_TOKEN,transport:process.env.EZ_EXECUTOR_TRANSPORT}
|
|
182
|
+
const launch=async(runId:string)=>{
|
|
183
|
+
await ownerRun(root,runId)
|
|
184
|
+
process.env.PATH=bin+path.delimiter+prior.path;process.env.TELEGRAM_BOT_TOKEN='relay-secret';delete process.env.EZ_EXECUTOR_TRANSPORT
|
|
185
|
+
const job=await startExecutorJob(['hello'],{workspace:root,controlDir:root,binDir:bin,runId,timeoutMs:0,cli:'opencode',model:'opencode-go/muse-spark-1.3-contributor',effort:'xhigh'})
|
|
186
|
+
assert.equal(await new Promise(resolve=>job.child.once('close',resolve)),0);await job.cleanup()
|
|
187
|
+
return JSON.parse(await readFile(path.join(root,'observed.json'),'utf8'))
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const bound=await launch('r_ocbound')
|
|
191
|
+
assert.equal(bound.xdg,path.join(root,'cli'))
|
|
192
|
+
assert.equal(bound.telegram,undefined)
|
|
193
|
+
assert.equal(bound.args[bound.args.indexOf('-m')+1],'opencode-go/muse-spark-1.3-contributor')
|
|
194
|
+
assert.equal(bound.args[bound.args.indexOf('--variant')+1],'xhigh')
|
|
195
|
+
await rm(path.join(root,'cli','opencode','auth.json'))
|
|
196
|
+
const free=await launch('r_ocfree')
|
|
197
|
+
assert.equal(free.xdg,undefined)
|
|
198
|
+
assert.equal(free.telegram,undefined)
|
|
199
|
+
} finally {
|
|
200
|
+
if(prior.path===undefined)delete process.env.PATH;else process.env.PATH=prior.path
|
|
201
|
+
if(prior.telegram===undefined)delete process.env.TELEGRAM_BOT_TOKEN;else process.env.TELEGRAM_BOT_TOKEN=prior.telegram
|
|
202
|
+
if(prior.transport===undefined)delete process.env.EZ_EXECUTOR_TRANSPORT;else process.env.EZ_EXECUTOR_TRANSPORT=prior.transport
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('pi jobs use the agent-bound config dir only when its auth binding exists', async t => {
|
|
207
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-pi-auth-'));t.after(()=>rm(root,{recursive:true,force:true}))
|
|
208
|
+
const bin=path.join(root,'bin');await mkdir(bin)
|
|
209
|
+
await writeFile(path.join(bin,'pi'),`#!${process.execPath}\nconst fs=require('fs');fs.writeFileSync(${JSON.stringify(path.join(root,'observed.json'))},JSON.stringify({args:process.argv.slice(2),agentDir:process.env.PI_CODING_AGENT_DIR,telegram:process.env.TELEGRAM_BOT_TOKEN}));`,{mode:0o755})
|
|
210
|
+
assert.equal(piAgentDir(root),undefined)
|
|
211
|
+
await mkdir(path.join(root,'cli','pi','agent'),{recursive:true})
|
|
212
|
+
assert.equal(piAgentDir(root),undefined)
|
|
213
|
+
await writeFile(path.join(root,'cli','pi','agent','auth.json'),'{}',{mode:0o600})
|
|
214
|
+
assert.equal(piAgentDir(root),path.join(root,'cli','pi','agent'))
|
|
215
|
+
const prior={path:process.env.PATH,telegram:process.env.TELEGRAM_BOT_TOKEN,transport:process.env.EZ_EXECUTOR_TRANSPORT,agentDir:process.env.PI_CODING_AGENT_DIR}
|
|
216
|
+
const launch=async(runId:string)=>{
|
|
217
|
+
await ownerRun(root,runId)
|
|
218
|
+
process.env.PATH=bin+path.delimiter+prior.path;process.env.TELEGRAM_BOT_TOKEN='relay-secret';delete process.env.EZ_EXECUTOR_TRANSPORT;delete process.env.PI_CODING_AGENT_DIR
|
|
219
|
+
const job=await startExecutorJob(['hello'],{workspace:root,controlDir:root,binDir:bin,runId,timeoutMs:0,cli:'pi',model:'opencode-go/muse-spark-1.3-contributor',effort:'xhigh'})
|
|
220
|
+
assert.equal(await new Promise(resolve=>job.child.once('close',resolve)),0);await job.cleanup()
|
|
221
|
+
return JSON.parse(await readFile(path.join(root,'observed.json'),'utf8'))
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const bound=await launch('r_pibound')
|
|
225
|
+
assert.equal(bound.agentDir,path.join(root,'cli','pi','agent'))
|
|
226
|
+
assert.equal(bound.telegram,undefined)
|
|
227
|
+
assert.equal(bound.args[bound.args.indexOf('--model')+1],'opencode-go/muse-spark-1.3-contributor')
|
|
228
|
+
assert.equal(bound.args[bound.args.indexOf('--thinking')+1],'xhigh')
|
|
229
|
+
await rm(path.join(root,'cli','pi','agent','auth.json'))
|
|
230
|
+
const free=await launch('r_pifree')
|
|
231
|
+
assert.equal(free.agentDir,undefined)
|
|
232
|
+
assert.equal(free.telegram,undefined)
|
|
233
|
+
// Owner-provisioned delivery skill loads by path; absence keeps prior args.
|
|
234
|
+
assert.ok(!bound.args.includes('--skill'))
|
|
235
|
+
await mkdir(path.join(root,'cli','pi','skills','ez-delivery'),{recursive:true})
|
|
236
|
+
await writeFile(path.join(root,'cli','pi','skills','ez-delivery','SKILL.md'),'# delivery',{mode:0o644})
|
|
237
|
+
const skilled=await launch('r_piskilled')
|
|
238
|
+
assert.equal(skilled.args[skilled.args.indexOf('--skill')+1],path.join(root,'cli','pi','skills','ez-delivery'))
|
|
239
|
+
} finally {
|
|
240
|
+
if(prior.path===undefined)delete process.env.PATH;else process.env.PATH=prior.path
|
|
241
|
+
if(prior.telegram===undefined)delete process.env.TELEGRAM_BOT_TOKEN;else process.env.TELEGRAM_BOT_TOKEN=prior.telegram
|
|
242
|
+
if(prior.transport===undefined)delete process.env.EZ_EXECUTOR_TRANSPORT;else process.env.EZ_EXECUTOR_TRANSPORT=prior.transport
|
|
243
|
+
if(prior.agentDir===undefined)delete process.env.PI_CODING_AGENT_DIR;else process.env.PI_CODING_AGENT_DIR=prior.agentDir
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('the pi invocation pins control sessions, model, and thinking level', () => {
|
|
248
|
+
assert.equal(resolveExecutor('pi').command, 'pi')
|
|
249
|
+
const args = EXECUTOR_REGISTRY.pi.buildArgs(
|
|
250
|
+
{ workspace: '/agent/mind', controlDir: '/agent/control', sessionId: 'ez-session', isResume: true, model: 'opencode-go/muse-spark-1.3-contributor', effort: 'xhigh' },
|
|
251
|
+
'/prompt', 'review CAMT')
|
|
252
|
+
assert.deepEqual(args, ['-p', '--mode', 'json', '--session-dir', '/agent/control/cli/pi/sessions', '--session-id', 'ez-session',
|
|
253
|
+
'--model', 'opencode-go/muse-spark-1.3-contributor', '--thinking', 'xhigh', '--', 'review CAMT'])
|
|
254
|
+
const other = EXECUTOR_REGISTRY.pi.buildArgs({ workspace: '/agent/mind', controlDir: '/agent/control', sessionId: 'other-session', isResume: true }, '/prompt', 'other')
|
|
255
|
+
assert.equal(other[other.indexOf('--session-id') + 1], 'other-session')
|
|
256
|
+
assert.ok(!other.includes('--continue'))
|
|
257
|
+
assert.throws(() => EXECUTOR_REGISTRY.pi.buildArgs({ workspace: '/agent' }, '/prompt', 'hi'), /bound control directory/)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('pi jobs run with control-bound sessions and no secret leakage', async t => {
|
|
261
|
+
const root = await mkdtemp(path.join(tmpdir(), 'ez-pi-auth-')); t.after(() => rm(root, { recursive: true, force: true }))
|
|
262
|
+
const bin = path.join(root, 'bin'); await mkdir(bin)
|
|
263
|
+
await writeFile(path.join(bin, 'pi'), `#!${process.execPath}\nconst fs=require('fs');fs.writeFileSync(${JSON.stringify(path.join(root, 'observed.json'))},JSON.stringify({args:process.argv.slice(2),telegram:process.env.TELEGRAM_BOT_TOKEN}));`, { mode: 0o755 })
|
|
264
|
+
const prior = { path: process.env.PATH, telegram: process.env.TELEGRAM_BOT_TOKEN, transport: process.env.EZ_EXECUTOR_TRANSPORT }
|
|
265
|
+
try {
|
|
266
|
+
await ownerRun(root, 'r_pibound')
|
|
267
|
+
process.env.PATH = bin + path.delimiter + prior.path; process.env.TELEGRAM_BOT_TOKEN = 'relay-secret'; delete process.env.EZ_EXECUTOR_TRANSPORT
|
|
268
|
+
const job = await startExecutorJob(['review CAMT'], { workspace: root, controlDir: root, binDir: bin, runId: 'r_pibound', timeoutMs: 0, cli: 'pi', model: 'opencode-go/muse-spark-1.3-contributor', effort: 'xhigh' })
|
|
269
|
+
assert.equal(await new Promise(resolve => job.child.once('close', resolve)), 0); await job.cleanup()
|
|
270
|
+
const observed = JSON.parse(await readFile(path.join(root, 'observed.json'), 'utf8'))
|
|
271
|
+
assert.deepEqual(observed.args, ['-p', '--mode', 'json', '--session-dir', path.join(root, 'cli', 'pi', 'sessions'),
|
|
272
|
+
'--model', 'opencode-go/muse-spark-1.3-contributor', '--thinking', 'xhigh', '--', 'review CAMT'])
|
|
273
|
+
assert.equal(observed.telegram, undefined)
|
|
274
|
+
} finally {
|
|
275
|
+
if (prior.path === undefined) delete process.env.PATH; else process.env.PATH = prior.path
|
|
276
|
+
if (prior.telegram === undefined) delete process.env.TELEGRAM_BOT_TOKEN; else process.env.TELEGRAM_BOT_TOKEN = prior.telegram
|
|
277
|
+
if (prior.transport === undefined) delete process.env.EZ_EXECUTOR_TRANSPORT; else process.env.EZ_EXECUTOR_TRANSPORT = prior.transport
|
|
278
|
+
}
|
|
279
|
+
})
|
|
180
280
|
test('a configured Codex provider receives only its declared key and runtime selection', async t => {
|
|
181
281
|
const root=await mkdtemp(path.join(tmpdir(),'ez-codex-provider-'));t.after(()=>rm(root,{recursive:true,force:true}))
|
|
182
282
|
const bin=path.join(root,'bin');await mkdir(bin)
|
|
@@ -4,7 +4,7 @@ import test from 'node:test'
|
|
|
4
4
|
import { mkdtemp, mkdir, readFile, writeFile, rm, realpath, symlink } from 'node:fs/promises'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { tmpdir } from 'node:os'
|
|
7
|
-
import { serveHostExecutor } from '../src/host-executor.js'
|
|
7
|
+
import { serveHostExecutor, refreshAgentCatalog } from '../src/host-executor.js'
|
|
8
8
|
import { spawn } from 'node:child_process'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
import { isHostRunId } from '../src/host-executor-protocol.js'
|
|
@@ -21,8 +21,8 @@ test('host restart replaces a lock whose PID was reused by another process',asyn
|
|
|
21
21
|
try {
|
|
22
22
|
await mkdir(workspace);await mkdir(directory,{recursive:true})
|
|
23
23
|
await writeFile(path.join(directory,'worker.lock'),JSON.stringify({pid:process.pid,started:'reused-pid'}))
|
|
24
|
-
server=serveHostExecutor({cli:'grok',agents:[{name:'test',workspace,controlDir,binDir:root}]},abort.signal)
|
|
25
|
-
for(let n=0;n<
|
|
24
|
+
server=serveHostExecutor({cli:'grok',agents:[{name:'test',workspace,controlDir,binDir:root}]},abort.signal,undefined,async()=>[])
|
|
25
|
+
for(let n=0;n<300;n++){try{await readFile(path.join(directory,'heartbeat.json'));break}catch{await new Promise(r=>setTimeout(r,20))}}
|
|
26
26
|
await readFile(path.join(directory,'heartbeat.json'))
|
|
27
27
|
const lock=JSON.parse(await readFile(path.join(directory,'worker.lock'),'utf8'))
|
|
28
28
|
assert.equal(lock.pid,process.pid)
|
|
@@ -47,6 +47,16 @@ test('isolated installations cannot start host transport', async () => {
|
|
|
47
47
|
abort.abort()
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
test('host bindings reject invalid opencode provider scopes before touching state', async () => {
|
|
51
|
+
const abort=new AbortController()
|
|
52
|
+
for (const opencodeProviders of [['BAD NAME'], [], ['ok', 'ok'], new Array(17).fill('ok')]) {
|
|
53
|
+
await assert.rejects(serveHostExecutor({cli:'grok',agents:[{
|
|
54
|
+
name:'t',workspace:'/tmp/ez-scope-x',controlDir:'/tmp/ez-scope-y',binDir:'/tmp/ez-scope-z',opencodeProviders} as never]},abort.signal),
|
|
55
|
+
/one to sixteen unique provider IDs/)
|
|
56
|
+
}
|
|
57
|
+
abort.abort()
|
|
58
|
+
})
|
|
59
|
+
|
|
50
60
|
test('host restart clears dead native lease only after proving previous CLI stopped',async()=>{
|
|
51
61
|
const root=await mkdtemp(path.join(tmpdir(),'ez-native-recovery-'));
|
|
52
62
|
const workspace=path.join(root,'mind'),controlDir=path.join(root,'control'),toolsHome=path.join(root,'tools'),directory=path.join(controlDir,'host-executor');
|
|
@@ -62,8 +72,8 @@ test('host restart clears dead native lease only after proving previous CLI stop
|
|
|
62
72
|
await assert.rejects(serveHostExecutor(installation,abort.signal),/Previous host CLI is still running/);
|
|
63
73
|
await readFile(path.join(toolsHome,'workspace-writer.lock'));
|
|
64
74
|
await writeFile(path.join(directory,'r_old.process.json'),JSON.stringify({pid:deadPid}));
|
|
65
|
-
server=serveHostExecutor(installation,abort.signal);
|
|
66
|
-
for(let n=0;n<
|
|
75
|
+
server=serveHostExecutor(installation,abort.signal,undefined,async()=>[]);
|
|
76
|
+
for(let n=0;n<300;n++){try{await readFile(path.join(directory,'heartbeat.json'));break}catch{await new Promise(r=>setTimeout(r,20))}}
|
|
67
77
|
await readFile(path.join(directory,'heartbeat.json'));
|
|
68
78
|
await assert.rejects(readFile(path.join(toolsHome,'workspace-writer.lock')),{code:'ENOENT'});
|
|
69
79
|
}finally{abort.abort();await server;await rm(root,{recursive:true,force:true});}
|
|
@@ -93,6 +103,7 @@ test('host execution resolves a declared provider from the bound agent, not a la
|
|
|
93
103
|
await mkdir(fakeBin, {recursive: true})
|
|
94
104
|
await mkdir(path.join(controlDir, 'cli', 'codex'), {recursive: true})
|
|
95
105
|
await writeFile(path.join(fakeBin, 'codex'), '#!/bin/sh\nexit 0\n', {mode: 0o700})
|
|
106
|
+
await writeFile(path.join(fakeBin, 'opencode'), '#!/bin/sh\nexit 0\n', {mode: 0o700})
|
|
96
107
|
await writeFile(path.join(controlDir, 'cli', 'codex', 'models_cache.json'), JSON.stringify({models: [
|
|
97
108
|
{slug: provider.models[0], visibility: 'list', display_name: 'DeepSeek V4.1 Flash', supported_reasoning_levels: [{effort: 'max'}]},
|
|
98
109
|
]}))
|
|
@@ -107,6 +118,9 @@ test('host execution resolves a declared provider from the bound agent, not a la
|
|
|
107
118
|
try { await readFile(path.join(directory, 'heartbeat.json')); break }
|
|
108
119
|
catch { await new Promise(resolve => setTimeout(resolve, 20)) }
|
|
109
120
|
}
|
|
121
|
+
// The selected model was advertised at startup. A later catalog probe
|
|
122
|
+
// can fail while the client remains able to execute the selected model.
|
|
123
|
+
await rm(path.join(controlDir, 'cli', 'codex', 'models_cache.json'))
|
|
110
124
|
await writeFile(path.join(directory, 'r_provider.request.json'), JSON.stringify({
|
|
111
125
|
texts: ['hello'], options: {cli: 'codex', provider: 'openrouter', model: provider.models[0], effort: 'max'},
|
|
112
126
|
}))
|
|
@@ -156,6 +170,7 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
156
170
|
await writeFile(binary,`#!${process.execPath}\nif(process.env.EZ_RUN_ID==='r_hold')setInterval(()=>{},1000);console.log(JSON.stringify({cwd:process.cwd(),home:process.env.HOME,token:process.env.TELEGRAM_BOT_TOKEN,control:process.env.EZ_CONTROL_DIR,run:process.env.EZ_RUN_ID,repair:process.env.EZ_REPAIR_ENABLED,args:process.argv.slice(2)}));\n`,{mode:0o700})
|
|
157
171
|
await writeFile(path.join(root,'claude'),await readFile(binary),{mode:0o700})
|
|
158
172
|
await writeFile(path.join(root,'codex'),await readFile(binary),{mode:0o700})
|
|
173
|
+
await writeFile(path.join(root,'opencode'),'#!/bin/sh\nexit 0\n',{mode:0o700})
|
|
159
174
|
process.env.PATH=root+path.delimiter+oldPath
|
|
160
175
|
EXECUTOR_REGISTRY.grok.command=binary
|
|
161
176
|
EXECUTOR_REGISTRY.grok.buildArgs=(opts,file,prompt)=>[...EXECUTOR_REGISTRY.codex.buildArgs(opts,file,prompt).slice(0,-1),prompt]
|
|
@@ -173,7 +188,7 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
173
188
|
server=serveHostExecutor({cli:'grok',agents},abort.signal)
|
|
174
189
|
for(const agent of agents){
|
|
175
190
|
const dir=path.join(agent.controlDir,'host-executor')
|
|
176
|
-
for(let n=0;n<
|
|
191
|
+
for(let n=0;n<300;n++){try{await readFile(path.join(dir,'heartbeat.json'));break}catch{await new Promise(r=>setTimeout(r,20))}}
|
|
177
192
|
assert.equal(JSON.parse(await readFile(path.join(dir,'heartbeat.json'),'utf8')).version,packageVersion)
|
|
178
193
|
const release = await workspaceLease(agent.toolsHome)
|
|
179
194
|
await ownerRun(agent.controlDir, `r_${agent.name}`)
|
|
@@ -231,10 +246,14 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
231
246
|
switched.stdin.end(JSON.stringify({texts:['Explicit CLI change'],options:executionDefaults('claude',{cli:'claude',timeoutMs:5000,effort:undefined})}))
|
|
232
247
|
assert.equal(await new Promise(resolve=>switched.once('close',resolve)),0)
|
|
233
248
|
assert.ok(JSON.parse(switchedOutput).args.includes('--print'))
|
|
234
|
-
// The
|
|
249
|
+
// The host's published catalog can gain a model after its first probe.
|
|
235
250
|
const codexHome=path.join(agents[0].controlDir,'cli','codex')
|
|
236
251
|
await mkdir(codexHome,{recursive:true})
|
|
237
252
|
await writeFile(path.join(codexHome,'models_cache.json'),JSON.stringify({models:[{slug:'agent-only-fixture',visibility:'list',display_name:'Agent model',supported_reasoning_levels:[]}]}))
|
|
253
|
+
const catalogFile=path.join(agents[0].controlDir,'host-executor','models.json')
|
|
254
|
+
const advertised=JSON.parse(await readFile(catalogFile,'utf8'))
|
|
255
|
+
advertised.push({cli:'codex',model:'agent-only-fixture',name:'Agent model',efforts:[]})
|
|
256
|
+
await writeFile(catalogFile,JSON.stringify(advertised))
|
|
238
257
|
await ownerRun(agents[0].controlDir,'tg_6293307')
|
|
239
258
|
const bound=spawn(process.execPath,['--import',fileURLToPath(new URL('../node_modules/tsx/dist/loader.mjs',import.meta.url)),fileURLToPath(new URL('../src/host-executor-client.ts',import.meta.url)),agents[0].controlDir,'tg_6293307'],{stdio:['pipe','pipe','pipe']})
|
|
240
259
|
let boundOutput='',boundError=''
|
|
@@ -344,9 +363,146 @@ test('client cancels on stale or invalid heartbeat instead of waiting indefinite
|
|
|
344
363
|
|
|
345
364
|
|
|
346
365
|
test('host rejects a plugin registry bound to another workspace',async()=>{
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
366
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-host-plugin-binding-'))
|
|
367
|
+
try {
|
|
368
|
+
await writeFile(path.join(root,'config.json'),JSON.stringify({schemaVersion:1,workspace:'/another-agent'}))
|
|
369
|
+
await assert.rejects(serveHostExecutor({cli:'codex',agents:[{name:'test',workspace:root,controlDir:root,binDir:root,toolsHome:root}]},new AbortController().signal),/another workspace/)
|
|
370
|
+
} finally {await rm(root,{recursive:true,force:true})}
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('refresh blip throws so the caller keeps last-good models instead of crashing',async()=>{
|
|
374
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-host-refresh-blip-'))
|
|
375
|
+
const workspace=path.join(root,'mind'),controlDir=path.join(root,'control'),directory=path.join(controlDir,'host-executor')
|
|
376
|
+
try {
|
|
377
|
+
await mkdir(workspace,{recursive:true});await mkdir(directory,{recursive:true})
|
|
378
|
+
const agent={name:'test',workspace,controlDir,binDir:root}
|
|
379
|
+
const lastGood=[{cli:'codex',model:'cached-model',name:'Cached',efforts:[]}]
|
|
380
|
+
await writeFile(path.join(directory,'models.json'),JSON.stringify(lastGood))
|
|
381
|
+
const seen=new Map()
|
|
382
|
+
await assert.rejects(refreshAgentCatalog(agent,async()=>{throw new Error('transient catalog blip')},seen),/transient catalog blip/)
|
|
383
|
+
assert.deepEqual(JSON.parse(await readFile(path.join(directory,'models.json'),'utf8')),lastGood)
|
|
384
|
+
assert.equal(seen.has(agent),false)
|
|
385
|
+
} finally {await rm(root,{recursive:true,force:true})}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('empty refresh preserves the last-good catalog and a later nonempty refresh replaces it',async()=>{
|
|
389
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-host-empty-refresh-'))
|
|
390
|
+
const directory=path.join(root,'host-executor')
|
|
391
|
+
try {
|
|
392
|
+
await mkdir(directory)
|
|
393
|
+
const agent={name:'test',workspace:root,controlDir:root,binDir:root}
|
|
394
|
+
const lastGood=[{cli:'codex',model:'cached-model',name:'Cached',efforts:[]}]
|
|
395
|
+
const fresh=[{cli:'codex',model:'new-model',name:'New',efforts:[]}]
|
|
396
|
+
await writeFile(path.join(directory,'models.json'),JSON.stringify(lastGood))
|
|
397
|
+
const seen=new Map([[agent,JSON.stringify(lastGood)]])
|
|
398
|
+
await refreshAgentCatalog(agent,async()=>[],seen)
|
|
399
|
+
assert.deepEqual(JSON.parse(await readFile(path.join(directory,'models.json'),'utf8')),lastGood)
|
|
400
|
+
assert.equal(seen.get(agent),JSON.stringify(lastGood))
|
|
401
|
+
await refreshAgentCatalog(agent,async()=>fresh,seen)
|
|
402
|
+
assert.deepEqual(JSON.parse(await readFile(path.join(directory,'models.json'),'utf8')),fresh)
|
|
403
|
+
assert.equal(seen.get(agent),JSON.stringify(fresh))
|
|
404
|
+
} finally {await rm(root,{recursive:true,force:true})}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('slow catalog refresh keeps heartbeats live and active client completes without cancellation', {timeout:70000}, async()=>{
|
|
408
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-host-slow-refresh-'))
|
|
409
|
+
const directory=path.join(root,'host-executor')
|
|
410
|
+
const abort=new AbortController()
|
|
411
|
+
let server:Promise<void>|undefined
|
|
412
|
+
let client:ReturnType<typeof spawn>|undefined
|
|
413
|
+
let closed:Promise<number|null>|undefined
|
|
414
|
+
let releaseRefresh!:()=>void
|
|
415
|
+
let startedRefresh!:()=>void
|
|
416
|
+
const blocked=new Promise<void>(resolve=>{releaseRefresh=resolve})
|
|
417
|
+
const refreshing=new Promise<void>(resolve=>{startedRefresh=resolve})
|
|
418
|
+
let calls=0
|
|
419
|
+
let finishRun: (()=>void) | undefined
|
|
420
|
+
try {
|
|
421
|
+
await mkdir(directory)
|
|
422
|
+
await ownerRun(root,'r_slow_catalog')
|
|
423
|
+
server=serveHostExecutor({cli:'grok',agents:[{name:'test',workspace:root,controlDir:root,binDir:root}]},abort.signal,async()=>{
|
|
424
|
+
const child=spawn(process.execPath,['-e','process.stdin.resume();process.stdin.once("data",()=>process.exit(0))'])
|
|
425
|
+
finishRun=()=>{child.stdin!.end('done')}
|
|
426
|
+
return {child,cleanup:async()=>{},stdout:''}
|
|
427
|
+
},async()=>{
|
|
428
|
+
if(++calls>1){startedRefresh();await blocked}
|
|
429
|
+
return [{cli:'grok',name:'Grok',efforts:[]}]
|
|
430
|
+
})
|
|
431
|
+
for(let n=0;n<300;n++){try{await readFile(path.join(directory,'heartbeat.json'));break}catch{await new Promise(r=>setTimeout(r,20))}}
|
|
432
|
+
client=spawn(process.execPath,['--import',fileURLToPath(new URL('../node_modules/tsx/dist/loader.mjs',import.meta.url)),fileURLToPath(new URL('../src/host-executor-client.ts',import.meta.url)),root,'r_slow_catalog'],{stdio:['pipe','pipe','pipe']})
|
|
433
|
+
closed=new Promise(resolve=>client!.once('close',resolve))
|
|
434
|
+
let stderr='';client.stderr!.on('data',chunk=>stderr+=chunk);client.stdout!.resume()
|
|
435
|
+
client.stdin!.end(JSON.stringify({texts:['test'],options:{cli:'grok'}}))
|
|
436
|
+
await refreshing
|
|
437
|
+
// Exceed the real relay client's 15s offline threshold while discovery hangs.
|
|
438
|
+
await new Promise(resolve=>setTimeout(resolve,16500))
|
|
439
|
+
const heartbeat=JSON.parse(await readFile(path.join(directory,'heartbeat.json'),'utf8'))
|
|
440
|
+
assert.ok(Date.now()-heartbeat.at<5000,'heartbeat stopped during catalog I/O')
|
|
441
|
+
assert.equal(client.exitCode,null,stderr)
|
|
442
|
+
await assert.rejects(readFile(path.join(directory,'r_slow_catalog.cancel')),{code:'ENOENT'})
|
|
443
|
+
assert.equal(calls,2,'catalog refresh must not overlap itself')
|
|
444
|
+
assert.ok(finishRun,'the native run must have started')
|
|
445
|
+
finishRun()
|
|
446
|
+
assert.equal(await closed,0,stderr)
|
|
447
|
+
releaseRefresh()
|
|
448
|
+
} finally {
|
|
449
|
+
releaseRefresh();abort.abort()
|
|
450
|
+
if(client?.exitCode===null && client.signalCode===null)client.kill()
|
|
451
|
+
await closed;await server;await rm(root,{recursive:true,force:true})
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
test('cross-CLI admission uses the advertised catalog without another discovery probe',async()=>{
|
|
456
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-host-advertised-'))
|
|
457
|
+
const workspace=path.join(root,'mind'),controlDir=path.join(root,'control'),directory=path.join(controlDir,'host-executor')
|
|
458
|
+
const abort=new AbortController();let server:Promise<void>|undefined
|
|
459
|
+
const previousPath=process.env.PATH
|
|
460
|
+
try {
|
|
461
|
+
await mkdir(workspace,{recursive:true});await mkdir(directory,{recursive:true})
|
|
462
|
+
const fakeBin=path.join(root,'bin');await mkdir(fakeBin,{recursive:true})
|
|
463
|
+
await writeFile(path.join(fakeBin,'codex'),'#!/bin/sh\nexit 0\n',{mode:0o700})
|
|
464
|
+
process.env.PATH=`${fakeBin}${path.delimiter}${previousPath ?? ''}`
|
|
465
|
+
const installation={cli:'grok' as const,agents:[{name:'test',workspace,controlDir,binDir:fakeBin}]}
|
|
466
|
+
const advertised=[{cli:'codex',model:'cached-model',name:'Cached',efforts:[]}]
|
|
467
|
+
let reads=0
|
|
468
|
+
server=serveHostExecutor(installation,abort.signal,async()=>{
|
|
469
|
+
const child=spawn(process.execPath,['-e','process.exit(0)'])
|
|
470
|
+
return {child,cleanup:async()=>{},stdout:''}
|
|
471
|
+
},async()=>{if(++reads>1)throw new Error('unexpected discovery during admission');return advertised})
|
|
472
|
+
for(let n=0;n<300;n++){try{await readFile(path.join(directory,'heartbeat.json'));break}catch{await new Promise(r=>setTimeout(r,20))}}
|
|
473
|
+
await readFile(path.join(directory,'heartbeat.json'))
|
|
474
|
+
await ownerRun(controlDir,'r_advertised')
|
|
475
|
+
await writeFile(path.join(directory,'r_advertised.request.json'),JSON.stringify({
|
|
476
|
+
texts:['hello'],options:{cli:'codex',model:'cached-model'},
|
|
477
|
+
}))
|
|
478
|
+
let events=''
|
|
479
|
+
for(let n=0;n<200;n++){
|
|
480
|
+
try{events=await readFile(path.join(directory,'r_advertised.events'),'utf8');if(events.includes('"stream":"exit"'))break}catch{}
|
|
481
|
+
await new Promise(r=>setTimeout(r,20))
|
|
482
|
+
}
|
|
483
|
+
assert.match(events,/"stream":"exit","code":0/)
|
|
484
|
+
assert.equal(reads,1)
|
|
485
|
+
} finally {
|
|
486
|
+
abort.abort();await server
|
|
487
|
+
if(previousPath===undefined)delete process.env.PATH;else process.env.PATH=previousPath
|
|
488
|
+
await rm(root,{recursive:true,force:true})
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
test('startup replaces prior catalog after provider policy changes even when discovery is empty',async()=>{
|
|
493
|
+
const root=await mkdtemp(path.join(tmpdir(),'ez-host-policy-reset-'))
|
|
494
|
+
const directory=path.join(root,'host-executor')
|
|
495
|
+
const abort=new AbortController();let server:Promise<void>|undefined
|
|
496
|
+
try {
|
|
497
|
+
await mkdir(directory)
|
|
498
|
+
await writeFile(path.join(directory,'models.json'),JSON.stringify([
|
|
499
|
+
{cli:'opencode',model:'old-provider/model',name:'Old provider',efforts:[]},
|
|
500
|
+
]))
|
|
501
|
+
server=serveHostExecutor({cli:'grok',agents:[{
|
|
502
|
+
name:'test',workspace:root,controlDir:root,binDir:root,opencodeProviders:['allowed-provider'],
|
|
503
|
+
}]},abort.signal,undefined,async()=>[])
|
|
504
|
+
for(let n=0;n<300;n++){try{await readFile(path.join(directory,'heartbeat.json'));break}catch{await new Promise(r=>setTimeout(r,20))}}
|
|
505
|
+
await readFile(path.join(directory,'heartbeat.json'))
|
|
506
|
+
assert.deepEqual(JSON.parse(await readFile(path.join(directory,'models.json'),'utf8')),[])
|
|
507
|
+
} finally {abort.abort();await server;await rm(root,{recursive:true,force:true})}
|
|
352
508
|
})
|
|
@@ -10,6 +10,9 @@ import { serveTestLedger } from './helpers/ledger.js'
|
|
|
10
10
|
import { deliveredMessages } from '../src/message-history.js'
|
|
11
11
|
import { RunStore } from '../src/runs.js'
|
|
12
12
|
import { ControlStore } from '../src/control-state.js'
|
|
13
|
+
import { ownerId, ownerEpoch } from '../src/control-state.js'
|
|
14
|
+
import { ApplicationBindings } from '../src/application-channel.js'
|
|
15
|
+
import { randomBytes } from 'node:crypto'
|
|
13
16
|
|
|
14
17
|
async function fixture(t: test.TestContext) {
|
|
15
18
|
const dir = await mkdtemp(join(tmpdir(), 'ez-history-'))
|
|
@@ -117,3 +120,47 @@ test('CLI flushes complete long reports and rejects send options in history mode
|
|
|
117
120
|
// History reads never create outbox state on disk.
|
|
118
121
|
await assert.rejects(stat(join(dir, 'outbox')))
|
|
119
122
|
})
|
|
123
|
+
|
|
124
|
+
test('application history reads only delivered literal text in its current binding and scope', async t => {
|
|
125
|
+
const { dir, runs } = await fixture(t)
|
|
126
|
+
const owner = (await new ControlStore(dir, 1000).status()).owner!
|
|
127
|
+
const bindings = new ApplicationBindings(dir)
|
|
128
|
+
const first = (await bindings.register('first', randomBytes(32).toString('base64url'), owner))!
|
|
129
|
+
const second = (await bindings.register('second', randomBytes(32).toString('base64url'), owner))!
|
|
130
|
+
const create = async (id: string, bindingId: string, scope: string) => {
|
|
131
|
+
const run = await runs.create({ id, ownerId: ownerId(owner), ownerEpoch: ownerEpoch(owner),
|
|
132
|
+
texts: ['test'], application: { bindingId, scope, requestId: id } })
|
|
133
|
+
await runs.patch(id, { status: 'running' })
|
|
134
|
+
return run
|
|
135
|
+
}
|
|
136
|
+
const earlier = await create('r_app_earlier', first.bindingId, 'book')
|
|
137
|
+
const current = await create('r_app_current', first.bindingId, 'book')
|
|
138
|
+
const otherScope = await create('r_app_other_scope', first.bindingId, 'private')
|
|
139
|
+
const otherBinding = await create('r_app_other_binding', second.bindingId, 'book')
|
|
140
|
+
const deliver = async (runId: string, text: string) => {
|
|
141
|
+
const item = await runs.enqueueMessage(runId, text)
|
|
142
|
+
await runs.claimOutbox(item.id)
|
|
143
|
+
await runs.markOutboxSent(item.id)
|
|
144
|
+
return item
|
|
145
|
+
}
|
|
146
|
+
const literal = 'Book is $1,234; path C:\\reports\\$raw'
|
|
147
|
+
const firstItem = await deliver(earlier.id, literal)
|
|
148
|
+
await runs.patch(earlier.id, { status: 'completed' })
|
|
149
|
+
await deliver(otherScope.id, 'other scope secret')
|
|
150
|
+
await deliver(otherBinding.id, 'other binding secret')
|
|
151
|
+
await deliver('tg_1', 'Telegram secret')
|
|
152
|
+
await runs.enqueueMessage(current.id, 'queued is not delivered')
|
|
153
|
+
const env = { ...process.env, EZ_CONTROL_DIR: dir, EZ_RUN_ID: current.id }
|
|
154
|
+
const { stdout } = await promisify(execFile)(process.execPath, ['bin/ezenciel-agents-message.mjs', 'history'], { env })
|
|
155
|
+
const result = JSON.parse(stdout)
|
|
156
|
+
assert.equal(result.scope, 'book')
|
|
157
|
+
assert.deepEqual(result.messages.map((message: { id: string; text: string }) => ({ id: message.id, text: message.text })),
|
|
158
|
+
[{ id: firstItem.id, text: literal }])
|
|
159
|
+
assert.equal(result.hasMore, false)
|
|
160
|
+
await assert.rejects(promisify(execFile)(process.execPath,
|
|
161
|
+
['bin/ezenciel-agents-message.mjs', 'history', '--message-id', '1'], { env }), /Telegram history/)
|
|
162
|
+
await bindings.register('first', null, owner)
|
|
163
|
+
await assert.rejects(deliveredMessages(dir, current.id), /revoked/)
|
|
164
|
+
await new ControlStore(dir, 1000).revokeOwner()
|
|
165
|
+
await assert.rejects(deliveredMessages(dir, otherBinding.id), /owner-mismatch|revoked/)
|
|
166
|
+
})
|
|
@@ -12,7 +12,7 @@ import { runCodexSession } from '../src/codex-session.js'
|
|
|
12
12
|
import { runDesktopTurn } from '../src/desktop-bridge.js'
|
|
13
13
|
|
|
14
14
|
test('engine defaults stay omitted and explicit native settings survive every adapter', async () => {
|
|
15
|
-
for(const cli of ['codex','codex-gui','grok','claude','opencode'
|
|
15
|
+
for(const cli of ['codex','codex-gui','grok','claude','opencode']) {
|
|
16
16
|
assert.deepEqual(executionDefaults(cli,{}),{})
|
|
17
17
|
const preset={id:'chosen',name:'Chosen',cli,model:'native-model',effort:'ultra'}
|
|
18
18
|
assert.deepEqual(executionDefaults(cli,preset),preset)
|
|
@@ -41,7 +41,7 @@ test('invalid setting syntax never mutates a saved choice', async () => {
|
|
|
41
41
|
test('non-Codex catalog defaults survive executor normalization and host revalidation', async () => {
|
|
42
42
|
const dir = await mkdtemp(join(tmpdir(), 'ez-adapter-defaults-'))
|
|
43
43
|
try {
|
|
44
|
-
const available = async (cli: string) => ['claude', 'opencode'
|
|
44
|
+
const available = async (cli: string) => ['claude', 'opencode'].includes(cli)
|
|
45
45
|
const catalog = await readModels(dir, available)
|
|
46
46
|
for (const choice of catalog) {
|
|
47
47
|
const preset = {id:'selected', name:choice.name, cli:choice.cli, model:choice.model}
|
|
@@ -276,6 +276,23 @@ test('isolated broker host binding validates exact mounts without deployment sym
|
|
|
276
276
|
if(previous===undefined)delete process.env.EZ_DOCKER_COMPOSE;else process.env.EZ_DOCKER_COMPOSE=previous;
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
|
+
test('isolated broker allows unbound plugins when another plugin has host binding',async t=>{
|
|
280
|
+
const f=await fixture(t),p=await snapshot(f.source),isolated=path.join(f.root,'isolated-other'),workspace=path.join(isolated,'workspace'),control=path.join(isolated,'control'),hostConfig=path.join(isolated,'host-executor.json'),previous=process.env.EZ_DOCKER_COMPOSE;
|
|
281
|
+
await fs.mkdir(f.home,{recursive:true});await fs.mkdir(workspace,{recursive:true});await fs.mkdir(control,{recursive:true});
|
|
282
|
+
await fs.writeFile(hostConfig,JSON.stringify({cli:'codex',isolation:'isolated',agents:[{name:'sample',workspace,controlDir:control,binDir:path.join(f.root,'bin'),toolsHome:f.home}]}));
|
|
283
|
+
const record={source:p.source,project:'ezp-synthetic',revision:p.revision,manifest:p.manifest,deployment:p.deployment};
|
|
284
|
+
const config={workspace:await fs.realpath(workspace),hostConfig:await fs.realpath(hostConfig)};
|
|
285
|
+
await fs.writeFile(hostConfig,JSON.stringify({cli:'codex',isolation:'isolated',agents:[{name:'sample',workspace,controlDir:control,binDir:path.join(f.root,'bin'),toolsHome:f.home,pluginNetworkBindings:{voice:{revisions:[p.revision],bindings:[{service:'sample',network:'voice_default'}]}}}]}));
|
|
286
|
+
process.env.EZ_DOCKER_COMPOSE='standalone';
|
|
287
|
+
try {
|
|
288
|
+
const c=await compose(config,record,{},f.home);
|
|
289
|
+
assert.equal(c.version,'3.8');assert.equal(c.networks,undefined);
|
|
290
|
+
await fs.writeFile(hostConfig,JSON.stringify({cli:'codex',isolation:'isolated',agents:[{name:'sample',workspace,controlDir:control,binDir:path.join(f.root,'bin'),toolsHome:f.home,pluginNetworkBindings:{sample:{revisions:[p.revision],bindings:[{service:'sample',network:'sample_default'}]}}}]}));
|
|
291
|
+
await assert.rejects(compose(config,record,{},f.home),/Isolated broker cannot use host network bindings/);
|
|
292
|
+
} finally {
|
|
293
|
+
if(previous===undefined)delete process.env.EZ_DOCKER_COMPOSE;else process.env.EZ_DOCKER_COMPOSE=previous;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
279
296
|
test('host-owned private networks survive registered-call Compose regeneration',async t=>{
|
|
280
297
|
const f=await fixture(t),p=await snapshot(f.source);await init(f.home,f.workspace);
|
|
281
298
|
await f.call('plugins','install','sample','--source',f.source,'--revision',p.revision);
|
package/test/setup.test.ts
CHANGED
|
@@ -13,11 +13,11 @@ test('findExecutableInPath locates existing binary and returns null for nonexist
|
|
|
13
13
|
assert.equal(missing, null)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
test('readActiveExecutor extracts EZ_EXECUTOR_CLI or defaults to
|
|
16
|
+
test('readActiveExecutor extracts EZ_EXECUTOR_CLI or defaults to codex', async () => {
|
|
17
17
|
const dir = await mkdtemp(path.join(tmpdir(), 'ez-setup-test-'))
|
|
18
18
|
try {
|
|
19
19
|
const envPath = path.join(dir, '.env')
|
|
20
|
-
assert.equal(await readActiveExecutor(envPath), '
|
|
20
|
+
assert.equal(await readActiveExecutor(envPath), 'codex')
|
|
21
21
|
|
|
22
22
|
await writeFile(envPath, 'EZ_EXECUTOR_CLI=claude\n', 'utf8')
|
|
23
23
|
assert.equal(await readActiveExecutor(envPath), 'claude')
|
|
@@ -37,10 +37,10 @@ test('setExecutorInEnv updates or adds EZ_EXECUTOR_CLI without altering other en
|
|
|
37
37
|
assert.match(content, /TELEGRAM_BOT_TOKEN=token123/)
|
|
38
38
|
assert.match(content, /EZ_EXECUTOR_CLI=claude/)
|
|
39
39
|
|
|
40
|
-
await setExecutorInEnv(envPath, '
|
|
40
|
+
await setExecutorInEnv(envPath, 'codex')
|
|
41
41
|
content = await readFile(envPath, 'utf8')
|
|
42
42
|
assert.match(content, /TELEGRAM_BOT_TOKEN=token123/)
|
|
43
|
-
assert.match(content, /EZ_EXECUTOR_CLI=
|
|
43
|
+
assert.match(content, /EZ_EXECUTOR_CLI=codex/)
|
|
44
44
|
assert.ok(!content.includes('claude'))
|
|
45
45
|
} finally {
|
|
46
46
|
await rm(dir, { recursive: true, force: true })
|
|
@@ -59,9 +59,9 @@ test('getExecutorsStatus returns all executors and sets isActive correctly', asy
|
|
|
59
59
|
assert.ok(claudeItem)
|
|
60
60
|
assert.equal(claudeItem.isActive, true)
|
|
61
61
|
|
|
62
|
-
const
|
|
63
|
-
assert.ok(
|
|
64
|
-
assert.equal(
|
|
62
|
+
const codexItem = status.items.find((item) => item.id === 'codex')
|
|
63
|
+
assert.ok(codexItem)
|
|
64
|
+
assert.equal(codexItem.isActive, false)
|
|
65
65
|
} finally {
|
|
66
66
|
await rm(dir, { recursive: true, force: true })
|
|
67
67
|
}
|