@agfpd/iapeer 0.2.77 → 0.2.79

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.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/package.json +21 -2
  4. package/src/cli/index.ts +95 -78
  5. package/src/connect/index.ts +4 -4
  6. package/src/core/constants.ts +3 -3
  7. package/src/core/normalize.ts +2 -2
  8. package/src/create/index.ts +1 -1
  9. package/src/daemon/index.ts +5 -5
  10. package/src/daemon/main.ts +9 -9
  11. package/src/enable/index.ts +19 -18
  12. package/src/enable/provisionCommand.ts +10 -10
  13. package/src/identity/index.ts +2 -2
  14. package/src/identity/profileStandard.ts +8 -8
  15. package/src/init/index.ts +10 -10
  16. package/src/install/index.ts +2 -2
  17. package/src/install/signing.ts +3 -4
  18. package/src/launch/adapters/claude.ts +7 -7
  19. package/src/launch/adapters/codex.ts +5 -6
  20. package/src/launch/canary.ts +8 -8
  21. package/src/launch/cmdlog.ts +3 -3
  22. package/src/launch/composeSystemPrompt.ts +2 -1
  23. package/src/launch/index.ts +5 -6
  24. package/src/launch/invocation.ts +1 -1
  25. package/src/launch/launchd.ts +14 -15
  26. package/src/launch/launchdRun.ts +6 -6
  27. package/src/launch/nativeMemory.ts +9 -9
  28. package/src/launch/ptyHost.ts +1 -1
  29. package/src/launch/readyGateModel.ts +3 -3
  30. package/src/launch/types.ts +7 -7
  31. package/src/lifecycle/eventlog.ts +2 -2
  32. package/src/lifecycle/index.ts +21 -21
  33. package/src/lifecycle/queue.ts +1 -1
  34. package/src/onboard/index.ts +19 -13
  35. package/src/onboard/memory.ts +5 -5
  36. package/src/onboard/steps.ts +8 -9
  37. package/src/provision/index.ts +2 -3
  38. package/src/runtime/deploy.ts +2 -2
  39. package/src/runtime/index.ts +4 -4
  40. package/src/shadow/gate.ts +6 -6
  41. package/src/shadow/index.ts +2 -2
  42. package/src/status/index.ts +11 -11
  43. package/src/storage/index.ts +1 -1
  44. package/src/supervisor/daemon.ts +20 -13
  45. package/src/transport/index.ts +11 -11
  46. package/src/update/index.ts +4 -4
  47. package/src/cli/cli.test.ts +0 -603
  48. package/src/cli/listTui.test.ts +0 -71
  49. package/src/codec/codec.test.ts +0 -271
  50. package/src/connect/connect.test.ts +0 -163
  51. package/src/core/constants.test.ts +0 -32
  52. package/src/core/normalize.test.ts +0 -98
  53. package/src/create/create.test.ts +0 -143
  54. package/src/daemon/daemon-http.test.ts +0 -114
  55. package/src/daemon/daemon.test.ts +0 -267
  56. package/src/daemon/main.test.ts +0 -413
  57. package/src/enable/enable.test.ts +0 -95
  58. package/src/enable/provisionCommand.test.ts +0 -107
  59. package/src/identity/identity.test.ts +0 -279
  60. package/src/identity/profileStandard.test.ts +0 -204
  61. package/src/init/init.test.ts +0 -215
  62. package/src/init/runtime-resolve.test.ts +0 -49
  63. package/src/install/install.test.ts +0 -66
  64. package/src/install/signing.test.ts +0 -84
  65. package/src/launch/bootdeliver.test.ts +0 -108
  66. package/src/launch/bootstrap.test.ts +0 -56
  67. package/src/launch/canary.test.ts +0 -346
  68. package/src/launch/cmdlog.test.ts +0 -94
  69. package/src/launch/codexHooksTrust.test.ts +0 -248
  70. package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
  71. package/src/launch/composeSystemPrompt.test.ts +0 -145
  72. package/src/launch/hostComposerOccupied.test.ts +0 -63
  73. package/src/launch/invocation.test.ts +0 -174
  74. package/src/launch/launch.test.ts +0 -359
  75. package/src/launch/launchd.test.ts +0 -499
  76. package/src/launch/nativeMemory.test.ts +0 -193
  77. package/src/launch/ptyHost.test.ts +0 -38
  78. package/src/launch/readyGateModel.test.ts +0 -72
  79. package/src/launch/sockdir.test.ts +0 -74
  80. package/src/lifecycle/eventlog.test.ts +0 -114
  81. package/src/lifecycle/lifecycle.test.ts +0 -1056
  82. package/src/lifecycle/queue.test.ts +0 -185
  83. package/src/onboard/memory.test.ts +0 -157
  84. package/src/onboard/onboard.test.ts +0 -39
  85. package/src/onboard/steps.test.ts +0 -201
  86. package/src/provision/provision.test.ts +0 -288
  87. package/src/registry/registry.test.ts +0 -432
  88. package/src/runtime/runtime.test.ts +0 -226
  89. package/src/runtime/update.test.ts +0 -192
  90. package/src/shadow/enumerate-skip.test.ts +0 -67
  91. package/src/shadow/gate.test.ts +0 -286
  92. package/src/shadow/install.test.ts +0 -153
  93. package/src/shadow/shadow.test.ts +0 -42
  94. package/src/status/status.test.ts +0 -208
  95. package/src/storage/peers-home.test.ts +0 -34
  96. package/src/storage/rotatelog.test.ts +0 -44
  97. package/src/storage/storage.test.ts +0 -65
  98. package/src/supervisor/attached-marker.test.ts +0 -84
  99. package/src/supervisor/boot.test.ts +0 -66
  100. package/src/supervisor/client.attach.test.ts +0 -107
  101. package/src/supervisor/daemon.test.ts +0 -213
  102. package/src/supervisor/deliver.test.ts +0 -58
  103. package/src/supervisor/panelog.test.ts +0 -95
  104. package/src/supervisor/protocol.test.ts +0 -222
  105. package/src/supervisor/selfargv.test.ts +0 -22
  106. package/src/supervisor/spinner-strip.test.ts +0 -52
  107. package/src/transport/compactDone.test.ts +0 -144
  108. package/src/transport/composerQueue.test.ts +0 -204
  109. package/src/transport/hostControl.test.ts +0 -87
  110. package/src/transport/hostDeliver.test.ts +0 -137
  111. package/src/transport/hostLive.test.ts +0 -50
  112. package/src/transport/routesend-wake.test.ts +0 -357
  113. package/src/update/update.test.ts +0 -208
@@ -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
- })