@jc_stack/ez-agents 0.1.0-beta.25 → 0.1.0-beta.27
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 +1 -1
- package/AGENTS.md +15 -8
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +5 -4
- package/docs/architecture/ai-selection.md +12 -15
- package/docs/docker-runtime.md +9 -0
- package/docs/host-service.md +5 -8
- package/docs/local-qa.md +1 -1
- package/docs/plugins.md +14 -1
- package/docs/releasing.md +3 -2
- package/docs/repair.md +26 -25
- package/docs/responsive-channels.md +13 -55
- package/docs/scheduling.md +40 -36
- package/docs/setup.md +11 -21
- package/docs/standalone-cli.md +2 -2
- package/docs/upgrades.md +32 -17
- package/package.json +2 -3
- package/src/agent-guidance.ts +32 -3
- package/src/ai-cli.ts +5 -1
- package/src/ai.ts +6 -28
- package/src/codex-session.ts +4 -9
- package/src/config.ts +3 -3
- package/src/control-state.ts +18 -6
- package/src/desktop-bridge.ts +11 -43
- package/src/executor.ts +20 -55
- package/src/host-executor.ts +4 -8
- package/src/index.ts +50 -45
- package/src/menu.ts +51 -47
- package/src/message-send.ts +1 -1
- package/src/message.ts +1 -0
- package/src/model-policy.ts +5 -15
- package/src/plugins/manager.mjs +31 -6
- package/src/repair-policy.ts +0 -8
- package/src/reply-context.ts +3 -29
- package/src/schedule-cli.ts +24 -11
- package/src/scheduled-tasks.ts +20 -21
- package/src/scheduler.ts +38 -15
- package/src/task-executor.ts +4 -5
- package/src/task-workspace.ts +2 -11
- package/src/update-attention.ts +1 -1
- package/src/updates/binding.mjs +2 -6
- package/src/updates/supervisor.mjs +10 -4
- package/src/workspace.ts +3 -1
- package/templates/agent/AGENTS.md +13 -55
- package/templates/agent-guidance.md +27 -30
- package/templates/failure-review.md +6 -0
- package/templates/maintainer-purpose.md +12 -6
- package/test/agent-guidance.test.ts +29 -39
- package/test/ai-cli.test.ts +9 -0
- package/test/ai.test.ts +66 -22
- package/test/busy-reply-relay.test.ts +11 -7
- package/test/client-defaults.test.ts +1 -1
- package/test/codex-session.test.ts +15 -10
- package/test/config.test.ts +1 -1
- package/test/desktop-bridge.test.ts +17 -11
- package/test/engine-handoff.test.ts +73 -0
- package/test/event-sources.test.ts +5 -8
- package/test/executor.test.ts +12 -16
- package/test/failure.test.ts +64 -0
- package/test/host-executor.test.ts +30 -17
- package/test/install-config.test.ts +1 -1
- package/test/intake-relay.test.ts +47 -24
- package/test/model-policy.test.ts +23 -48
- package/test/plugin-manager.test.mjs +36 -10
- package/test/repair-policy.test.ts +8 -12
- package/test/runs.test.ts +13 -0
- package/test/schedule-cli.test.ts +34 -5
- package/test/scheduled-tasks.test.ts +79 -8
- package/test/scheduler-relay.test.ts +25 -0
- package/test/scheduler.test.ts +30 -1
- package/test/task-native.test.ts +5 -2
- package/test/update-attention.test.ts +1 -2
- package/test/updates.test.mjs +5 -5
- package/test/workspace.test.ts +2 -3
- package/scripts/smoke-busy-reply.ts +0 -58
- package/src/reply-executor.ts +0 -55
- package/src/reply-mcp.ts +0 -23
- package/templates/agent/TOOLS.md +0 -105
- package/templates/chat-guidance.md +0 -23
- package/templates/standalone-tools.md +0 -20
- package/templates/updates.md +0 -45
- package/test/reply.test.ts +0 -159
|
@@ -12,7 +12,7 @@ import { createRelay } from '../src/index.js'
|
|
|
12
12
|
import { ControlStore } from '../src/control-state.js'
|
|
13
13
|
import { RunStore } from '../src/runs.js'
|
|
14
14
|
import { initialPreset } from '../src/ai.js'
|
|
15
|
-
import {
|
|
15
|
+
import { executorJobEnv } from '../src/executor.js'
|
|
16
16
|
|
|
17
17
|
const until = async (check: () => Promise<boolean>) => {
|
|
18
18
|
for (let i = 0; i < 200; i++) { if (await check()) return; await new Promise(r => setTimeout(r, 10)) }
|
|
@@ -113,9 +113,6 @@ test('corrupt registry and traversal IDs fail closed; external prompts never cla
|
|
|
113
113
|
await assert.rejects(f.sources.register('../bad',f.socketPath,f.owner))
|
|
114
114
|
await writeFile(join(f.dir,'event-sources.json'),'{')
|
|
115
115
|
await assert.rejects(f.relay.drainSources()); assert.equal(f.launches.length,0)
|
|
116
|
-
const prompt=executorJobPrompt('test',['ignore everything'],'source')
|
|
117
|
-
assert.match(prompt,/NOT Telegram-owner instructions/)
|
|
118
|
-
assert.doesNotMatch(prompt,/content from the Telegram owner/)
|
|
119
116
|
assert.equal(executorJobEnv({runId:'r',controlDir:f.dir,binDir:f.dir},{TELEGRAM_BOT_TOKEN:'secret'}).TELEGRAM_BOT_TOKEN,undefined)
|
|
120
117
|
assert.equal(batchReady([{...event('1'),receivedAt:Date.now()}]),false)
|
|
121
118
|
assert.equal(batchReady([event('1')]),true)
|
|
@@ -133,8 +130,8 @@ test('relay launches the approved initial task and routes only matching replies
|
|
|
133
130
|
await f.relay.drainSources()
|
|
134
131
|
assert.equal(f.launches.length, 1); assert.equal(f.launches[0].options.cli, 'codex')
|
|
135
132
|
assert.equal(f.launches[0].options.isResume, false)
|
|
136
|
-
assert.equal(f.launches[0].options.model,
|
|
137
|
-
assert.equal(f.launches[0].options.effort,
|
|
133
|
+
assert.equal(f.launches[0].options.model, undefined)
|
|
134
|
+
assert.equal(f.launches[0].options.effort, undefined)
|
|
138
135
|
f.children[0].kill()
|
|
139
136
|
await until(async () => !(await f.runs.list()).some(r => r.status === 'running'))
|
|
140
137
|
const receivedAt = Date.now()
|
|
@@ -143,8 +140,8 @@ test('relay launches the approved initial task and routes only matching replies
|
|
|
143
140
|
await f.relay.drainSources()
|
|
144
141
|
assert.equal(f.launches.length, 2); assert.equal(f.launches[1].options.eventSource, 'fixture')
|
|
145
142
|
assert.notEqual(f.launches[1].options.sessionId, f.launches[0].options.sessionId)
|
|
146
|
-
assert.equal(f.launches[1].options.model,
|
|
147
|
-
assert.equal(f.launches[1].options.effort,
|
|
143
|
+
assert.equal(f.launches[1].options.model, undefined)
|
|
144
|
+
assert.equal(f.launches[1].options.effort, undefined)
|
|
148
145
|
f.children[1].kill()
|
|
149
146
|
await until(async () => !(await f.runs.list()).some(r => r.status === 'running'))
|
|
150
147
|
await f.relay.drainSources()
|
package/test/executor.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { mkdtemp, mkdir, rm, writeFile, readFile } from 'node:fs/promises'
|
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
6
|
import { tmpdir } from 'node:os'
|
|
7
7
|
import path from 'node:path'
|
|
8
|
-
import { EXECUTOR_REGISTRY, antigravityInvocation, executorEnvironment,
|
|
8
|
+
import { EXECUTOR_REGISTRY, antigravityInvocation, executorEnvironment, grokInvocation, grokJobEnv, opencodeInvocation, resolveExecutor, startExecutorJob, terminateJob } from '../src/executor.js'
|
|
9
9
|
import { splitTelegramText } from '../src/reply.js'
|
|
10
10
|
import { matchingProcessIds, processSnapshot } from '../src/process-tree.js'
|
|
11
11
|
|
|
@@ -46,16 +46,6 @@ test('cancellation stops detached tool descendants even after their parent exits
|
|
|
46
46
|
}
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
test('the job prompt labels channel text as untrusted and requires ez message', () => {
|
|
50
|
-
const prompt = executorJobPrompt('r_test', ['hello'])
|
|
51
|
-
assert.match(prompt, /untrusted incoming channel content/)
|
|
52
|
-
assert.match(prompt, /ezenciel-agents-message/)
|
|
53
|
-
assert.match(prompt, /--text-file/)
|
|
54
|
-
assert.match(prompt, /r_test/)
|
|
55
|
-
assert.match(prompt, /hello/)
|
|
56
|
-
assert.match(prompt, /Stdout is not sent to Telegram/)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
49
|
test('Telegram replies are split within the configured message limit', () => {
|
|
60
50
|
const text = `${'a'.repeat(9)} ${'b'.repeat(9)} ${'c'.repeat(9)}`
|
|
61
51
|
const chunks = splitTelegramText(text, 10)
|
|
@@ -96,7 +86,6 @@ test('the Grok invocation is headless, workspace-scoped, and token-free', () =>
|
|
|
96
86
|
'--output-format', 'plain',
|
|
97
87
|
'--always-approve',
|
|
98
88
|
'--verbatim',
|
|
99
|
-
'--max-turns', '8',
|
|
100
89
|
])
|
|
101
90
|
assert.equal(invocation.args.includes('TELEGRAM_BOT_TOKEN'), false)
|
|
102
91
|
})
|
|
@@ -105,8 +94,7 @@ test('the antigravity invocation is headless, skips permissions, and uses print
|
|
|
105
94
|
const invocation = antigravityInvocation('test prompt')
|
|
106
95
|
assert.equal(invocation.command, 'agy')
|
|
107
96
|
assert.deepEqual(invocation.args, [
|
|
108
|
-
'--
|
|
109
|
-
'--dangerously-skip-permissions',
|
|
97
|
+
'--dangerously-skip-permissions', '--print=test prompt',
|
|
110
98
|
])
|
|
111
99
|
})
|
|
112
100
|
|
|
@@ -118,6 +106,7 @@ test('the opencode invocation is headless, auto-approves, and sets model and wor
|
|
|
118
106
|
'--auto',
|
|
119
107
|
'--format',
|
|
120
108
|
'json',
|
|
109
|
+
'--',
|
|
121
110
|
'test prompt',
|
|
122
111
|
])
|
|
123
112
|
})
|
|
@@ -179,8 +168,15 @@ test('Codex plugin access stays scoped to the explicitly bound registry', () =>
|
|
|
179
168
|
test('Codex compaction preserves native resume and validates transported options',()=>{
|
|
180
169
|
const args=EXECUTOR_REGISTRY.codex.buildArgs({workspace:'/agent',sessionId:'native-id',isResume:true,codexAutoCompactTokens:32000},'', 'hello')
|
|
181
170
|
assert.ok(args.includes('model_auto_compact_token_limit=32000'))
|
|
182
|
-
assert.deepEqual(args.slice(-3),['resume','native-id','
|
|
183
|
-
assert.ok(EXECUTOR_REGISTRY.codex.buildArgs({workspace:'/agent'},'','hello').includes('model_auto_compact_token_limit
|
|
171
|
+
assert.deepEqual(args.slice(-3),['resume','native-id','-'])
|
|
172
|
+
assert.ok(!EXECUTOR_REGISTRY.codex.buildArgs({workspace:'/agent'},'','hello').some(a=>a.includes('model_auto_compact_token_limit')))
|
|
184
173
|
for(const value of [0,-1,NaN,1.5]) assert.throws(()=>EXECUTOR_REGISTRY.codex.buildArgs({workspace:'/agent',codexAutoCompactTokens:value},'','hello'),/compaction/)
|
|
185
174
|
assert.ok(!EXECUTOR_REGISTRY.claude.buildArgs({workspace:'/agent',codexAutoCompactTokens:32000},'','hello').some(arg=>arg.includes('compact')))
|
|
186
175
|
})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
test('adapters do not append instruction files or impose a workflow turn budget', () => {
|
|
179
|
+
const opts={workspace:'/agent/work/tasks/example'}
|
|
180
|
+
assert.ok(!EXECUTOR_REGISTRY.claude.buildArgs(opts,'','literal').includes('--append-system-prompt-file'))
|
|
181
|
+
assert.ok(!EXECUTOR_REGISTRY.grok.buildArgs(opts,'/tmp/prompt','literal').includes('--max-turns'))
|
|
182
|
+
})
|
package/test/failure.test.ts
CHANGED
|
@@ -198,6 +198,8 @@ for (const cleanupFails of [false,true]) test(`polling conflict preserves work u
|
|
|
198
198
|
let finished=false
|
|
199
199
|
const start=relay.start().finally(()=>{finished=true})
|
|
200
200
|
await until(async()=>polls===1)
|
|
201
|
+
if(!cleanupFails)await new Promise(resolve=>setTimeout(resolve,5200))
|
|
202
|
+
assert.equal(polls,1,'permanent conflict must not restart after the old five-second retry interval')
|
|
201
203
|
assert.equal(sourceStops,0,'a polling conflict must not stop the relay')
|
|
202
204
|
assert.equal((await runs.get('tg_92'))?.status,'running')
|
|
203
205
|
assert.ok(child && child.exitCode===null && child.signalCode===null)
|
|
@@ -280,3 +282,65 @@ test('group members can inspect failures and wake review without exposing other
|
|
|
280
282
|
assert.equal((await runs.list()).filter(r=>r.scheduled).length,1)
|
|
281
283
|
await assert.rejects(exec(process.execPath,[bin,'run','tg_2'],{env}),/Unknown owner/)
|
|
282
284
|
})
|
|
285
|
+
|
|
286
|
+
for (const failure of [400,401,503,'programming'] as const) test(`setup failure ${failure} retries only transient errors`,async t=>{
|
|
287
|
+
const dir=await mkdtemp(join(tmpdir(),'ez-setup-failure-'))
|
|
288
|
+
const relay=createRelay({workspace:dir,controlDir:dir,pairingTtlMs:1000,executorTimeoutMs:0,executorCli:'grok',telegramBotToken:'fixture'},async()=>{throw Error('No executor expected')})
|
|
289
|
+
let commands=0,polls=0
|
|
290
|
+
relay.bot.botInfo={id:999,is_bot:true,first_name:'Fixture',username:'fixture_bot'} as any
|
|
291
|
+
relay.bot.api.config.use(async(_prev,method,_payload,signal)=>{
|
|
292
|
+
if(method==='setMyCommands' && ++commands===1) {
|
|
293
|
+
if(failure==='programming')throw Error('Synthetic permanent setup fault')
|
|
294
|
+
return {ok:false,error_code:failure,description:'Synthetic setup failure'} as any
|
|
295
|
+
}
|
|
296
|
+
if(method==='getUpdates'){
|
|
297
|
+
polls++
|
|
298
|
+
if(signal && !signal.aborted)await new Promise<void>(resolve=>signal.addEventListener('abort',()=>resolve(),{once:true}))
|
|
299
|
+
return {ok:true,result:[]} as any
|
|
300
|
+
}
|
|
301
|
+
return {ok:true,result:true} as any
|
|
302
|
+
})
|
|
303
|
+
const started=relay.start()
|
|
304
|
+
try {
|
|
305
|
+
await until(async()=>commands>0)
|
|
306
|
+
if(failure===503){await until(async()=>polls>0);assert.equal(commands,3)}
|
|
307
|
+
else {await new Promise(resolve=>setTimeout(resolve,5200));assert.equal(commands,1);assert.equal(polls,0)}
|
|
308
|
+
}finally{await relay.stop();await started;await rm(dir,{recursive:true,force:true})}
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('shutdown aborts a pending bot initialization without starting polling',async()=>{
|
|
312
|
+
const dir=await mkdtemp(join(tmpdir(),'ez-init-stop-'))
|
|
313
|
+
const relay=createRelay({workspace:dir,controlDir:dir,pairingTtlMs:1000,executorTimeoutMs:0,executorCli:'grok',telegramBotToken:'fixture'},async()=>{throw Error('No executor expected')})
|
|
314
|
+
let initSignal:Parameters<typeof relay.bot.init>[0],polls=0
|
|
315
|
+
relay.bot.api.config.use(async(_prev,method,_payload,signal)=>{
|
|
316
|
+
if(method==='getMe'){
|
|
317
|
+
initSignal=signal
|
|
318
|
+
if(signal && !signal.aborted)await new Promise<void>(resolve=>signal.addEventListener('abort',()=>resolve(),{once:true}))
|
|
319
|
+
throw Error('Initialization aborted')
|
|
320
|
+
}
|
|
321
|
+
if(method==='getUpdates')polls++
|
|
322
|
+
return {ok:true,result:true} as any
|
|
323
|
+
})
|
|
324
|
+
const started=relay.start()
|
|
325
|
+
try {await until(async()=>Boolean(initSignal));await relay.stop();await started;assert.equal(initSignal?.aborted,true);assert.equal(polls,0)}
|
|
326
|
+
finally {await relay.stop();await started;await rm(dir,{recursive:true,force:true})}
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('failed reviewer readback exposes its stop through show, list and resume until explicit edit',async t=>{
|
|
330
|
+
const dir=await mkdtemp(join(tmpdir(),'ez-review-stop-'));t.after(()=>rm(dir,{recursive:true,force:true}))
|
|
331
|
+
const control=new ControlStore(dir,1000),runs=new RunStore(dir),scheduler=new Scheduler(dir)
|
|
332
|
+
await control.requestPairing(101,101);const owner=await control.approveOwner(101)
|
|
333
|
+
const execution=await control.captureChoice(initialPreset('grok')),now=Date.now()+1000
|
|
334
|
+
const s=await scheduler.save({id:'review',name:'Review',text:'Review failures',trigger:{everySeconds:60,start:new Date(now).toISOString()},when:'unreviewed-failures',enabled:true,owner,execution})
|
|
335
|
+
await runs.create({id:'r_original',chatId:101,telegramUserId:101,texts:['Work'],execution});await runs.patch('r_original',{status:'failed'})
|
|
336
|
+
await scheduler.tick(owner,runs,now)
|
|
337
|
+
const reviewer=(await runs.list()).find(r=>r.scheduled)!
|
|
338
|
+
await runs.patch(reviewer.id,{status:'failed'})
|
|
339
|
+
const cli=async(args:string[])=>JSON.parse((await exec(process.execPath,[bin,...args],{env:{...process.env,EZ_CONTROL_DIR:dir,EZ_EXECUTOR_CLI:'grok',EZ_RUN_ID:''}})).stdout)
|
|
340
|
+
for(const action of ['show','resume']){
|
|
341
|
+
const result=await cli([action,s.id]);assert.equal(result.nextEligibleAt,null);assert.deepEqual(result.failedReviewRunIds,[reviewer.id]);assert.match(result.recovery,/explicitly edit/)
|
|
342
|
+
}
|
|
343
|
+
const listed=await cli(['list']);assert.equal(listed[0].nextEligibleAt,null)
|
|
344
|
+
await scheduler.save({...s,text:'Repaired reviewer'})
|
|
345
|
+
const result=await cli(['show',s.id]);assert.notEqual(result.nextEligibleAt,null);assert.deepEqual(result.failedReviewRunIds,[])
|
|
346
|
+
})
|
|
@@ -23,12 +23,12 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
23
23
|
let server:Promise<void>|undefined
|
|
24
24
|
try {
|
|
25
25
|
const binary=path.join(root,'cli')
|
|
26
|
-
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,args:process.argv.slice(2)}));\n`,{mode:0o700})
|
|
26
|
+
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})
|
|
27
27
|
await writeFile(path.join(root,'claude'),await readFile(binary),{mode:0o700})
|
|
28
28
|
await writeFile(path.join(root,'codex'),await readFile(binary),{mode:0o700})
|
|
29
29
|
process.env.PATH=root+path.delimiter+oldPath
|
|
30
30
|
EXECUTOR_REGISTRY.grok.command=binary
|
|
31
|
-
EXECUTOR_REGISTRY.grok.buildArgs=EXECUTOR_REGISTRY.codex.buildArgs
|
|
31
|
+
EXECUTOR_REGISTRY.grok.buildArgs=(opts,file,prompt)=>[...EXECUTOR_REGISTRY.codex.buildArgs(opts,file,prompt).slice(0,-1),prompt]
|
|
32
32
|
process.env.TELEGRAM_BOT_TOKEN='must-not-reach-host-cli'
|
|
33
33
|
const sharedAlias=path.join(root,'shared-alias')
|
|
34
34
|
await symlink(root,sharedAlias)
|
|
@@ -56,7 +56,8 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
56
56
|
assert.equal(result.cwd,await realpath(agent.workspace))
|
|
57
57
|
assert.equal(result.control,agent.controlDir)
|
|
58
58
|
assert.equal(result.token,undefined)
|
|
59
|
-
assert.
|
|
59
|
+
assert.equal(result.args.at(-1),'test')
|
|
60
|
+
assert.equal(result.repair,'true')
|
|
60
61
|
assert.ok(result.args.includes(agent.toolsHome))
|
|
61
62
|
assert.ok(result.args.includes(await realpath(root)))
|
|
62
63
|
assert.ok(!result.args.includes('/wrong'))
|
|
@@ -72,8 +73,8 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
72
73
|
client.stdin.end(JSON.stringify({texts:['Telegram message'],options:{cli:'grok',timeoutMs:5000,codexAutoCompactTokens:32000,repairEnabled:false}}))
|
|
73
74
|
assert.equal(await new Promise(resolve=>client.once('close',resolve)),0,stderr)
|
|
74
75
|
assert.equal(JSON.parse(stdout).run,'tg_6293305')
|
|
75
|
-
assert.
|
|
76
|
-
assert.
|
|
76
|
+
assert.equal(JSON.parse(stdout).args.at(-1),'Telegram message')
|
|
77
|
+
assert.equal(JSON.parse(stdout).repair,'false')
|
|
77
78
|
assert.ok(JSON.parse(stdout).args.includes('model_auto_compact_token_limit=32000'))
|
|
78
79
|
const eventId='event_'+'a'.repeat(64)
|
|
79
80
|
await ownerRun(agents[0].controlDir, eventId, {sourceId:'fixture',bindingId:'binding',eventIds:['1']})
|
|
@@ -107,8 +108,10 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
107
108
|
assert.ok(JSON.parse(boundOutput).args.includes('agent-only-fixture'))
|
|
108
109
|
await assert.rejects(serveHostExecutor({cli:'grok',agents},new AbortController().signal),/already running/)
|
|
109
110
|
const directory=path.join(agents[0].controlDir,'host-executor')
|
|
110
|
-
const
|
|
111
|
-
await
|
|
111
|
+
const runs=new RunStore(agents[0].controlDir)
|
|
112
|
+
await runs.create({id:'r_hold',chatId:101,telegramUserId:101,texts:['test'],scheduled:{id:'held',revision:'v1',dueAt:new Date().toISOString(),pairedAt:new Date().toISOString()}})
|
|
113
|
+
await runs.patch('r_hold',{status:'running'})
|
|
114
|
+
await writeFile(path.join(directory,'r_hold.request.json'),JSON.stringify({texts:['test'],options:{cli:'grok'}}))
|
|
112
115
|
for(let n=0;n<100;n++){try{await readFile(path.join(directory,'r_hold.process.json'));break}catch{await new Promise(r=>setTimeout(r,20))}}
|
|
113
116
|
await new RunStore(agents[0].controlDir).create({id:'r_schedule_queued',chatId:101,telegramUserId:101,texts:['test'],scheduled:{id:'shared',revision:'v1',dueAt:new Date().toISOString(),pairedAt:new Date().toISOString()}})
|
|
114
117
|
await new RunStore(agents[0].controlDir).patch('r_schedule_queued',{status:'running'})
|
|
@@ -116,17 +119,27 @@ test('one installed CLI executes two agent bindings with separate minds and sani
|
|
|
116
119
|
const otherDirectory=path.join(agents[1].controlDir,'host-executor')
|
|
117
120
|
await ownerRun(agents[1].controlDir,'r_other_shared')
|
|
118
121
|
await writeFile(path.join(otherDirectory,'r_other_shared.request.json'),JSON.stringify({texts:['test'],options:{cli:'grok'}}))
|
|
119
|
-
await
|
|
120
|
-
await
|
|
121
|
-
await
|
|
122
|
-
|
|
122
|
+
await runs.create({id:'tg_42',chatId:101,telegramUserId:101,messageId:42,texts:['Chat while scheduled work runs']})
|
|
123
|
+
await runs.patch('tg_42',{status:'running'})
|
|
124
|
+
await writeFile(path.join(directory,'tg_42.request.json'),JSON.stringify({texts:['Chat while scheduled work runs'],options:{cli:'grok'}}))
|
|
125
|
+
const completed=async(dir:string,id:string)=>{
|
|
126
|
+
let output=''
|
|
127
|
+
for(let n=0;n<200;n++){try{output=await readFile(path.join(dir,id+'.events'),'utf8');if(output.includes('"stream":"exit"'))break}catch{}await new Promise(r=>setTimeout(r,20))}
|
|
128
|
+
assert.match(output, /"stream":"exit","code":0/)
|
|
129
|
+
return output.trim().split('\n').map(line=>JSON.parse(line))
|
|
130
|
+
}
|
|
131
|
+
// A running scheduled engine does not reserve either its agent or its
|
|
132
|
+
// shared workspace, including another binding through a filesystem alias.
|
|
133
|
+
const [,,chatEvents]=await Promise.all([completed(directory,'r_schedule_queued'),completed(otherDirectory,'r_other_shared'),completed(directory,'tg_42')])
|
|
134
|
+
const chat=JSON.parse(chatEvents.filter(e=>e.stream==='stdout').map(e=>e.text).join(''))
|
|
135
|
+
assert.match(chat.args.at(-1),/^Chat while scheduled work runs/)
|
|
136
|
+
assert.match(chat.args.at(-1),/ezenciel-agents-message/)
|
|
137
|
+
assert.doesNotMatch(await readFile(path.join(directory,'r_hold.events'),'utf8'),/"stream":"exit"/)
|
|
138
|
+
await readFile(path.join(directory,'r_hold.running.json'))
|
|
123
139
|
await writeFile(path.join(directory,'r_hold.cancel'),'')
|
|
124
|
-
let
|
|
125
|
-
for(let n=0;n<200;n++){
|
|
126
|
-
assert.match(
|
|
127
|
-
assert.match(await readFile(path.join(directory,'r_hold.events'),'utf8'), /"stream":"exit","code":1/)
|
|
128
|
-
for(let n=0;n<200;n++){try{output=await readFile(path.join(otherDirectory,'r_other_shared.events'),'utf8');if(output.includes('"stream":"exit"'))break}catch{}await new Promise(r=>setTimeout(r,20))}
|
|
129
|
-
assert.match(output, /"stream":"exit","code":0/)
|
|
140
|
+
let heldOutput=''
|
|
141
|
+
for(let n=0;n<200;n++){heldOutput=await readFile(path.join(directory,'r_hold.events'),'utf8');if(heldOutput.includes('"stream":"exit"'))break;await new Promise(r=>setTimeout(r,20))}
|
|
142
|
+
assert.match(heldOutput, /"stream":"exit","code":1/)
|
|
130
143
|
} finally {
|
|
131
144
|
abort.abort();await server
|
|
132
145
|
EXECUTOR_REGISTRY.grok.command=old
|
|
@@ -60,7 +60,7 @@ test('packaged configure accepts token through stdin without echo or extra initi
|
|
|
60
60
|
const result = spawnSync(process.execPath, [bin, 'configure', 'codex', '--token-stdin'], { cwd: root, input: token, encoding: 'utf8', timeout: 10000 })
|
|
61
61
|
assert.equal(result.status, 0, result.stderr)
|
|
62
62
|
assert.equal((result.stdout + result.stderr).includes(token), false)
|
|
63
|
-
assert.equal(JSON.parse(result.stdout).created.length,
|
|
63
|
+
assert.equal(JSON.parse(result.stdout).created.length, 3)
|
|
64
64
|
assert.equal(parseEnv(await readFile(path.join(root, '.env'), 'utf8')).TELEGRAM_BOT_TOKEN, token)
|
|
65
65
|
} finally { await rm(root, { recursive: true, force: true }) }
|
|
66
66
|
})
|
|
@@ -14,6 +14,7 @@ import { ApprovalStore } from '../src/approval.js'
|
|
|
14
14
|
import { Tasks } from '../src/tasks.js'
|
|
15
15
|
import { ownerRun } from './helpers/owner-run.js'
|
|
16
16
|
import { packageVersion } from '../src/version.js'
|
|
17
|
+
import type { Config } from '../src/config.js'
|
|
17
18
|
|
|
18
19
|
const message = (id: number, text = 'hello'): Update => ({
|
|
19
20
|
update_id: id,
|
|
@@ -25,7 +26,7 @@ const message = (id: number, text = 'hello'): Update => ({
|
|
|
25
26
|
chat: { id: 101, type: 'private', first_name: 'Fixture' },
|
|
26
27
|
},
|
|
27
28
|
})
|
|
28
|
-
const fixture = async () => {
|
|
29
|
+
const fixture = async (overrides: Partial<Config> = {}) => {
|
|
29
30
|
const dir = await mkdtemp(join(tmpdir(), 'ez-intake-relay-'))
|
|
30
31
|
const launched: string[][] = []
|
|
31
32
|
const replies: string[] = []
|
|
@@ -40,6 +41,7 @@ const fixture = async () => {
|
|
|
40
41
|
executorCli: 'grok' as const,
|
|
41
42
|
telegramBotToken: 'fixture',
|
|
42
43
|
geminiApiKey: 'fixture',
|
|
44
|
+
...overrides,
|
|
43
45
|
}
|
|
44
46
|
const make = () => {
|
|
45
47
|
const relay = createRelay(config, async (texts) => {
|
|
@@ -191,7 +193,7 @@ test('owner group discovery routes only to the private chat and rechecks identit
|
|
|
191
193
|
const run = (await new RunStore(f.dir).list())[0]
|
|
192
194
|
assert.equal(run.chatId, 101)
|
|
193
195
|
assert.equal(run.messageId, undefined)
|
|
194
|
-
assert.
|
|
196
|
+
assert.deepEqual(JSON.parse(run.texts[0]),{event:'owner_message_in_unbound_group',chatId:-101,title:'Family',messageId:3,text:'Hi from the group'})
|
|
195
197
|
assert.match(run.texts[0], /"chatId":-101/)
|
|
196
198
|
assert.equal(f.launched.length, 1)
|
|
197
199
|
await f.relay.bot.handleUpdate(group(4))
|
|
@@ -201,7 +203,7 @@ test('owner group discovery routes only to the private chat and rechecks identit
|
|
|
201
203
|
} finally { await f.close() }
|
|
202
204
|
})
|
|
203
205
|
|
|
204
|
-
test('
|
|
206
|
+
test('three-item menu is owner-only and removes the retired settings control', async () => {
|
|
205
207
|
const f = await fixture()
|
|
206
208
|
const callback = (id: number, data: string, user = 101): Update => ({
|
|
207
209
|
update_id: id,
|
|
@@ -211,32 +213,53 @@ test('four-item menu is owner-only; available AI choices work and forged/stale b
|
|
|
211
213
|
try {
|
|
212
214
|
await f.relay.bot.handleUpdate(message(1, '/menu'))
|
|
213
215
|
assert.deepEqual(f.keyboards.at(-1)!.flat().map((b) => b.text),
|
|
214
|
-
['New conversation', 'Choose AI', 'Work status'
|
|
215
|
-
await f.relay.bot.handleUpdate(message(2, '/
|
|
216
|
-
|
|
216
|
+
['New conversation', 'Choose AI', 'Work status'])
|
|
217
|
+
await f.relay.bot.handleUpdate(message(2, '/settings'))
|
|
218
|
+
assert.match(f.replies.at(-1)!, /Settings was removed.*Use \/ai/)
|
|
219
|
+
await f.relay.bot.handleUpdate(message(3, '/ai'))
|
|
220
|
+
const pick = f.keyboards.at(-1)!.flat().find((button) => button.text === 'Refresh available AIs')!.callback_data
|
|
217
221
|
const store = new ControlStore(f.dir, 1000)
|
|
218
|
-
await
|
|
219
|
-
|
|
220
|
-
await f.relay.bot.handleUpdate(callback(4,
|
|
221
|
-
assert.equal(
|
|
222
|
-
await
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
assert.ok(await store.getActiveSession())
|
|
232
|
-
await f.relay.bot.handleUpdate(callback(8, pick))
|
|
222
|
+
const before = await store.status()
|
|
223
|
+
const keyboardCount = f.keyboards.length
|
|
224
|
+
await f.relay.bot.handleUpdate(callback(4, pick, 202))
|
|
225
|
+
assert.equal(f.keyboards.length, keyboardCount)
|
|
226
|
+
assert.deepEqual(await store.status(), before)
|
|
227
|
+
await f.relay.bot.handleUpdate(callback(5, 'ai:forged'))
|
|
228
|
+
assert.match(f.replies.at(-1)!, /Menu expired/)
|
|
229
|
+
assert.deepEqual(await store.status(), before)
|
|
230
|
+
await f.relay.bot.handleUpdate(callback(6, pick))
|
|
231
|
+
assert.equal(f.keyboards.length, keyboardCount + 1)
|
|
232
|
+
const refreshed = await store.status()
|
|
233
|
+
await f.relay.bot.handleUpdate(callback(7, pick))
|
|
233
234
|
assert.match(f.replies.at(-1)!, /Menu expired/)
|
|
234
|
-
await
|
|
235
|
-
assert.match(f.replies.at(-1)!, /Default for new conversations/)
|
|
235
|
+
assert.deepEqual(await store.status(), refreshed)
|
|
236
236
|
await f.relay.bot.handleUpdate(message(8, '/status'))
|
|
237
237
|
assert.ok(f.keyboards.at(-1)!.flat().some((button) => button.text === 'Scheduled tasks'))
|
|
238
238
|
await f.relay.bot.handleUpdate(callback(9, 'menu:scheduled-tasks'))
|
|
239
|
-
assert.match(f.replies.at(-1)!, /No scheduled tasks for this owner/)
|
|
239
|
+
assert.match(f.replies.at(-1)!, /No active scheduled tasks for this owner/)
|
|
240
|
+
assert.equal(f.launched.length, 0)
|
|
241
|
+
} finally { await f.close() }
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('application-backed channels keep AI and retired settings controls in the application', async () => {
|
|
245
|
+
const f = await fixture({ channelBackendUrl: 'http://127.0.0.1:1', channelBackendToken: 'fixture' })
|
|
246
|
+
try {
|
|
247
|
+
const store = new ControlStore(f.dir, 1000)
|
|
248
|
+
const before = await store.status()
|
|
249
|
+
let id = 1
|
|
250
|
+
for (const text of ['/ai', '/settings', '/new']) {
|
|
251
|
+
await f.relay.bot.handleUpdate(message(id++, text))
|
|
252
|
+
assert.match(f.replies.at(-1)!, /managed in the connected application/)
|
|
253
|
+
}
|
|
254
|
+
for (const data of ['menu:ai', 'menu:settings', 'menu:new', 'ai:old-button']) {
|
|
255
|
+
await f.relay.bot.handleUpdate({ update_id: id, callback_query: {
|
|
256
|
+
id: String(id), chat_instance: 'fixture', data,
|
|
257
|
+
from: { id: 101, first_name: 'Fixture', is_bot: false }, message: message(id++).message!,
|
|
258
|
+
} })
|
|
259
|
+
assert.match(f.replies.at(-1)!, /managed in the connected application/)
|
|
260
|
+
}
|
|
261
|
+
assert.deepEqual(await store.status(), before)
|
|
262
|
+
assert.equal(f.keyboards.length, 0)
|
|
240
263
|
assert.equal(f.launched.length, 0)
|
|
241
264
|
} finally { await f.close() }
|
|
242
265
|
})
|
|
@@ -11,56 +11,31 @@ import { taskArguments } from '../src/task-executor.js'
|
|
|
11
11
|
import { runCodexSession } from '../src/codex-session.js'
|
|
12
12
|
import { runDesktopTurn } from '../src/desktop-bridge.js'
|
|
13
13
|
|
|
14
|
-
test('
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
assert.deepEqual(executionDefaults('codex', { model:'gpt-5.6-luna', effort }), { model:'gpt-5.6-luna', effort })
|
|
31
|
-
assert.throws(() => executionDefaults('grok', { model:'gpt-5.6-luna', effort }), /capped at high/)
|
|
32
|
-
assert.throws(() => executionDefaults('codex', { model:'gpt-5.6-terra', effort }), /capped at high/)
|
|
33
|
-
}
|
|
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','agy']) {
|
|
16
|
+
assert.deepEqual(executionDefaults(cli,{}),{})
|
|
17
|
+
const preset={id:'chosen',name:'Chosen',cli,model:'native-model',effort:'ultra'}
|
|
18
|
+
assert.deepEqual(executionDefaults(cli,preset),preset)
|
|
19
|
+
await validateSelection(preset,[{cli,model:'native-model',name:'Native',efforts:['ultra']}],async()=>true)
|
|
20
|
+
await assert.rejects(validateSelection({...preset,effort:'unsupported'},[{cli,model:'native-model',name:'Native',efforts:['ultra']}],async()=>true),/installed client catalog/)
|
|
21
|
+
assert.throws(()=>executionDefaults(cli,{effort:'bad option'}),/Invalid reasoning effort/)
|
|
22
|
+
}
|
|
23
|
+
assert.deepEqual(executionOverrides('codex',{model:'old',effort:'max'},'new'),{model:'new',effort:undefined})
|
|
24
|
+
const saved={id:'saved',name:'Saved',cli:'codex',model:'chosen',effort:'max'}
|
|
25
|
+
assert.deepEqual(persistedPreset(saved),saved)
|
|
26
|
+
const args=taskArguments('/unused',['broker'],'--literal')
|
|
27
|
+
assert.equal(args.at(-1),'-');assert(!args.includes('--model'));assert(!args.some(s=>s.includes('model_reasoning_effort')))
|
|
28
|
+
const explicit=taskArguments('/unused',['broker'],'text',undefined,saved)
|
|
29
|
+
assert(explicit.includes('chosen'));assert(explicit.includes('model_reasoning_effort="max"'))
|
|
34
30
|
})
|
|
35
31
|
|
|
36
|
-
test('
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
assert.
|
|
42
|
-
|
|
43
|
-
assert.throws(() => taskArguments('/unused', ['broker'], 'prompt', undefined, {effort:'xhigh'}), /capped at high/)
|
|
44
|
-
assert.deepEqual(executionDefaults('codex', {model:'custom-model',effort:'medium'}), {model:'custom-model',effort:'medium'})
|
|
45
|
-
assert.deepEqual(executionDefaults('codex', {model:'gpt-6-astra'}), {model:'gpt-6-astra',effort:'high'})
|
|
46
|
-
assert.deepEqual(executionDefaults('codex', {model:'gpt-5.6-terra'}), {model:'gpt-5.6-terra',effort:'high'})
|
|
47
|
-
assert.deepEqual(executionDefaults('codex', {model:'gpt-5.6-luna'}), {model:'gpt-5.6-luna',effort:'max'})
|
|
48
|
-
assert.deepEqual(executionDefaults('codex', {}), {model:'gpt-5.6-luna',effort:'max'})
|
|
49
|
-
assert.deepEqual(executionOverrides('codex', {model:'gpt-5.6-luna',effort:'max'}, 'gpt-6-astra'), {model:'gpt-6-astra',effort:'high'})
|
|
50
|
-
assert.deepEqual(executionOverrides('codex', {model:'gpt-5.6-luna',effort:'max'}, 'gpt-5.6-luna'), {model:'gpt-5.6-luna',effort:'max'})
|
|
51
|
-
const stored = persistedPreset({id:'luna',name:'Luna',cli:'codex',model:'gpt-5.6-luna',effort:'max'})
|
|
52
|
-
assert.equal(stored.effort, undefined)
|
|
53
|
-
assert.deepEqual(executionDefaults('codex', stored), {id:'luna',name:'Luna',cli:'codex',model:'gpt-5.6-luna',effort:'max'})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('preset persistence rejects above-high choices without changing current settings', async () => {
|
|
57
|
-
const dir = await mkdtemp(join(tmpdir(), 'ez-effort-'))
|
|
58
|
-
try {
|
|
59
|
-
const control = new ControlStore(dir, 1000)
|
|
60
|
-
const before = await control.aiState(initialPreset('codex'))
|
|
61
|
-
await assert.rejects(control.savePreset({id:'bad',name:'Bad',cli:'codex',model:'any',effort:'max'}), /capped at high/)
|
|
62
|
-
assert.deepEqual(await control.aiState(initialPreset('codex')), before)
|
|
63
|
-
} finally { await rm(dir,{recursive:true,force:true}) }
|
|
32
|
+
test('invalid setting syntax never mutates a saved choice', async () => {
|
|
33
|
+
const dir=await mkdtemp(join(tmpdir(),'ez-native-setting-'))
|
|
34
|
+
try {
|
|
35
|
+
const control=new ControlStore(dir,1000),before=await control.aiState(initialPreset('codex'))
|
|
36
|
+
await assert.rejects(control.savePreset({id:'bad',name:'Bad',cli:'codex',effort:'bad option'}),/Invalid AI preset/)
|
|
37
|
+
assert.deepEqual(await control.aiState(initialPreset('codex')),before)
|
|
38
|
+
}finally{await rm(dir,{recursive:true,force:true})}
|
|
64
39
|
})
|
|
65
40
|
|
|
66
41
|
test('non-Codex catalog defaults survive executor normalization and host revalidation', async () => {
|
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import {execFile,spawn} from 'node:child_process';
|
|
7
7
|
import {once} from 'node:events';
|
|
8
8
|
import {promisify} from 'node:util';
|
|
9
|
-
import {snapshot,init as initManager,validate,compose,locked} from '../src/plugins/manager.mjs';
|
|
9
|
+
import {snapshot,init as initManager,validate,compose,locked,bindToolDiscovery} from '../src/plugins/manager.mjs';
|
|
10
10
|
// Synthetic manager tests explicitly opt out of the product's default packages.
|
|
11
11
|
async function init(home,workspace,catalog,hostConfig) {
|
|
12
12
|
const file=path.join(path.dirname(home),'test-catalog.json');
|
|
@@ -53,14 +53,40 @@ test('catalog paths resolve relative to the catalog and pin each new agent indep
|
|
|
53
53
|
assert.notEqual(first.sample.revision,next.catalog.sample.revision);
|
|
54
54
|
assert.deepEqual(JSON.parse((await f.call('plugins','available')).stdout),first);
|
|
55
55
|
});
|
|
56
|
-
test('
|
|
56
|
+
test('native discovery binding preserves notes and never seeds a tool inventory',async t=>{
|
|
57
|
+
const f=await fixture(t),instructions=path.join(f.workspace,'AGENTS.md'),notes=path.join(f.workspace,'TOOLS.md');
|
|
58
|
+
await fs.writeFile(instructions,'Owner mandate\n');
|
|
59
|
+
await init(f.home,f.workspace);
|
|
60
|
+
await assert.rejects(fs.access(notes),{code:'ENOENT'});
|
|
61
|
+
const first=await fs.readFile(instructions,'utf8');assert(first.startsWith('Owner mandate\n'));assert(first.includes(f.home+'/bin/ez'));
|
|
62
|
+
await bindToolDiscovery(f.home,f.workspace);assert.equal(await fs.readFile(instructions,'utf8'),first);
|
|
63
|
+
await fs.writeFile(notes,'Legacy policy');
|
|
64
|
+
await bindToolDiscovery(f.home,f.workspace);assert.equal(await fs.readFile(notes,'utf8'),'Legacy policy');
|
|
65
|
+
await fs.writeFile(path.join(f.workspace,'AGENTS.override.md'),'Override mandate');
|
|
66
|
+
await bindToolDiscovery(f.home,f.workspace);assert.match(await fs.readFile(path.join(f.workspace,'AGENTS.override.md'),'utf8'),/Override mandate/);
|
|
67
|
+
await fs.writeFile(instructions,'<!-- ez tools: begin -->broken');
|
|
68
|
+
await assert.rejects(bindToolDiscovery(f.home,f.workspace),/Malformed/);
|
|
69
|
+
assert.equal(await fs.readFile(instructions,'utf8'),'<!-- ez tools: begin -->broken');
|
|
70
|
+
await fs.rm(instructions);await fs.symlink(notes,instructions);
|
|
71
|
+
await assert.rejects(bindToolDiscovery(f.home,f.workspace),/regular file/);
|
|
72
|
+
assert.equal(await fs.readFile(notes,'utf8'),'Legacy policy');
|
|
73
|
+
});
|
|
74
|
+
test('installed snippets follow install, upgrade and uninstall without files or Docker reads',async t=>{
|
|
57
75
|
const f=await fixture(t);await init(f.home,f.workspace);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await
|
|
63
|
-
|
|
76
|
+
const details=async()=>JSON.parse((await f.call('tools','list','--details')).stdout);
|
|
77
|
+
assert.deepEqual(await details(),{});
|
|
78
|
+
for(const description of ['Synthetic capability','Updated capability']) {
|
|
79
|
+
await fs.writeFile(path.join(f.source,'ez-plugin.json'),JSON.stringify({...f.manifest,description}));
|
|
80
|
+
const p=await snapshot(f.source);
|
|
81
|
+
await f.call('plugins','install','sample','--source',f.source,'--revision',p.revision);
|
|
82
|
+
const before=await fs.readFile(f.log,'utf8'),index=await details();
|
|
83
|
+
assert.equal(index.sample.description,description);assert.deepEqual(index.sample.commands,['ez sample --help']);
|
|
84
|
+
assert.equal(await fs.readFile(index.sample.skills[0],'utf8'),'Synthetic');
|
|
85
|
+
assert.equal(await fs.readFile(f.log,'utf8'),before);
|
|
86
|
+
assert.deepEqual(JSON.parse((await f.call('tools','list')).stdout),{sample:'sample'});
|
|
87
|
+
await f.call('plugins','uninstall','sample');assert.deepEqual(await details(),{});
|
|
88
|
+
}
|
|
89
|
+
await assert.rejects(fs.access(path.join(f.workspace,'TOOLS.md')),{code:'ENOENT'});
|
|
64
90
|
});
|
|
65
91
|
test('bound launcher installs without startup; literal args and exit codes; scopes and secrets',async t=>{
|
|
66
92
|
const f=await fixture(t);const p=await snapshot(f.source);await init(f.home,f.workspace);
|
|
@@ -229,8 +255,8 @@ test('standalone CLI has discoverable setup, independent guidance and status wit
|
|
|
229
255
|
const help=JSON.parse((await exec(process.execPath,[bin,'--help'])).stdout);
|
|
230
256
|
assert.match(help.usage,/--standalone/);
|
|
231
257
|
await exec(process.execPath,[bin,'init','--standalone','--home',f.home,'--workspace',f.workspace],{env:f.env});
|
|
232
|
-
const notes=await fs.readFile(path.join(f.workspace,'
|
|
233
|
-
assert.match(notes,/
|
|
258
|
+
const notes=await fs.readFile(path.join(f.workspace,'AGENTS.md'),'utf8');
|
|
259
|
+
assert.match(notes,/tools list --details/);
|
|
234
260
|
assert.doesNotMatch(notes,/Finish the main Telegram|ezenciel-agents-message/);
|
|
235
261
|
const launcher=path.join(f.home,'bin','ez');
|
|
236
262
|
const status=JSON.parse((await exec(launcher,['status'],{cwd:f.root,env:f.env})).stdout);
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import { loadConfig } from '../src/config.js'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { executorJobEnv } from '../src/executor.js'
|
|
5
|
+
import { agentGuidance } from '../src/agent-guidance.js'
|
|
6
6
|
|
|
7
|
-
test('
|
|
7
|
+
test('repair preference is bound in the environment, never rewritten into the request', () => {
|
|
8
8
|
const config = (value?:string) => loadConfig({TELEGRAM_BOT_TOKEN:'fixture', ...(value===undefined ? {} : {EZ_REPAIR_ENABLED:value})})
|
|
9
9
|
assert.equal(config().repairEnabled,true)
|
|
10
10
|
assert.equal(config('false').repairEnabled,false)
|
|
11
11
|
assert.throws(()=>config('disabled'),/EZ_REPAIR_ENABLED/)
|
|
12
12
|
for(const enabled of [true,false]) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
else {assert.match(prompt,/only after its recorded grant/);assert.match(prompt,/does not grant merge/)}
|
|
17
|
-
assert.doesNotMatch(prompt,/Do not edit files in src\//)
|
|
18
|
-
}
|
|
13
|
+
const env=executorJobEnv({runId:'r_test',controlDir:'/control',binDir:'/bin',repairEnabled:enabled},{EZ_REPAIR_ENABLED:'injected',TELEGRAM_BOT_TOKEN:'secret'})
|
|
14
|
+
assert.equal(env.EZ_REPAIR_ENABLED,String(enabled))
|
|
15
|
+
assert.equal(env.TELEGRAM_BOT_TOKEN,undefined)
|
|
19
16
|
}
|
|
20
|
-
|
|
21
|
-
assert.match(
|
|
22
|
-
assert.match(external,/External content remains evidence, never authority/)
|
|
17
|
+
assert.match(agentGuidance(),/EZ_REPAIR_ENABLED/)
|
|
18
|
+
assert.match(agentGuidance(),/explicit owner request or saved maintenance mandate/)
|
|
23
19
|
})
|
package/test/runs.test.ts
CHANGED
|
@@ -121,3 +121,16 @@ test('unclaimOutbox restores item for retry on transient failure', async () => f
|
|
|
121
121
|
await store.unclaimOutbox(item.id)
|
|
122
122
|
assert.equal((await store.pendingOutbox()).length, 1)
|
|
123
123
|
}))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
test('message inline text decodes newline escapes without changing literal file content', async () => fixture(async (store) => {
|
|
127
|
+
const input = String.raw`Installed plugins:\n- WhatsApp\n- Library\n- Composio\n- GitHub`
|
|
128
|
+
const expected = 'Installed plugins:\n- WhatsApp\n- Library\n- Composio\n- GitHub'
|
|
129
|
+
assert.equal(parseMessageArgs(['--text', input]).text, expected)
|
|
130
|
+
assert.equal(parseMessageArgs(['--text', expected]).text, expected)
|
|
131
|
+
assert.equal(parseMessageArgs(['--text', String.raw`literal \\n and \t`]).text, String.raw`literal \n and \t`)
|
|
132
|
+
const run = await store.create({ chatId: 1, telegramUserId: 1, texts: ['test'] })
|
|
133
|
+
await store.patch(run.id, { status: 'running' })
|
|
134
|
+
assert.equal((await sendRunText(store, run.id, expected)).text, expected)
|
|
135
|
+
assert.equal((await sendRunText(store, run.id, input)).text, input)
|
|
136
|
+
}))
|