@agfpd/iapeer 0.2.77 → 0.2.78
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/LICENSE +202 -0
- package/README.md +2 -2
- package/package.json +21 -2
- package/src/cli/index.ts +47 -48
- package/src/connect/index.ts +4 -4
- package/src/core/constants.ts +3 -3
- package/src/core/normalize.ts +2 -2
- package/src/create/index.ts +1 -1
- package/src/daemon/index.ts +5 -5
- package/src/daemon/main.ts +9 -9
- package/src/enable/index.ts +19 -18
- package/src/enable/provisionCommand.ts +10 -10
- package/src/identity/index.ts +2 -2
- package/src/identity/profileStandard.ts +8 -8
- package/src/init/index.ts +10 -10
- package/src/install/index.ts +2 -2
- package/src/install/signing.ts +3 -4
- package/src/launch/adapters/claude.ts +7 -7
- package/src/launch/adapters/codex.ts +5 -6
- package/src/launch/canary.ts +8 -8
- package/src/launch/cmdlog.ts +3 -3
- package/src/launch/composeSystemPrompt.ts +2 -1
- package/src/launch/index.ts +5 -6
- package/src/launch/invocation.ts +1 -1
- package/src/launch/launchd.ts +14 -15
- package/src/launch/launchdRun.ts +6 -6
- package/src/launch/nativeMemory.ts +9 -9
- package/src/launch/ptyHost.ts +1 -1
- package/src/launch/readyGateModel.ts +3 -3
- package/src/launch/types.ts +7 -7
- package/src/lifecycle/eventlog.ts +2 -2
- package/src/lifecycle/index.ts +21 -21
- package/src/lifecycle/queue.ts +1 -1
- package/src/onboard/index.ts +19 -13
- package/src/onboard/memory.ts +5 -5
- package/src/onboard/steps.ts +8 -9
- package/src/provision/index.ts +2 -3
- package/src/runtime/deploy.ts +2 -2
- package/src/runtime/index.ts +4 -4
- package/src/shadow/gate.ts +6 -6
- package/src/shadow/index.ts +2 -2
- package/src/status/index.ts +11 -11
- package/src/storage/index.ts +1 -1
- package/src/supervisor/daemon.ts +14 -8
- package/src/transport/index.ts +11 -11
- package/src/update/index.ts +4 -4
- package/src/cli/cli.test.ts +0 -603
- package/src/cli/listTui.test.ts +0 -71
- package/src/codec/codec.test.ts +0 -271
- package/src/connect/connect.test.ts +0 -163
- package/src/core/constants.test.ts +0 -32
- package/src/core/normalize.test.ts +0 -98
- package/src/create/create.test.ts +0 -143
- package/src/daemon/daemon-http.test.ts +0 -114
- package/src/daemon/daemon.test.ts +0 -267
- package/src/daemon/main.test.ts +0 -413
- package/src/enable/enable.test.ts +0 -95
- package/src/enable/provisionCommand.test.ts +0 -107
- package/src/identity/identity.test.ts +0 -279
- package/src/identity/profileStandard.test.ts +0 -204
- package/src/init/init.test.ts +0 -215
- package/src/init/runtime-resolve.test.ts +0 -49
- package/src/install/install.test.ts +0 -66
- package/src/install/signing.test.ts +0 -84
- package/src/launch/bootdeliver.test.ts +0 -108
- package/src/launch/bootstrap.test.ts +0 -56
- package/src/launch/canary.test.ts +0 -346
- package/src/launch/cmdlog.test.ts +0 -94
- package/src/launch/codexHooksTrust.test.ts +0 -248
- package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
- package/src/launch/composeSystemPrompt.test.ts +0 -145
- package/src/launch/hostComposerOccupied.test.ts +0 -63
- package/src/launch/invocation.test.ts +0 -174
- package/src/launch/launch.test.ts +0 -359
- package/src/launch/launchd.test.ts +0 -499
- package/src/launch/nativeMemory.test.ts +0 -193
- package/src/launch/ptyHost.test.ts +0 -38
- package/src/launch/readyGateModel.test.ts +0 -72
- package/src/launch/sockdir.test.ts +0 -74
- package/src/lifecycle/eventlog.test.ts +0 -114
- package/src/lifecycle/lifecycle.test.ts +0 -1056
- package/src/lifecycle/queue.test.ts +0 -185
- package/src/onboard/memory.test.ts +0 -157
- package/src/onboard/onboard.test.ts +0 -39
- package/src/onboard/steps.test.ts +0 -201
- package/src/provision/provision.test.ts +0 -288
- package/src/registry/registry.test.ts +0 -432
- package/src/runtime/runtime.test.ts +0 -226
- package/src/runtime/update.test.ts +0 -192
- package/src/shadow/enumerate-skip.test.ts +0 -67
- package/src/shadow/gate.test.ts +0 -286
- package/src/shadow/install.test.ts +0 -153
- package/src/shadow/shadow.test.ts +0 -42
- package/src/status/status.test.ts +0 -208
- package/src/storage/peers-home.test.ts +0 -34
- package/src/storage/rotatelog.test.ts +0 -44
- package/src/storage/storage.test.ts +0 -65
- package/src/supervisor/attached-marker.test.ts +0 -84
- package/src/supervisor/boot.test.ts +0 -66
- package/src/supervisor/client.attach.test.ts +0 -107
- package/src/supervisor/daemon.test.ts +0 -213
- package/src/supervisor/deliver.test.ts +0 -58
- package/src/supervisor/panelog.test.ts +0 -95
- package/src/supervisor/protocol.test.ts +0 -222
- package/src/supervisor/selfargv.test.ts +0 -22
- package/src/supervisor/spinner-strip.test.ts +0 -52
- package/src/transport/compactDone.test.ts +0 -144
- package/src/transport/composerQueue.test.ts +0 -204
- package/src/transport/hostControl.test.ts +0 -87
- package/src/transport/hostDeliver.test.ts +0 -137
- package/src/transport/hostLive.test.ts +0 -50
- package/src/transport/routesend-wake.test.ts +0 -357
- package/src/update/update.test.ts +0 -208
package/src/cli/cli.test.ts
DELETED
|
@@ -1,603 +0,0 @@
|
|
|
1
|
-
// iapeer CLI verbs — list / stop / start (C1 + fleet guard) / send validation.
|
|
2
|
-
// No tmux: killSession on a non-existent session is a no-op, the C1 flag is the
|
|
3
|
-
// observable. The FLEET GUARD (H4) is the safety-critical case — a foreign
|
|
4
|
-
// persistent-peer launchd plist must be refused.
|
|
5
|
-
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
7
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
8
|
-
import { tmpdir } from 'os'
|
|
9
|
-
import { join } from 'path'
|
|
10
|
-
import { addRuntime, compactPeer, defaultRuntime, formatListTable, listPeers, newPeer, parseArgs, removePeerCli, runCli, sendMessage, startPeer, stopPeer } from './index.ts'
|
|
11
|
-
import { findPeer, readPeersIndex, upsertPeer } from '../registry/index.ts'
|
|
12
|
-
import { hasIdleReaped, isStopped, loadLifecycleConfig, setIdleReaped, setStopped } from '../lifecycle/index.ts'
|
|
13
|
-
import { launchdPlistPath } from '../launch/launchd.ts'
|
|
14
|
-
import { err, ok } from '../core/errors.ts'
|
|
15
|
-
|
|
16
|
-
let root: string
|
|
17
|
-
let laDir: string
|
|
18
|
-
function env(): NodeJS.ProcessEnv {
|
|
19
|
-
const e: NodeJS.ProcessEnv = {
|
|
20
|
-
...process.env,
|
|
21
|
-
IAPEER_ROOT: root,
|
|
22
|
-
IAPEER_LAUNCHAGENTS_DIR: laDir,
|
|
23
|
-
IAPEER_SOCK_DIR: join(root, 'socks'),
|
|
24
|
-
}
|
|
25
|
-
delete e.PEER_PERSONALITY
|
|
26
|
-
delete e.PEER_IDENTITY
|
|
27
|
-
delete e.PEER_RUNTIME
|
|
28
|
-
return e
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-cli-root-'))
|
|
33
|
-
laDir = mkdtempSync(join(tmpdir(), 'iapeer-cli-la-'))
|
|
34
|
-
})
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
rmSync(root, { recursive: true, force: true })
|
|
37
|
-
rmSync(laDir, { recursive: true, force: true })
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
async function register(personality: string, runtime = 'claude', intelligence: 'artificial' | 'natural' | 'absent' = 'artificial'): Promise<void> {
|
|
41
|
-
await upsertPeer({ personality, runtime, cwd: `/tmp/${personality}`, intelligence }, { rootDir: root })
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ─── v1.2 provision-инверсия at the CLI joints (remove → unprovision) ─────────
|
|
45
|
-
// NB: the memory-plugin verb was REMOVED 11.06 (ADR-017 — the plugin form is
|
|
46
|
-
// retired ecosystem-wide; fleet sweeps are the provider's side). The core's
|
|
47
|
-
// remaining v1.2 joints are birth (provision.test.ts) and remove (below).
|
|
48
|
-
|
|
49
|
-
describe('remove with a v1.2 provision slot', () => {
|
|
50
|
-
function declareV12Slot(): string {
|
|
51
|
-
const journal = join(root, 'journal.txt')
|
|
52
|
-
const script = join(root, 'fake-provider.sh')
|
|
53
|
-
writeFileSync(script, `#!/bin/sh\nprintf '%s\\n' "$@" >> '${journal}'\n`, { mode: 0o755 })
|
|
54
|
-
writeFileSync(
|
|
55
|
-
join(root, 'memory-provider.json'),
|
|
56
|
-
JSON.stringify({
|
|
57
|
-
provider: 'fake-mem',
|
|
58
|
-
package: '@x/fake',
|
|
59
|
-
version: '0.0.1',
|
|
60
|
-
registeredAt: 'x',
|
|
61
|
-
provision: { command: script, args: ['provision-peer', '--occasion', '{occasion}'] },
|
|
62
|
-
unprovision: { command: script, args: ['unprovision-peer', '--cwd', '{cwd}', '--occasion', '{occasion}'] },
|
|
63
|
-
}),
|
|
64
|
-
)
|
|
65
|
-
return journal
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
test('memory-plugin verb is GONE — unknown verb exits with usage (removal 11.06)', async () => {
|
|
69
|
-
const code = await runCli(['memory-plugin', 'on', '--all'], env())
|
|
70
|
-
expect(code).toBe(2) // usage exit — the verb no longer exists
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('remove: unprovision runs with occasion=remove BEFORE the purge, outcome reported; cmdlog dir dies with the record', async () => {
|
|
74
|
-
const journal = declareV12Slot()
|
|
75
|
-
await register('beta')
|
|
76
|
-
// identity-keyed tmux command-log dir (0.2.40) — must die with the registry
|
|
77
|
-
// record (the leak class noted at the 0.2.40 acceptance)
|
|
78
|
-
const { mkdirSync: mkd } = await import('fs')
|
|
79
|
-
const cmdDir = join(root, 'logs', 'iapeer', 'tmux-cmdlog', 'claude-beta')
|
|
80
|
-
mkd(cmdDir, { recursive: true })
|
|
81
|
-
writeFileSync(join(cmdDir, 'tmux-server-1.log'), 'x')
|
|
82
|
-
const o = await removePeerCli('beta', { env: env() })
|
|
83
|
-
expect(o.action).toBe('removed')
|
|
84
|
-
expect(o.unprovision).toEqual(['claude:ok'])
|
|
85
|
-
const { readFileSync, existsSync: ex } = await import('fs')
|
|
86
|
-
const j = readFileSync(journal, 'utf8')
|
|
87
|
-
expect(j).toContain('unprovision-peer')
|
|
88
|
-
expect(j).toContain('--cwd\n/tmp/beta')
|
|
89
|
-
expect(j).toContain('remove')
|
|
90
|
-
expect(ex(cmdDir)).toBe(false)
|
|
91
|
-
expect(o.purgedState).toContain('tmux-cmdlog/claude-beta')
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
describe('list', () => {
|
|
96
|
-
test('lists registered peers with per-runtime liveness (asleep / stopped)', async () => {
|
|
97
|
-
await register('alpha')
|
|
98
|
-
await register('beta')
|
|
99
|
-
const e = env()
|
|
100
|
-
setStopped(loadLifecycleConfig(e), 'claude-beta')
|
|
101
|
-
const rows = listPeers({ env: e })
|
|
102
|
-
const alpha = rows.find(r => r.personality === 'alpha')!
|
|
103
|
-
const beta = rows.find(r => r.personality === 'beta')!
|
|
104
|
-
expect(alpha.runtimes[0]).toEqual({ runtime: 'claude', status: 'asleep' })
|
|
105
|
-
expect(beta.runtimes[0]).toEqual({ runtime: 'claude', status: 'stopped' })
|
|
106
|
-
expect(formatListTable(rows)).toContain('alpha')
|
|
107
|
-
expect(formatListTable(rows)).toContain('✕ claude') // stopped glyph for beta
|
|
108
|
-
})
|
|
109
|
-
test('empty registry → friendly message', () => {
|
|
110
|
-
expect(formatListTable(listPeers({ env: env() }))).toBe('no peers registered\n')
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe('stop / start (C1 durable flag, warm runtime)', () => {
|
|
115
|
-
test('stop sets the durable flag; start clears it', async () => {
|
|
116
|
-
await register('gamma')
|
|
117
|
-
const e = env()
|
|
118
|
-
const cfg = loadLifecycleConfig(e)
|
|
119
|
-
expect(isStopped(cfg, 'claude-gamma')).toBe(false)
|
|
120
|
-
expect(stopPeer('gamma', undefined, { env: e })[0].action).toBe('stopped')
|
|
121
|
-
expect(isStopped(cfg, 'claude-gamma')).toBe(true)
|
|
122
|
-
expect(startPeer('gamma', undefined, { env: e })[0].action).toBe('started')
|
|
123
|
-
expect(isStopped(cfg, 'claude-gamma')).toBe(false)
|
|
124
|
-
})
|
|
125
|
-
test('stop an unregistered peer → throws', () => {
|
|
126
|
-
expect(() => stopPeer('nobody', undefined, { env: env() })).toThrow(/not registered/)
|
|
127
|
-
})
|
|
128
|
-
test('stop is a CLEAN PARK: leaves the resume marker and drops the session-state (boris 10.06)', async () => {
|
|
129
|
-
await register('parked')
|
|
130
|
-
const e = env()
|
|
131
|
-
const cfg = loadLifecycleConfig(e)
|
|
132
|
-
// a live session-state the daemon would otherwise tag as a death post-kill
|
|
133
|
-
mkdirSync(cfg.stateDir, { recursive: true })
|
|
134
|
-
writeFileSync(
|
|
135
|
-
join(cfg.stateDir, 'claude-parked.session'),
|
|
136
|
-
JSON.stringify({ identity: 'claude-parked', runtime: 'claude', personality: 'parked', cwd: '/tmp/parked', wokeAt: Date.now() }),
|
|
137
|
-
)
|
|
138
|
-
stopPeer('parked', undefined, { env: e })
|
|
139
|
-
expect(hasIdleReaped(cfg, 'claude-parked')).toBe(true) // park marker → post-start wake RESUMES
|
|
140
|
-
expect(existsSync(join(cfg.stateDir, 'claude-parked.session'))).toBe(false) // not a death for supervise
|
|
141
|
-
// start clears only the stop flag — the park marker survives for the wake
|
|
142
|
-
startPeer('parked', undefined, { env: e })
|
|
143
|
-
expect(isStopped(cfg, 'claude-parked')).toBe(false)
|
|
144
|
-
expect(hasIdleReaped(cfg, 'claude-parked')).toBe(true)
|
|
145
|
-
})
|
|
146
|
-
test('start --all clears the flag of EVERY registered peer; unregistered garbage untouched (incident 11.06)', async () => {
|
|
147
|
-
await register('p1')
|
|
148
|
-
await register('p2')
|
|
149
|
-
const e = env()
|
|
150
|
-
const cfg = loadLifecycleConfig(e)
|
|
151
|
-
// a stopped fleet …
|
|
152
|
-
setStopped(cfg, 'claude-p1')
|
|
153
|
-
setStopped(cfg, 'claude-p2')
|
|
154
|
-
// … plus a stray stop-flag for an identity NO registry record claims
|
|
155
|
-
setStopped(cfg, 'claude-ghost')
|
|
156
|
-
expect(await runCli(['start', '--all'], e)).toBe(0)
|
|
157
|
-
expect(isStopped(cfg, 'claude-p1')).toBe(false)
|
|
158
|
-
expect(isStopped(cfg, 'claude-p2')).toBe(false)
|
|
159
|
-
expect(isStopped(cfg, 'claude-ghost')).toBe(true) // unregistered → never enumerated, never touched
|
|
160
|
-
})
|
|
161
|
-
test('start without a peer and without --all → usage, nothing executed', async () => {
|
|
162
|
-
await register('p3')
|
|
163
|
-
const e = env()
|
|
164
|
-
const cfg = loadLifecycleConfig(e)
|
|
165
|
-
setStopped(cfg, 'claude-p3')
|
|
166
|
-
expect(await runCli(['start'], e)).toBe(2)
|
|
167
|
-
expect(isStopped(cfg, 'claude-p3')).toBe(true) // flag untouched — usage only
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
describe('FLEET GUARD (H4) — foreign persistent-peer launchd plist is off-limits', () => {
|
|
172
|
-
test('stop refuses a peer whose com.iapeer.<p>.plist is NOT foundation-owned', async () => {
|
|
173
|
-
await register('boris')
|
|
174
|
-
// a live PP peer owns com.iapeer.boris.plist WITHOUT the foundation sentinel
|
|
175
|
-
writeFileSync(launchdPlistPath('boris', env()), '<?xml version="1.0"?>\n<plist><dict><key>Label</key><string>com.iapeer.boris</string></dict></plist>\n')
|
|
176
|
-
const e = env()
|
|
177
|
-
const out = stopPeer('boris', undefined, { env: e })
|
|
178
|
-
expect(out[0].action).toBe('refused-foreign-launchd')
|
|
179
|
-
// the durable flag was NOT set — the foundation did not touch the PP peer
|
|
180
|
-
expect(isStopped(loadLifecycleConfig(e), 'claude-boris')).toBe(false)
|
|
181
|
-
// start likewise refuses
|
|
182
|
-
expect(startPeer('boris', undefined, { env: e })[0].action).toBe('refused-foreign-launchd')
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
describe('remove (registry record via the locked writer)', () => {
|
|
187
|
-
test('removes a registered peer through registry.removePeer', async () => {
|
|
188
|
-
await register('zombie')
|
|
189
|
-
const e = env()
|
|
190
|
-
expect(findPeer(readPeersIndex({ env: e }), 'zombie')).not.toBeNull()
|
|
191
|
-
const o = await removePeerCli('zombie', { env: e })
|
|
192
|
-
expect(o.action).toBe('removed')
|
|
193
|
-
expect(findPeer(readPeersIndex({ env: e }), 'zombie')).toBeNull()
|
|
194
|
-
// the folder is deliberately KEPT; the outcome carries the cwd so the verb can
|
|
195
|
-
// say so instead of leaving silent orphans (boris 10.06)
|
|
196
|
-
expect(o.cwd).toBe('/tmp/zombie')
|
|
197
|
-
})
|
|
198
|
-
test('removing an absent peer is an idempotent no-op (not an error)', async () => {
|
|
199
|
-
const o = await removePeerCli('never-existed', { env: env() })
|
|
200
|
-
expect(o.action).toBe('absent')
|
|
201
|
-
})
|
|
202
|
-
test('a second remove of the same peer is also a no-op', async () => {
|
|
203
|
-
await register('twice')
|
|
204
|
-
const e = env()
|
|
205
|
-
expect((await removePeerCli('twice', { env: e })).action).toBe('removed')
|
|
206
|
-
expect((await removePeerCli('twice', { env: e })).action).toBe('absent')
|
|
207
|
-
})
|
|
208
|
-
test('self-done arms the caller\'s own quiet-reap (non-waking silent finish); refuses without PEER_IDENTITY', async () => {
|
|
209
|
-
const e = env()
|
|
210
|
-
// no PEER_IDENTITY → self-call refusal
|
|
211
|
-
expect(await runCli(['self-done'], e)).toBe(1)
|
|
212
|
-
// with PEER_IDENTITY → marker set, exit 0, nobody contacted
|
|
213
|
-
const e2 = { ...e, PEER_IDENTITY: 'claude-silentworker' }
|
|
214
|
-
expect(await runCli(['self-done'], e2)).toBe(0)
|
|
215
|
-
const { hasEphemeralArmed } = await import('../lifecycle/index.ts')
|
|
216
|
-
expect(hasEphemeralArmed(loadLifecycleConfig(e2), 'claude-silentworker')).toBe(true)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test('self-done ephemeral check keys on the CANONICAL registry cwd, not process.cwd() (live false-warning, scriber 11.06)', async () => {
|
|
220
|
-
// an ephemeral peer registered with a cwd carrying wake_policy:ephemeral
|
|
221
|
-
const cwd = join(root, 'worker')
|
|
222
|
-
mkdirSync(join(cwd, '.iapeer'), { recursive: true })
|
|
223
|
-
writeFileSync(join(cwd, '.iapeer', 'peer-profile.json'), JSON.stringify({ personality: 'worker', runtime: 'claude', wake_policy: 'ephemeral' }))
|
|
224
|
-
await upsertPeer({ personality: 'worker', runtime: 'claude', cwd, intelligence: 'artificial' }, { rootDir: root })
|
|
225
|
-
const e = { ...env(), PEER_IDENTITY: 'claude-worker' }
|
|
226
|
-
let captured = ''
|
|
227
|
-
const origWrite = process.stdout.write
|
|
228
|
-
process.stdout.write = ((s: string | Uint8Array) => {
|
|
229
|
-
captured += typeof s === 'string' ? s : Buffer.from(s).toString('utf8')
|
|
230
|
-
return true
|
|
231
|
-
}) as typeof process.stdout.write
|
|
232
|
-
try {
|
|
233
|
-
// invoked from THIS test process's cwd (≠ peer cwd) — the warning must NOT fire
|
|
234
|
-
expect(await runCli(['self-done'], e)).toBe(0)
|
|
235
|
-
expect(captured).toContain('armed claude-worker')
|
|
236
|
-
expect(captured).not.toContain('marker is inert')
|
|
237
|
-
} finally {
|
|
238
|
-
process.stdout.write = origWrite
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test('purges identity-keyed lifecycle state with the record — a namesake newborn must not inherit a dead peer\'s parking (boris 10.06)', async () => {
|
|
243
|
-
await register('reborn')
|
|
244
|
-
const e = env()
|
|
245
|
-
const cfg = loadLifecycleConfig(e)
|
|
246
|
-
// the dead peer left the full marker cemetery behind (the live-defect shape:
|
|
247
|
-
// .stopped + .idle-reaped → a namesake newborn is REFUSED its wake)
|
|
248
|
-
setStopped(cfg, 'claude-reborn')
|
|
249
|
-
setIdleReaped(cfg, 'claude-reborn')
|
|
250
|
-
writeFileSync(join(cfg.stateDir, 'claude-reborn.topic'), 'old-topic')
|
|
251
|
-
mkdirSync(join(cfg.stateDir, 'claude-reborn.queue'), { recursive: true })
|
|
252
|
-
// a NEIGHBOR identity sharing the name as a PREFIX must survive untouched
|
|
253
|
-
setStopped(cfg, 'claude-reborn2')
|
|
254
|
-
|
|
255
|
-
const o = await removePeerCli('reborn', { env: e })
|
|
256
|
-
expect(o.action).toBe('removed')
|
|
257
|
-
expect(o.purgedState?.sort()).toEqual([
|
|
258
|
-
'claude-reborn.idle-reaped',
|
|
259
|
-
'claude-reborn.queue',
|
|
260
|
-
'claude-reborn.stopped',
|
|
261
|
-
'claude-reborn.topic',
|
|
262
|
-
])
|
|
263
|
-
expect(isStopped(cfg, 'claude-reborn')).toBe(false) // the newborn namesake wakes
|
|
264
|
-
expect(existsSync(join(cfg.stateDir, 'claude-reborn.queue'))).toBe(false)
|
|
265
|
-
expect(isStopped(cfg, 'claude-reborn2')).toBe(true) // dot-delimited: no prefix bleed
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
describe('send validation', () => {
|
|
270
|
-
test('invalid --from identity → throws', async () => {
|
|
271
|
-
await register('alpha')
|
|
272
|
-
await expect(
|
|
273
|
-
sendMessage({ from: 'notanidentity', target: 'alpha', message: 'hi', env: env() }),
|
|
274
|
-
).rejects.toThrow(/invalid/)
|
|
275
|
-
})
|
|
276
|
-
test('unknown --from caller → throws (not registered)', async () => {
|
|
277
|
-
await register('alpha')
|
|
278
|
-
await expect(
|
|
279
|
-
sendMessage({ from: 'claude-ghost', target: 'alpha', message: 'hi', env: env() }),
|
|
280
|
-
).rejects.toThrow(/unknown caller|not registered/)
|
|
281
|
-
})
|
|
282
|
-
test('target not in registry → throws before any wake', async () => {
|
|
283
|
-
await register('alpha')
|
|
284
|
-
await expect(
|
|
285
|
-
sendMessage({ from: 'claude-alpha', target: 'nobody', message: 'hi', env: env() }),
|
|
286
|
-
).rejects.toThrow(/not in the iapeer peers index|self/)
|
|
287
|
-
})
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
describe('send → ephemeral target: M3 FIFO parity with the daemon path (iapeer-memory ask)', () => {
|
|
291
|
-
test('a CLI send to a wake_policy:ephemeral peer ENQUEUES (queued ack), no live/miss bypass', async () => {
|
|
292
|
-
// an ephemeral worker cwd (profile declares wake_policy) registered in the index
|
|
293
|
-
const cwd = mkdtempSync(join(tmpdir(), 'iapeer-cli-eph-'))
|
|
294
|
-
mkdirSync(join(cwd, '.iapeer'), { recursive: true })
|
|
295
|
-
writeFileSync(
|
|
296
|
-
join(cwd, '.iapeer', 'peer-profile.json'),
|
|
297
|
-
JSON.stringify({ personality: 'ephw', runtime: 'claude', runtimes: ['claude'], intelligence: 'artificial', wake_policy: 'ephemeral' }),
|
|
298
|
-
)
|
|
299
|
-
const e = env()
|
|
300
|
-
// routeSend resolves the peers index from the PROCESS env (transport reads
|
|
301
|
-
// readPeersIndex() bare) — point the process-level root at the sandbox too.
|
|
302
|
-
const prevRoot = process.env.IAPEER_ROOT
|
|
303
|
-
process.env.IAPEER_ROOT = root
|
|
304
|
-
try {
|
|
305
|
-
await upsertPeer({ personality: 'ephw', runtime: 'claude', cwd, intelligence: 'artificial' }, { rootDir: root })
|
|
306
|
-
await register('sender')
|
|
307
|
-
const r = await sendMessage({ from: 'claude-sender', target: 'ephw', message: 'task', env: e })
|
|
308
|
-
expect(r.queued).toBe(true) // serialized via the disk FIFO, exactly like the daemon path
|
|
309
|
-
expect(r.queueDepth).toBe(1)
|
|
310
|
-
// the task is durably on disk for the daemon tick to drain
|
|
311
|
-
const qdir = join(loadLifecycleConfig(e).stateDir, 'claude-ephw.queue')
|
|
312
|
-
expect(existsSync(qdir)).toBe(true)
|
|
313
|
-
} finally {
|
|
314
|
-
if (prevRoot !== undefined) process.env.IAPEER_ROOT = prevRoot
|
|
315
|
-
else delete process.env.IAPEER_ROOT
|
|
316
|
-
rmSync(cwd, { recursive: true, force: true })
|
|
317
|
-
}
|
|
318
|
-
})
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
describe('--help/-h global intercept (CLI hygiene — usage printed, NOTHING executed)', () => {
|
|
322
|
-
let captured: string
|
|
323
|
-
let origWrite: typeof process.stdout.write
|
|
324
|
-
beforeEach(() => {
|
|
325
|
-
captured = ''
|
|
326
|
-
origWrite = process.stdout.write
|
|
327
|
-
process.stdout.write = ((s: string | Uint8Array) => {
|
|
328
|
-
captured += typeof s === 'string' ? s : Buffer.from(s).toString('utf8')
|
|
329
|
-
return true
|
|
330
|
-
}) as typeof process.stdout.write
|
|
331
|
-
})
|
|
332
|
-
afterEach(() => {
|
|
333
|
-
process.stdout.write = origWrite
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
test('every verb with --help prints usage to stdout and exits 0', async () => {
|
|
337
|
-
// The full verb surface — including verbs with REAL side effects (onboard ran on
|
|
338
|
-
// prod swallowing --help; stop would set a durable flag; remove would delete).
|
|
339
|
-
const verbs = [
|
|
340
|
-
'onboard', 'install', 'update', 'rollback', 'version', 'daemon', 'status',
|
|
341
|
-
'install-runtime', 'init', 'create', 'list', 'stop', 'start', 'remove', 'send',
|
|
342
|
-
'enable', 'attach', 'interrupt', 'compact', 'self-fresh', 'native-memory', 'run-infra',
|
|
343
|
-
]
|
|
344
|
-
for (const v of verbs) {
|
|
345
|
-
captured = ''
|
|
346
|
-
const code = await runCli([v, '--help'], env())
|
|
347
|
-
expect({ verb: v, code }).toEqual({ verb: v, code: 0 })
|
|
348
|
-
expect(captured).toContain('usage: iapeer')
|
|
349
|
-
}
|
|
350
|
-
})
|
|
351
|
-
test('-h works like --help, anywhere on the line', async () => {
|
|
352
|
-
expect(await runCli(['stop', 'somebody', '-h'], env())).toBe(0)
|
|
353
|
-
expect(captured).toContain('usage: iapeer')
|
|
354
|
-
})
|
|
355
|
-
test('bare `iapeer --help` / `-h` / `help` print usage', async () => {
|
|
356
|
-
for (const a of [['--help'], ['-h'], ['help']]) {
|
|
357
|
-
captured = ''
|
|
358
|
-
expect(await runCli(a, env())).toBe(0)
|
|
359
|
-
expect(captured).toContain('usage: iapeer')
|
|
360
|
-
}
|
|
361
|
-
})
|
|
362
|
-
test('--help does NOT execute the verb: `stop <peer> --help` leaves no durable stop flag', async () => {
|
|
363
|
-
await register('helpcheck')
|
|
364
|
-
const e = env()
|
|
365
|
-
expect(await runCli(['stop', 'helpcheck', '--help'], e)).toBe(0)
|
|
366
|
-
expect(isStopped(loadLifecycleConfig(e), 'claude-helpcheck')).toBe(false)
|
|
367
|
-
})
|
|
368
|
-
test('--help does NOT execute the verb: `remove <peer> --help` keeps the registry record', async () => {
|
|
369
|
-
await register('keepme')
|
|
370
|
-
const e = env()
|
|
371
|
-
expect(await runCli(['remove', 'keepme', '--help'], e)).toBe(0)
|
|
372
|
-
expect(findPeer(readPeersIndex({ env: e }), 'keepme')).not.toBeNull()
|
|
373
|
-
})
|
|
374
|
-
test('version --help shows usage, not the version number', async () => {
|
|
375
|
-
expect(await runCli(['version', '--help'], env())).toBe(0)
|
|
376
|
-
expect(captured).toContain('usage: iapeer')
|
|
377
|
-
expect(captured.trim().split('\n').length).toBeGreaterThan(3) // usage, not a bare semver line
|
|
378
|
-
})
|
|
379
|
-
test('a literal "--help" value stays expressible via --key=--help (not intercepted)', () => {
|
|
380
|
-
expect(parseArgs(['boris', '--message=--help']).flags.message).toBe('--help')
|
|
381
|
-
})
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
describe('parseArgs (audit #27 — value beginning with --)', () => {
|
|
385
|
-
test('--key=value preserves a value that starts with --', () => {
|
|
386
|
-
expect(parseArgs(['send', 'boris', '--message=--look', '--topic=re: x']).flags).toMatchObject({
|
|
387
|
-
message: '--look',
|
|
388
|
-
topic: 're: x',
|
|
389
|
-
})
|
|
390
|
-
})
|
|
391
|
-
test('look-ahead --key value still works; a following --flag is not consumed as a value', () => {
|
|
392
|
-
const { flags, positionals } = parseArgs(['send', 'boris', '--message', 'hi', '--json'])
|
|
393
|
-
expect(positionals).toEqual(['send', 'boris'])
|
|
394
|
-
expect(flags.message).toBe('hi')
|
|
395
|
-
expect(flags.json).toBe(true)
|
|
396
|
-
})
|
|
397
|
-
test('empty value via = stays a string, not true', () => {
|
|
398
|
-
expect(parseArgs(['x', '--description=']).flags.description).toBe('')
|
|
399
|
-
})
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
// ─── new — the UNCONDITIONAL fresh-restart control (docs/Control-команды §new) ─
|
|
403
|
-
|
|
404
|
-
describe('new (unconditional fresh restart)', () => {
|
|
405
|
-
test('happy path: fresh-slate (un-park + stale markers cleared) → fresh wake with resume:false task:"" → action fresh', async () => {
|
|
406
|
-
await register('alpha')
|
|
407
|
-
const e = env()
|
|
408
|
-
const cfg = loadLifecycleConfig(e)
|
|
409
|
-
// a parked peer with stale markers — /new must override ALL of it
|
|
410
|
-
setStopped(cfg, 'claude-alpha')
|
|
411
|
-
setIdleReaped(cfg, 'claude-alpha')
|
|
412
|
-
const calls: Array<{ personality: string; runtime: string; task: string; resume: boolean }> = []
|
|
413
|
-
const o = await newPeer('alpha', undefined, {
|
|
414
|
-
env: e,
|
|
415
|
-
wakeFn: async args => {
|
|
416
|
-
calls.push(args)
|
|
417
|
-
return { status: 'READY', woke: true, runtime: args.runtime, process_address: `${args.runtime}-${args.personality}`, taskDelivered: true }
|
|
418
|
-
},
|
|
419
|
-
})
|
|
420
|
-
expect(o.action).toBe('fresh')
|
|
421
|
-
expect(o.runtime).toBe('claude')
|
|
422
|
-
expect(calls).toEqual([{ personality: 'alpha', runtime: 'claude', task: '', resume: false }])
|
|
423
|
-
expect(isStopped(cfg, 'claude-alpha')).toBe(false) // un-parked: /new outranks the C1 stop
|
|
424
|
-
expect(hasIdleReaped(cfg, 'claude-alpha')).toBe(false) // stale marker cleared
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
test('wake FAILED → action failed with the reason (exit-1 contract for the bot)', async () => {
|
|
428
|
-
await register('beta2')
|
|
429
|
-
const o = await newPeer('beta2', undefined, {
|
|
430
|
-
env: env(),
|
|
431
|
-
wakeFn: async () => ({ status: 'FAILED', woke: false, reason: 'boot hang' }),
|
|
432
|
-
})
|
|
433
|
-
expect(o.action).toBe('failed')
|
|
434
|
-
expect(o.reason).toBe('boot hang')
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
test('refusals are explicit: foreign launchd / infra runtime / undeclared runtime / unknown peer', async () => {
|
|
438
|
-
const e = env()
|
|
439
|
-
// foreign PP plist → fleet guard
|
|
440
|
-
await register('boris')
|
|
441
|
-
writeFileSync(launchdPlistPath('boris', e), '<?xml version="1.0"?>\n<plist><dict><key>Label</key><string>com.iapeer.boris</string></dict></plist>\n')
|
|
442
|
-
expect((await newPeer('boris', undefined, { env: e })).action).toBe('refused-foreign-launchd')
|
|
443
|
-
// infra runtime → launchd-held, kickstart hint
|
|
444
|
-
await register('tg', 'telegram', 'natural')
|
|
445
|
-
const infra = await newPeer('tg', undefined, { env: e })
|
|
446
|
-
expect(infra.action).toBe('refused-infra')
|
|
447
|
-
expect(infra.reason).toContain('kickstart')
|
|
448
|
-
// undeclared runtime → explicit refusal, no silent launch
|
|
449
|
-
await register('gamma2')
|
|
450
|
-
expect((await newPeer('gamma2', 'codex', { env: e })).action).toBe('refused-undeclared-runtime')
|
|
451
|
-
// unknown peer → throws (same contract as stop/start)
|
|
452
|
-
await expect(newPeer('ghost-nobody', undefined, { env: e })).rejects.toThrow('not registered')
|
|
453
|
-
})
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
// ─── compact-resume — control addresses the dialogue, not just a live session ──
|
|
457
|
-
|
|
458
|
-
describe('compact (resume sleeping dialogue)', () => {
|
|
459
|
-
test('clean idle-reaped asleep peer → explicit resume wake, then /compact control', async () => {
|
|
460
|
-
await register('sleepy', 'codex')
|
|
461
|
-
const e = env()
|
|
462
|
-
const cfg = loadLifecycleConfig(e)
|
|
463
|
-
setIdleReaped(cfg, 'codex-sleepy')
|
|
464
|
-
const wakeCalls: unknown[] = []
|
|
465
|
-
const controlCalls: unknown[] = []
|
|
466
|
-
const doneCalls: unknown[] = []
|
|
467
|
-
const o = await compactPeer('sleepy', 'codex', {
|
|
468
|
-
env: e,
|
|
469
|
-
wakeFn: async args => {
|
|
470
|
-
wakeCalls.push(args)
|
|
471
|
-
return { status: 'READY', woke: true, runtime: args.runtime, process_address: `${args.runtime}-${args.personality}`, taskDelivered: true }
|
|
472
|
-
},
|
|
473
|
-
controlFn: async (personality, runtime, command) => {
|
|
474
|
-
controlCalls.push({ personality, runtime, command })
|
|
475
|
-
return ok({ ok: true, controlled: { personality, runtime: runtime! }, command: command.name, ts: 't' })
|
|
476
|
-
},
|
|
477
|
-
compactDoneFn: (target, cwd, baseline) => {
|
|
478
|
-
doneCalls.push({ target, cwd, runtime: baseline.runtime })
|
|
479
|
-
return ok({ ok: true, ms: 42, signal: 'transcript+ready' })
|
|
480
|
-
},
|
|
481
|
-
})
|
|
482
|
-
expect(o).toMatchObject({ action: 'compacted', runtime: 'codex', woke: true })
|
|
483
|
-
expect(wakeCalls).toEqual([{ personality: 'sleepy', runtime: 'codex', task: '', resume: true }])
|
|
484
|
-
expect(controlCalls).toEqual([{ personality: 'sleepy', runtime: 'codex', command: { name: 'compact' } }])
|
|
485
|
-
expect(doneCalls).toHaveLength(1)
|
|
486
|
-
expect(doneCalls[0]).toMatchObject({ cwd: '/tmp/sleepy', runtime: 'codex', target: { runtime: 'codex', personality: 'sleepy' } })
|
|
487
|
-
expect(hasIdleReaped(cfg, 'codex-sleepy')).toBe(false)
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
test('compact done gate failure surfaces as failed (no false success)', async () => {
|
|
491
|
-
await register('slow', 'codex')
|
|
492
|
-
const e = env()
|
|
493
|
-
const cfg = loadLifecycleConfig(e)
|
|
494
|
-
setIdleReaped(cfg, 'codex-slow')
|
|
495
|
-
const o = await compactPeer('slow', 'codex', {
|
|
496
|
-
env: e,
|
|
497
|
-
wakeFn: async args => ({ status: 'READY', woke: true, runtime: args.runtime, process_address: `${args.runtime}-${args.personality}`, taskDelivered: true }),
|
|
498
|
-
controlFn: async (personality, runtime, command) => ok({ ok: true, controlled: { personality, runtime: runtime! }, command: command.name, ts: 't' }),
|
|
499
|
-
compactDoneFn: () => err('compact did not complete within test'),
|
|
500
|
-
})
|
|
501
|
-
expect(o.action).toBe('failed')
|
|
502
|
-
expect(o.reason).toContain('did not complete')
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
test('crashed / first-wake asleep peer (no clean idle marker) → nothing-to-compact, no fresh wake', async () => {
|
|
506
|
-
await register('freshy', 'codex')
|
|
507
|
-
let woke = false
|
|
508
|
-
const o = await compactPeer('freshy', 'codex', {
|
|
509
|
-
env: env(),
|
|
510
|
-
wakeFn: async () => ((woke = true), { status: 'READY', woke: true }),
|
|
511
|
-
})
|
|
512
|
-
expect(o.action).toBe('nothing-to-compact')
|
|
513
|
-
expect(o.reason).toContain('nothing to compact')
|
|
514
|
-
expect(woke).toBe(false)
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
test('idle marker but no transcript/session to resume → nothing-to-compact (not fresh)', async () => {
|
|
518
|
-
await register('empty', 'codex')
|
|
519
|
-
const e = env()
|
|
520
|
-
setIdleReaped(loadLifecycleConfig(e), 'codex-empty')
|
|
521
|
-
const o = await compactPeer('empty', 'codex', {
|
|
522
|
-
env: e,
|
|
523
|
-
wakeFn: async args => ({ status: 'FAILED', woke: false, runtime: args.runtime, reason: 'no codex session to resume' }),
|
|
524
|
-
})
|
|
525
|
-
expect(o.action).toBe('nothing-to-compact')
|
|
526
|
-
expect(o.reason).toContain('nothing to compact')
|
|
527
|
-
})
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
// ─── add-runtime / default-runtime — the fleet-switch levers (codex-parity) ───
|
|
531
|
-
|
|
532
|
-
describe('add-runtime / default-runtime (fleet-switch levers)', () => {
|
|
533
|
-
test('add-runtime merges the runtime into profile+registry; idempotent; infra peers skipped', async () => {
|
|
534
|
-
const e = env()
|
|
535
|
-
// a claude-only peer with a REAL cwd (initPeer scaffolds it)
|
|
536
|
-
const cwd = join(root, 'peers', 'aud1')
|
|
537
|
-
mkdirSync(cwd, { recursive: true })
|
|
538
|
-
await upsertPeer({ personality: 'aud1', runtime: 'claude', cwd, intelligence: 'artificial' }, { rootDir: root })
|
|
539
|
-
const { initPeer } = await import('../init/index.ts')
|
|
540
|
-
await initPeer({ cwd, runtime: 'claude', env: e }) // give it a local profile (claude-only)
|
|
541
|
-
// an infra peer in the registry — must be skipped by --all
|
|
542
|
-
await register('tginfra', 'telegram', 'natural')
|
|
543
|
-
const outcomes = await addRuntime('codex', { all: true, env: e })
|
|
544
|
-
const aud1 = outcomes.find(o => o.personality === 'aud1')!
|
|
545
|
-
expect(aud1.action).toBe('added')
|
|
546
|
-
expect(outcomes.find(o => o.personality === 'tginfra')!.action).toBe('skipped-infra-peer')
|
|
547
|
-
// profile + registry both carry codex now; default untouched
|
|
548
|
-
const { readPeerProfile } = await import('../identity/index.ts')
|
|
549
|
-
const prof = readPeerProfile(cwd)!
|
|
550
|
-
expect(prof.runtimes).toContain('codex')
|
|
551
|
-
expect(prof.runtime).toBe('claude') // default_runtime NOT flipped
|
|
552
|
-
const rec = findPeer(readPeersIndex({ env: e }), 'aud1')!
|
|
553
|
-
expect(rec.runtimes).toContain('codex')
|
|
554
|
-
expect(rec.runtime).toBe('claude') // registry default NOT flipped (capability ≠ routing; live-caught: upsert's «args.runtime wins» needed the reindex heal)
|
|
555
|
-
// idempotent
|
|
556
|
-
expect((await addRuntime('codex', { peer: 'aud1', env: e }))[0].action).toBe('already')
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
test('add-runtime works from INSIDE another peer session (poisoned PEER_* env) — live-caught 12.06', async () => {
|
|
560
|
-
const e = env()
|
|
561
|
-
e.PEER_PERSONALITY = 'iapeer' // the caller's own identity must not poison the target's init
|
|
562
|
-
e.PEER_RUNTIME = 'claude'
|
|
563
|
-
e.PEER_IDENTITY = 'claude-iapeer'
|
|
564
|
-
const cwd = join(root, 'peers', 'aud3')
|
|
565
|
-
mkdirSync(cwd, { recursive: true })
|
|
566
|
-
await upsertPeer({ personality: 'aud3', runtime: 'claude', cwd, intelligence: 'artificial' }, { rootDir: root })
|
|
567
|
-
const { initPeer } = await import('../init/index.ts')
|
|
568
|
-
const cleanE = { ...e }
|
|
569
|
-
delete cleanE.PEER_PERSONALITY; delete cleanE.PEER_RUNTIME; delete cleanE.PEER_IDENTITY
|
|
570
|
-
await initPeer({ cwd, runtime: 'claude', env: cleanE })
|
|
571
|
-
const outcomes = await addRuntime('codex', { peer: 'aud3', env: e })
|
|
572
|
-
expect(outcomes[0].action).toBe('added')
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
test('add-runtime refuses an infra runtime as the argument', async () => {
|
|
576
|
-
await register('x1')
|
|
577
|
-
await expect(addRuntime('telegram', { peer: 'x1', env: env() })).rejects.toThrow('infra runtime')
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
test('default-runtime flips the primary (profile + registry in one command); refuses undeclared; symmetric back', async () => {
|
|
581
|
-
const e = env()
|
|
582
|
-
const cwd = join(root, 'peers', 'aud2')
|
|
583
|
-
mkdirSync(cwd, { recursive: true })
|
|
584
|
-
await upsertPeer({ personality: 'aud2', runtime: 'claude', cwd, intelligence: 'artificial' }, { rootDir: root })
|
|
585
|
-
const { initPeer } = await import('../init/index.ts')
|
|
586
|
-
await initPeer({ cwd, runtime: 'claude', env: e })
|
|
587
|
-
// undeclared codex → explicit refusal with the add-runtime hint
|
|
588
|
-
const refused = await defaultRuntime('codex', { peer: 'aud2', env: e })
|
|
589
|
-
expect(refused[0].action).toBe('refused-undeclared-runtime')
|
|
590
|
-
expect(refused[0].detail).toContain('add-runtime')
|
|
591
|
-
// declare codex, then flip
|
|
592
|
-
await addRuntime('codex', { peer: 'aud2', env: e })
|
|
593
|
-
expect((await defaultRuntime('codex', { peer: 'aud2', env: e }))[0].action).toBe('flipped')
|
|
594
|
-
const { readPeerProfile } = await import('../identity/index.ts')
|
|
595
|
-
expect(readPeerProfile(cwd)!.runtime).toBe('codex')
|
|
596
|
-
expect(readPeerProfile(cwd)!.runtimes[0]).toBe('codex') // normalizeRuntimes prepends the default
|
|
597
|
-
expect(findPeer(readPeersIndex({ env: e }), 'aud2')!.runtime).toBe('codex') // registry healed same-command
|
|
598
|
-
// idempotent + symmetric revert
|
|
599
|
-
expect((await defaultRuntime('codex', { peer: 'aud2', env: e }))[0].action).toBe('already')
|
|
600
|
-
expect((await defaultRuntime('claude', { peer: 'aud2', env: e }))[0].action).toBe('flipped')
|
|
601
|
-
expect(findPeer(readPeersIndex({ env: e }), 'aud2')!.runtime).toBe('claude')
|
|
602
|
-
})
|
|
603
|
-
})
|