@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.
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 +47 -48
  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 +14 -8
  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,346 +0,0 @@
1
- // Server-death canary — pure script-builder shape + live-tmux behavior:
2
- // dirty server death (SIGKILL) → ev=server-exit line + forensics snapshot;
3
- // clean teardown (signal / killSession) → silent, no record. Live tests follow
4
- // the suite's test.if(tmuxAvailable) sandbox-socket pattern (lifecycle.test.ts).
5
-
6
- import { afterAll, describe, expect, test } from 'bun:test'
7
- import { spawnSync } from 'child_process'
8
- import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'fs'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import {
12
- canaryChannel,
13
- canaryProcessPattern,
14
- canaryScript,
15
- dismissCanary,
16
- ensureServerCanary,
17
- exitLogPath,
18
- serverDeathsDir,
19
- signalCanaryClean,
20
- } from './canary.ts'
21
- import { killSession } from '../lifecycle/index.ts'
22
- import { teardownAlwaysOnSession } from './launchdRun.ts'
23
-
24
- const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
25
-
26
- const sleep = (ms: number) => new Promise(r => setTimeout(r, ms))
27
-
28
- /** Poll until `cond` is true or `ms` elapsed. */
29
- async function waitFor(cond: () => boolean, ms: number): Promise<boolean> {
30
- const deadline = Date.now() + ms
31
- while (Date.now() < deadline) {
32
- if (cond()) return true
33
- await sleep(100)
34
- }
35
- return cond()
36
- }
37
-
38
- function canaryRunning(identity: string): boolean {
39
- return (
40
- spawnSync('pgrep', ['-f', `wait-for.*${canaryChannel(identity)}([^a-z0-9-]|$)`], { stdio: 'ignore' }).status === 0
41
- )
42
- }
43
-
44
- function serverPid(sock: string): number {
45
- const r = spawnSync('tmux', ['-S', sock, 'display-message', '-p', '#{pid}'], { encoding: 'utf8' })
46
- return Number((r.stdout ?? '').trim())
47
- }
48
-
49
- describe('canary script (pure)', () => {
50
- test('channel is per-identity', () => {
51
- expect(canaryChannel('claude-bob')).toBe('iap-canary-claude-bob')
52
- })
53
-
54
- test('exitLogPath → exits.log inside the dir', () => {
55
- expect(exitLogPath('/r/logs/iapeer')).toBe('/r/logs/iapeer/exits.log')
56
- })
57
-
58
- test('script carries the v2 protocol: wait-for channel, deliberate-silence guards, liveness probe, record line, forensics', () => {
59
- const s = canaryScript({
60
- identity: 'claude-bob',
61
- sock: '/tmp/x.sock',
62
- tmuxBin: '/opt/homebrew/bin/tmux',
63
- exitLogFile: '/r/logs/iapeer/exits.log',
64
- forensicsDir: '/r/logs/iapeer/server-deaths',
65
- })
66
- expect(s).toContain(`wait-for 'iap-canary-claude-bob'`) // the blocking client
67
- expect(s).toContain(`trap 'exit 0' HUP INT TERM`) // dismissed sh = silent (the ONLY deliberate silencer)
68
- // v2: NO exit code is trusted as deliberate (a TERMed client returns rc=0 —
69
- // proven live); the SERVER's liveness decides, after a dismissal grace sleep.
70
- expect(s).not.toContain(`[ "$rc" -eq 0 ] || [ "$rc" -ge 128 ]; then exit 0`) // the v1 hole
71
- expect(s).toContain('sleep 2') // dismissal grace window
72
- expect(s).toContain(`kill -0 "$SPID" 2>/dev/null; then exit 0`) // original server alive → nothing to record
73
- expect(s).toContain(`[ -z "$SPID" ]`) // fallback only when the original pid was unavailable
74
- expect(s).toContain('cause=server-vanished') // connection drop (SIGKILL/OOM class)
75
- expect(s).toContain('cause=signaled-server-gone') // rc=0: channel/client-TERM, server died
76
- expect(s).toContain('cause=client-killed-server-gone') // rc≥128: client took a hard kill
77
- expect(s).toContain('ev=server-exit identity=claude-bob') // the exits.log record
78
- expect(s).toContain(`>> '/r/logs/iapeer/exits.log'`)
79
- expect(s).toContain('/r/logs/iapeer/server-deaths/claude-bob') // forensics file
80
- expect(s).toContain('vm_stat') // memory evidence (OOM hypothesis)
81
- expect(s).toContain(`'/opt/homebrew/bin/tmux' -S '/tmp/x.sock'`) // abs tmux, quoted
82
- expect(s).not.toContain('cmdlog-tail') // no cmdLogDir → no cmdlog capture line
83
- })
84
-
85
- test('with cmdLogDir → forensics tail the dead server cmdlog (client-COMMAND kill attribution)', () => {
86
- const s = canaryScript({
87
- identity: 'claude-bob',
88
- sock: '/tmp/x.sock',
89
- tmuxBin: '/opt/homebrew/bin/tmux',
90
- exitLogFile: '/r/logs/iapeer/exits.log',
91
- forensicsDir: '/r/logs/iapeer/server-deaths',
92
- cmdLogDir: '/r/logs/iapeer/tmux-cmdlog/claude-bob',
93
- })
94
- expect(s).toContain('cmdlog-tail') // the kill-server/kill-session witness
95
- // tails the dead server's own -v log by its captured pid, greps the command lines
96
- expect(s).toContain(`'/r/logs/iapeer/tmux-cmdlog/claude-bob/tmux-server-'"$SPID"'.log'`)
97
- expect(s).toContain(`grep -aE 'command:'`)
98
- })
99
-
100
- test('ensureServerCanary without exitLogDir → skipped (observability off)', () => {
101
- expect(ensureServerCanary({ identity: 'claude-none', sock: '/tmp/none.sock' })).toBe('skipped')
102
- })
103
-
104
- test('canaryProcessPattern is identity-anchored (no prefix bleed) and matches both canary processes', () => {
105
- const p = canaryProcessPattern('claude-iap')
106
- expect(p).toBe('iap-canary-claude-iap([^a-z0-9-]|$)')
107
- const re = new RegExp(p)
108
- expect(re.test(`/opt/homebrew/bin/tmux -S /tmp/x.sock wait-for iap-canary-claude-iap`)).toBe(true) // client argv
109
- expect(re.test(`/bin/sh -c ... wait-for 'iap-canary-claude-iap'\n...`)).toBe(true) // sh -c script (quoted channel)
110
- expect(re.test(`tmux wait-for iap-canary-claude-iap-memory`)).toBe(false) // prefix identity must NOT match
111
- })
112
-
113
- test('dismissCanary is a harmless no-op when no canary runs', () => {
114
- expect(() => dismissCanary('claude-nobody-here')).not.toThrow()
115
- })
116
- })
117
-
118
- describe.if(tmuxAvailable)('canary live (sandbox tmux servers)', () => {
119
- const dir = mkdtempSync(join(tmpdir(), 'iap-canary-'))
120
- const socks: string[] = []
121
-
122
- function bringUp(identity: string): { sock: string; logDir: string } {
123
- const sock = join(dir, `${identity}.sock`)
124
- socks.push(sock)
125
- const r = spawnSync('tmux', ['-S', sock, 'new-session', '-d', '-s', identity, 'sleep', '120'], {
126
- stdio: 'ignore',
127
- })
128
- expect(r.status).toBe(0)
129
- return { sock, logDir: join(dir, `logs-${identity}`) }
130
- }
131
-
132
- const allIds = [
133
- 'claude-canadirty',
134
- 'claude-canaclean',
135
- 'claude-canakill',
136
- 'notifier-canatear',
137
- 'claude-canasweep',
138
- 'claude-canaclient',
139
- 'claude-canareplaced',
140
- ]
141
-
142
- afterAll(() => {
143
- for (const sock of socks) {
144
- // teardown is DELIBERATE → silence each canary (signal + dismiss) before
145
- // killing its server — the v2 contract for every deliberate path.
146
- for (const id of allIds) {
147
- signalCanaryClean(sock, id)
148
- dismissCanary(id)
149
- }
150
- spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
151
- }
152
- rmSync(dir, { recursive: true, force: true })
153
- })
154
-
155
- test(
156
- 'dirty server death (SIGKILL) → ev=server-exit + forensics snapshot',
157
- async () => {
158
- const identity = 'claude-canadirty'
159
- const { sock, logDir } = bringUp(identity)
160
- const pid = serverPid(sock)
161
- expect(pid).toBeGreaterThan(0)
162
-
163
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
164
- // idempotency: a second ensure must NOT double-spawn
165
- expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
166
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('already')
167
- await sleep(500) // let the wait-for client register on the channel
168
-
169
- process.kill(pid, 'SIGKILL') // the silent server-killer class
170
- const log = exitLogPath(logDir)
171
- expect(await waitFor(() => existsSync(log) && readFileSync(log, 'utf8').includes('ev=server-exit'), 8000)).toBe(
172
- true,
173
- )
174
- const line = readFileSync(log, 'utf8')
175
- expect(line).toContain(`ev=server-exit identity=${identity}`)
176
- expect(line).toContain(`server_pid=${pid}`)
177
- const snaps = readdirSync(serverDeathsDir(logDir))
178
- expect(snaps.length).toBe(1)
179
- const snap = readFileSync(join(serverDeathsDir(logDir), snaps[0]!), 'utf8')
180
- expect(snap).toContain(`server-death identity=${identity}`)
181
- // canary is one-shot: after firing it must be gone
182
- expect(await waitFor(() => !canaryRunning(identity), 3000)).toBe(true)
183
- },
184
- 20000,
185
- )
186
-
187
- test(
188
- 'clean teardown (signal, then kill-server) → silent, no record',
189
- async () => {
190
- const identity = 'claude-canaclean'
191
- const { sock, logDir } = bringUp(identity)
192
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
193
- expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
194
- await sleep(500) // let the wait-for client register before signaling
195
-
196
- signalCanaryClean(sock, identity)
197
- expect(await waitFor(() => !canaryRunning(identity), 3000)).toBe(true) // exited silently
198
- spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
199
- await sleep(300)
200
- expect(existsSync(exitLogPath(logDir)) && readFileSync(exitLogPath(logDir), 'utf8').includes('ev=server-exit')).toBe(
201
- false,
202
- )
203
- },
204
- 20000,
205
- )
206
-
207
- test(
208
- 'teardownAlwaysOnSession (signal-exit of runAlwaysOn) kills session+server, canary stays silent',
209
- async () => {
210
- const identity = 'notifier-canatear'
211
- const { sock, logDir } = bringUp(identity)
212
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
213
- expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
214
- await sleep(500)
215
-
216
- teardownAlwaysOnSession(sock, identity) // the bootout/shutdown path
217
- // the whole server must be gone (the poller dies WITH the watcher — грабля closed)
218
- expect(
219
- await waitFor(
220
- () => spawnSync('tmux', ['-S', sock, 'has-session', '-t', identity], { stdio: 'ignore' }).status !== 0,
221
- 3000,
222
- ),
223
- ).toBe(true)
224
- expect(await waitFor(() => !canaryRunning(identity), 3000)).toBe(true)
225
- await sleep(300)
226
- expect(existsSync(exitLogPath(logDir)) && readFileSync(exitLogPath(logDir), 'utf8').includes('ev=server-exit')).toBe(
227
- false,
228
- )
229
- },
230
- 20000,
231
- )
232
-
233
- test(
234
- 'killSession (lifecycle clean reap) signals the canary before kill-server → no record',
235
- async () => {
236
- const identity = 'claude-canakill'
237
- const { sock, logDir } = bringUp(identity)
238
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
239
- expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
240
- await sleep(500)
241
-
242
- killSession(sock, identity) // last session → kills the SERVER, signal-first
243
- expect(await waitFor(() => !canaryRunning(identity), 3000)).toBe(true)
244
- await sleep(300)
245
- expect(existsSync(exitLogPath(logDir)) && readFileSync(exitLogPath(logDir), 'utf8').includes('ev=server-exit')).toBe(
246
- false,
247
- )
248
- },
249
- 20000,
250
- )
251
-
252
- test(
253
- 'death-#4 shape: external killer sweeps server AND canary client → ev=server-exit cause=client-signaled',
254
- async () => {
255
- const identity = 'claude-canasweep'
256
- const { sock, logDir } = bringUp(identity)
257
- const pid = serverPid(sock)
258
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
259
- expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
260
- await sleep(500)
261
-
262
- // The pre-clean-shaped external killer: one pattern takes the server AND
263
- // the canary CLIENT (both argv contain `tmux -S <sock> `), the sh recorder
264
- // survives. The TERMed client returns rc=0 (proven live) — v1 read that as
265
- // a clean channel signal and stayed silent; exactly how deaths #4–#6
266
- // (10.06) left zero records. v2 probes the server instead: dead → record.
267
- spawnSync('pkill', ['-f', `tmux -S ${sock} `], { stdio: 'ignore' })
268
- const log = exitLogPath(logDir)
269
- expect(
270
- await waitFor(() => existsSync(log) && readFileSync(log, 'utf8').includes('ev=server-exit'), 10000),
271
- ).toBe(true)
272
- const line = readFileSync(log, 'utf8')
273
- expect(line).toContain(`ev=server-exit identity=${identity}`)
274
- expect(line).toContain('cause=signaled-server-gone') // rc=0 shape, server found dead
275
- expect(line).toContain(`server_pid=${pid}`)
276
- expect(readdirSync(serverDeathsDir(logDir)).length).toBe(1) // forensics captured
277
- },
278
- 20000,
279
- )
280
-
281
- test(
282
- 'canary client killed while server lives → silent (no false record), canary gone for the retrofit to re-arm',
283
- async () => {
284
- const identity = 'claude-canaclient'
285
- const { sock, logDir } = bringUp(identity)
286
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
287
- expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
288
- await sleep(500)
289
-
290
- // TERM the CLIENT only (argv ends with the bare channel — the sh's quoted
291
- // form does not match this $-anchored pattern), server stays up.
292
- spawnSync('pkill', ['-f', `wait-for ${canaryChannel(identity)}$`], { stdio: 'ignore' })
293
- // the sh probes (≈2 s), finds the server ALIVE → exits silently
294
- expect(await waitFor(() => !canaryRunning(identity), 6000)).toBe(true)
295
- await sleep(300)
296
- expect(
297
- existsSync(exitLogPath(logDir)) && readFileSync(exitLogPath(logDir), 'utf8').includes('ev=server-exit'),
298
- ).toBe(false) // no false server-death while the server lives
299
- // server must still be alive — and ensure re-arms a fresh canary (the
300
- // retrofit path; in prod the supervise tick does this and logs it)
301
- expect(spawnSync('tmux', ['-S', sock, 'has-session', '-t', identity], { stdio: 'ignore' }).status).toBe(0)
302
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
303
- },
304
- 20000,
305
- )
306
-
307
- test(
308
- 'dirty server death followed by immediate replacement on the same socket still records the ORIGINAL death',
309
- async () => {
310
- const identity = 'claude-canareplaced'
311
- const { sock, logDir } = bringUp(identity)
312
- const pid = serverPid(sock)
313
- expect(pid).toBeGreaterThan(0)
314
- expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
315
- expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
316
- await sleep(500)
317
-
318
- process.kill(pid, 'SIGKILL')
319
- // Reuse the same socket before the canary's 2s dismissal-grace window
320
- // expires. The old socket-level probe treated this successor as "server
321
- // alive" and lost the predecessor's death; the canary must key on SPID.
322
- let started = false
323
- for (let i = 0; i < 20; i++) {
324
- const r = spawnSync('tmux', ['-S', sock, 'new-session', '-d', '-s', identity, 'sleep', '120'], {
325
- stdio: 'ignore',
326
- })
327
- if (r.status === 0) {
328
- started = true
329
- break
330
- }
331
- await sleep(50)
332
- }
333
- expect(started).toBe(true)
334
-
335
- const log = exitLogPath(logDir)
336
- expect(await waitFor(() => existsSync(log) && readFileSync(log, 'utf8').includes('ev=server-exit'), 8000)).toBe(
337
- true,
338
- )
339
- const line = readFileSync(log, 'utf8')
340
- expect(line).toContain(`ev=server-exit identity=${identity}`)
341
- expect(line).toContain(`server_pid=${pid}`)
342
- expect(readdirSync(serverDeathsDir(logDir)).length).toBe(1)
343
- },
344
- 20000,
345
- )
346
- })
@@ -1,94 +0,0 @@
1
- // tmux command-log (cmdlog.ts) — the killer-hunt socket-command layer.
2
- //
3
- // The live suite encodes boris's acceptance criterion directly: a kill-session
4
- // issued by an EXTERNAL client must land in the -v server log WITH client
5
- // attribution (`message: client-<pid> command: …`), file modes never wider than
6
- // 0600, and the cap keeps volume bounded without losing the tail.
7
-
8
- import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
9
- import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, existsSync } from 'fs'
10
- import { tmpdir } from 'os'
11
- import { join } from 'path'
12
- import { spawnSync } from 'child_process'
13
- import {
14
- CMDLOG_KEEP_BYTES,
15
- capCmdLogs,
16
- cmdLogDirFor,
17
- hardenCmdLogDir,
18
- prepareCmdLogDir,
19
- } from './cmdlog.ts'
20
-
21
- const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
22
-
23
- let root: string
24
- beforeAll(() => {
25
- root = mkdtempSync(join(tmpdir(), 'cmdlog-'))
26
- })
27
- afterAll(() => {
28
- rmSync(root, { recursive: true, force: true })
29
- })
30
-
31
- describe('prepareCmdLogDir / hardenCmdLogDir / capCmdLogs (hermetic)', () => {
32
- test('prepare WIPES the previous generation and recreates 0700', () => {
33
- const dir = prepareCmdLogDir(root, 'claude-x')
34
- expect(dir).toBe(cmdLogDirFor(root, 'claude-x'))
35
- writeFileSync(join(dir!, 'tmux-server-1.log'), 'old generation')
36
- const again = prepareCmdLogDir(root, 'claude-x')
37
- expect(readdirSync(again!)).toEqual([]) // old log gone — one generation per dir
38
- expect(statSync(again!).mode & 0o777).toBe(0o700)
39
- })
40
-
41
- test('harden clamps every file to 0600', () => {
42
- const dir = prepareCmdLogDir(root, 'claude-h')!
43
- writeFileSync(join(dir, 'tmux-server-2.log'), 'x', { mode: 0o644 })
44
- hardenCmdLogDir(dir)
45
- expect(statSync(join(dir, 'tmux-server-2.log')).mode & 0o777).toBe(0o600)
46
- })
47
-
48
- test('cap tail-keeps an oversized log (KEEP bytes of TAIL survive), leaves small logs alone', () => {
49
- const dir = prepareCmdLogDir(root, 'claude-c')!
50
- const big = join(dir, 'tmux-server-3.log')
51
- const small = join(dir, 'tmux-server-4.log')
52
- // 2 MB of filler ending in a recognizable tail — cap to 1 KB keep for the test
53
- writeFileSync(big, Buffer.concat([Buffer.alloc(2 * 1024 * 1024, 0x61), Buffer.from('THE-DEATH-TAIL')]))
54
- writeFileSync(small, 'tiny')
55
- const capped = capCmdLogs(root, 1024 * 1024, 1024)
56
- expect(capped).toEqual([big])
57
- const after = readFileSync(big)
58
- expect(after.length).toBe(1024)
59
- expect(after.toString()).toEndWith('THE-DEATH-TAIL') // the tail is what survives
60
- expect(readFileSync(small, 'utf8')).toBe('tiny')
61
- })
62
-
63
- test('cap on a missing tree is a silent no-op', () => {
64
- expect(capCmdLogs(join(root, 'absent'))).toEqual([])
65
- })
66
-
67
- test('default keep constant stays under the cap', () => {
68
- expect(CMDLOG_KEEP_BYTES).toBeLessThan(8 * 1024 * 1024)
69
- })
70
- })
71
-
72
- describe.if(tmuxAvailable)('live -v server log (acceptance criterion)', () => {
73
- test('external client kill-session lands in the server log with client attribution; harden → 0600', () => {
74
- const dir = prepareCmdLogDir(root, 'claude-live')!
75
- const sock = join(root, 'live.sock')
76
- // Start the server FROM the cmdlog dir with -v — exactly the launch wiring.
77
- const start = spawnSync('tmux', ['-S', sock, '-v', 'new-session', '-d', '-x', '80', '-y', '24', '-s', 'claude-live', 'sleep 60'], {
78
- cwd: dir,
79
- encoding: 'utf8',
80
- })
81
- expect(start.status).toBe(0)
82
- hardenCmdLogDir(dir)
83
- // An EXTERNAL client (separate tmux invocation = separate client pid) kills it.
84
- const kill = spawnSync('tmux', ['-S', sock, 'kill-session', '-t', 'claude-live'], { encoding: 'utf8' })
85
- expect(kill.status).toBe(0)
86
- const serverLog = readdirSync(dir).find(f => f.startsWith('tmux-server-'))
87
- expect(serverLog).toBeDefined()
88
- expect(statSync(join(dir, serverLog!)).mode & 0o777).toBe(0o600)
89
- const text = readFileSync(join(dir, serverLog!), 'utf8')
90
- // Attribution line: message: client-<pid> command: kill-session -t claude-live
91
- expect(text).toMatch(/message: client-\d+ command: kill-session -t claude-live/)
92
- spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
93
- })
94
- })