@agfpd/iapeer 0.2.76 → 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 +20 -13
- 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 -213
- 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
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import {
|
|
7
|
-
BackpressureWriter,
|
|
8
|
-
FRAME_DATA,
|
|
9
|
-
FRAME_RESIZE,
|
|
10
|
-
TERM_RESET,
|
|
11
|
-
capabilityResponses,
|
|
12
|
-
frame,
|
|
13
|
-
keysToBytes,
|
|
14
|
-
makeFramer,
|
|
15
|
-
parseSize,
|
|
16
|
-
sizePayload,
|
|
17
|
-
stripQueries,
|
|
18
|
-
} from './protocol.ts'
|
|
19
|
-
|
|
20
|
-
const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
|
|
21
|
-
|
|
22
|
-
describe('frame protocol', () => {
|
|
23
|
-
test('frame() lays out [type][len:u32be][payload]', () => {
|
|
24
|
-
const f = frame(FRAME_DATA, 'hi')
|
|
25
|
-
expect(f[0]).toBe(FRAME_DATA)
|
|
26
|
-
expect(f.readUInt32BE(1)).toBe(2)
|
|
27
|
-
expect(f.subarray(5).toString()).toBe('hi')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('makeFramer reassembles a frame split across chunk boundaries', () => {
|
|
31
|
-
const got: Array<{ t: number; p: string }> = []
|
|
32
|
-
const fr = makeFramer((t, p) => got.push({ t, p: p.toString() }))
|
|
33
|
-
const f = frame(FRAME_DATA, 'hello world')
|
|
34
|
-
fr(f.subarray(0, 3)) // partial header
|
|
35
|
-
fr(f.subarray(3, 8)) // rest of header + part of payload
|
|
36
|
-
expect(got).toHaveLength(0) // nothing complete yet
|
|
37
|
-
fr(f.subarray(8)) // tail
|
|
38
|
-
expect(got).toEqual([{ t: FRAME_DATA, p: 'hello world' }])
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('makeFramer splits multiple coalesced frames', () => {
|
|
42
|
-
const got: string[] = []
|
|
43
|
-
const fr = makeFramer((_t, p) => got.push(p.toString()))
|
|
44
|
-
fr(Buffer.concat([frame(FRAME_DATA, 'a'), frame(FRAME_DATA, 'bb'), frame(FRAME_DATA, 'ccc')]))
|
|
45
|
-
expect(got).toEqual(['a', 'bb', 'ccc'])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('sizePayload/parseSize round-trip', () => {
|
|
49
|
-
expect(parseSize(sizePayload(132, 50))).toEqual({ cols: 132, rows: 50 })
|
|
50
|
-
expect(frame(FRAME_RESIZE, sizePayload(80, 24))[0]).toBe(FRAME_RESIZE)
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('capability responder (the queries @xterm does not auto-answer)', () => {
|
|
55
|
-
test('secondary-DA-style XTVERSION query → version report', () => {
|
|
56
|
-
expect(capabilityResponses('\x1b[>0q')).toEqual(['\x1bP>|iapeer-pts(0.1)\x1b\\'])
|
|
57
|
-
})
|
|
58
|
-
test('OSC-11 theme query → an rgb background report', () => {
|
|
59
|
-
expect(capabilityResponses('\x1b]11;?\x07')[0]).toContain('\x1b]11;rgb:')
|
|
60
|
-
})
|
|
61
|
-
test('secondary DA (>c) → a DA2 report', () => {
|
|
62
|
-
expect(capabilityResponses('\x1b[>0c')).toEqual(['\x1b[>0;276;0c'])
|
|
63
|
-
})
|
|
64
|
-
test('plain output → no responses', () => {
|
|
65
|
-
expect(capabilityResponses('just some text\r\n')).toEqual([])
|
|
66
|
-
})
|
|
67
|
-
test('stripQueries removes DA1/DSR/theme but keeps real output', () => {
|
|
68
|
-
const s = 'before\x1b[0c\x1b[6nmid\x1b]11;?\x07after'
|
|
69
|
-
const out = stripQueries(s)
|
|
70
|
-
expect(out).toBe('beforemidafter')
|
|
71
|
-
expect(out).not.toContain('\x1b[6n')
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
describe('TERM_RESET (port-dep #1: clean every client exit path)', () => {
|
|
76
|
-
test('resets the leak-prone app-modes', () => {
|
|
77
|
-
expect(TERM_RESET).toContain('\x1b[?1049l') // leave alt-screen
|
|
78
|
-
expect(TERM_RESET).toContain('\x1b[?1000l') // mouse tracking off
|
|
79
|
-
expect(TERM_RESET).toContain('\x1b[?25h') // show cursor
|
|
80
|
-
expect(TERM_RESET).toContain('\x1b[<u') // pop kitty-keyboard
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe('BackpressureWriter (port-dep #2: the ~90 KB-over-8 KB truncation that staled reattach)', () => {
|
|
85
|
-
test('a fully-accepted write leaves nothing queued', () => {
|
|
86
|
-
const w = new BackpressureWriter(b => b.length)
|
|
87
|
-
expect(w.send(Buffer.alloc(100))).toBe(true)
|
|
88
|
-
expect(w.queuedBytes).toBe(0)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('a heavy frame past the socket buffer is QUEUED, then fully delivered across drains', () => {
|
|
92
|
-
const CAP = 8192
|
|
93
|
-
let delivered = 0
|
|
94
|
-
const w = new BackpressureWriter(b => {
|
|
95
|
-
const n = Math.min(CAP, b.length)
|
|
96
|
-
delivered += n
|
|
97
|
-
return n
|
|
98
|
-
})
|
|
99
|
-
const heavy = Buffer.alloc(89305, 0x41) // the measured heavy-claude serialize size
|
|
100
|
-
expect(w.send(heavy)).toBe(true)
|
|
101
|
-
expect(delivered).toBe(CAP) // first write took only the socket buffer
|
|
102
|
-
expect(w.queuedBytes).toBe(89305 - CAP) // the rest is queued, NOT dropped
|
|
103
|
-
let guard = 0
|
|
104
|
-
while (w.queuedBytes > 0 && guard++ < 1000) w.flush() // drain events
|
|
105
|
-
expect(delivered).toBe(89305) // every byte eventually delivered (no truncation)
|
|
106
|
-
expect(w.queuedBytes).toBe(0)
|
|
107
|
-
expect(w.isDead).toBe(false)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('coalesces a new send onto the pending tail (ordering preserved)', () => {
|
|
111
|
-
const CAP = 4
|
|
112
|
-
const chunks: string[] = []
|
|
113
|
-
const w = new BackpressureWriter(b => {
|
|
114
|
-
const n = Math.min(CAP, b.length)
|
|
115
|
-
chunks.push(b.subarray(0, n).toString())
|
|
116
|
-
return n
|
|
117
|
-
})
|
|
118
|
-
w.send(Buffer.from('ABCDEF')) // 'ABCD' written, 'EF' queued
|
|
119
|
-
w.send(Buffer.from('GH')) // coalesced onto 'EF' → 'EFGH'
|
|
120
|
-
let guard = 0
|
|
121
|
-
while (w.queuedBytes > 0 && guard++ < 100) w.flush()
|
|
122
|
-
expect(chunks.join('')).toBe('ABCDEFGH')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test('a dead socket (write throws) → send returns false and the writer is dead', () => {
|
|
126
|
-
const w = new BackpressureWriter(() => {
|
|
127
|
-
throw new Error('EPIPE')
|
|
128
|
-
})
|
|
129
|
-
expect(w.send(Buffer.alloc(10))).toBe(false)
|
|
130
|
-
expect(w.isDead).toBe(true)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('a negative write count → dead', () => {
|
|
134
|
-
const w = new BackpressureWriter(() => -1)
|
|
135
|
-
expect(w.send(Buffer.alloc(10))).toBe(false)
|
|
136
|
-
expect(w.isDead).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
describe('keysToBytes (tmux send-keys vocabulary → pty bytes, the boot key-set)', () => {
|
|
141
|
-
test('Enter → CR (the trust/proceed dialogs)', () => {
|
|
142
|
-
expect(keysToBytes(['Enter'])).toEqual(Buffer.from('\r'))
|
|
143
|
-
})
|
|
144
|
-
test('codex update-decline [2,Enter] → "2" + CR', () => {
|
|
145
|
-
expect(keysToBytes(['2', 'Enter'])).toEqual(Buffer.from('2\r'))
|
|
146
|
-
})
|
|
147
|
-
test('codex hooks-review [Down,Enter] in NORMAL cursor mode → CSI-B + CR', () => {
|
|
148
|
-
expect(keysToBytes(['Down', 'Enter'])).toEqual(Buffer.from('\x1b[B\r'))
|
|
149
|
-
})
|
|
150
|
-
test('[Down,Enter] under DECCKM application-cursor mode → SS3-B + CR (the mode-sensitivity boris flagged)', () => {
|
|
151
|
-
expect(keysToBytes(['Down', 'Enter'], { appCursorKeys: true })).toEqual(Buffer.from('\x1bOB\r'))
|
|
152
|
-
})
|
|
153
|
-
test('claude resume-picker [Down] → cursor-down only', () => {
|
|
154
|
-
expect(keysToBytes(['Down'])).toEqual(Buffer.from('\x1b[B'))
|
|
155
|
-
})
|
|
156
|
-
test('literal mode -l sends the rest verbatim (executeControl /compact)', () => {
|
|
157
|
-
expect(keysToBytes(['-l', '/compact'])).toEqual(Buffer.from('/compact'))
|
|
158
|
-
})
|
|
159
|
-
test('Escape (interrupt) → ESC', () => {
|
|
160
|
-
expect(keysToBytes(['Escape'])).toEqual(Buffer.from('\x1b'))
|
|
161
|
-
})
|
|
162
|
-
test('an unknown token falls through to literal text', () => {
|
|
163
|
-
expect(keysToBytes(['hello'])).toEqual(Buffer.from('hello'))
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// boris's acceptance bar: the translator must be BYTE-IDENTICAL to real `tmux send-keys` for the
|
|
168
|
-
// key-set the adapters actually return — not a "looks right" table. We capture exactly what tmux
|
|
169
|
-
// delivers to a pane (raw mode bypasses the line discipline so no CR↔NL cooking) and compare.
|
|
170
|
-
// Guarded by tmuxAvailable (a clean CI runner has no tmux → these skip, per the live-tmux lesson).
|
|
171
|
-
function tmuxSendKeysBytes(keys: string[], appCursor: boolean, n: number): Buffer {
|
|
172
|
-
const dir = mkdtempSync(join(tmpdir(), 'pts-keys-'))
|
|
173
|
-
const sock = join(dir, 's.sock')
|
|
174
|
-
const out = join(dir, 'out.bin')
|
|
175
|
-
// The pane program: optionally emit DECCKM (CSI ? 1 h) as its OWN output so tmux tracks
|
|
176
|
-
// application-cursor mode; then raw mode (capture send-keys bytes verbatim); then read exactly n
|
|
177
|
-
// bytes and exit (head flushes on exit — a buffered cat would lose the tail on kill).
|
|
178
|
-
const pre = appCursor ? `printf '\\033[?1h'; ` : ''
|
|
179
|
-
const paneCmd = `${pre}stty raw -echo; head -c ${n} > '${out}'`
|
|
180
|
-
try {
|
|
181
|
-
const create = spawnSync(
|
|
182
|
-
'tmux',
|
|
183
|
-
['-S', sock, 'new-session', '-d', '-s', 'k', '-x', '80', '-y', '24', 'sh', '-c', paneCmd],
|
|
184
|
-
{ stdio: 'ignore' },
|
|
185
|
-
)
|
|
186
|
-
if (create.status !== 0) throw new Error('tmux new-session failed')
|
|
187
|
-
Bun.sleepSync(400) // let the pane reach raw mode + tmux process the DECCKM output
|
|
188
|
-
spawnSync('tmux', ['-S', sock, 'send-keys', '-t', 'k', ...keys], { stdio: 'ignore' })
|
|
189
|
-
const deadline = Date.now() + 3000
|
|
190
|
-
while (Date.now() < deadline) {
|
|
191
|
-
if (existsSync(out) && statSync(out).size >= n) break
|
|
192
|
-
Bun.sleepSync(50)
|
|
193
|
-
}
|
|
194
|
-
return existsSync(out) ? readFileSync(out) : Buffer.alloc(0)
|
|
195
|
-
} finally {
|
|
196
|
-
spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
|
|
197
|
-
try {
|
|
198
|
-
rmSync(dir, { recursive: true, force: true })
|
|
199
|
-
} catch {
|
|
200
|
-
/* */
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
describe.if(tmuxAvailable)('keysToBytes byte-identity vs real tmux send-keys (live)', () => {
|
|
206
|
-
test('NORMAL cursor mode: [Down,Enter]', () => {
|
|
207
|
-
const ours = keysToBytes(['Down', 'Enter'])
|
|
208
|
-
expect(tmuxSendKeysBytes(['Down', 'Enter'], false, ours.length)).toEqual(ours)
|
|
209
|
-
})
|
|
210
|
-
test('DECCKM application-cursor mode: [Down,Enter] → SS3', () => {
|
|
211
|
-
const ours = keysToBytes(['Down', 'Enter'], { appCursorKeys: true })
|
|
212
|
-
expect(tmuxSendKeysBytes(['Down', 'Enter'], true, ours.length)).toEqual(ours)
|
|
213
|
-
})
|
|
214
|
-
test('literal digit + Enter: [2,Enter]', () => {
|
|
215
|
-
const ours = keysToBytes(['2', 'Enter'])
|
|
216
|
-
expect(tmuxSendKeysBytes(['2', 'Enter'], false, ours.length)).toEqual(ours)
|
|
217
|
-
})
|
|
218
|
-
test('bare Enter', () => {
|
|
219
|
-
const ours = keysToBytes(['Enter'])
|
|
220
|
-
expect(tmuxSendKeysBytes(['Enter'], false, ours.length)).toEqual(ours)
|
|
221
|
-
})
|
|
222
|
-
})
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Spawn-flip Ф1 serving-wiring — resolveDaemonSelfArgv picks the right re-invocation for the detached
|
|
2
|
-
// supervisor daemon: a real on-disk module (bun/source) runs directly; an EMBEDDED module (a compiled
|
|
3
|
-
// `bun build --compile` standalone, whose self path is NOT on disk) re-invokes the installed binary
|
|
4
|
-
// through its `supervisor` verb. The bug this guards: a bare bunfs-path invocation mis-routes through
|
|
5
|
-
// the CLI (unknown verb → usage), the daemon never comes up, and a hosted launch falls back to tmux.
|
|
6
|
-
import { describe, expect, test } from 'bun:test'
|
|
7
|
-
import { resolveDaemonSelfArgv } from './index.ts'
|
|
8
|
-
|
|
9
|
-
describe('resolveDaemonSelfArgv', () => {
|
|
10
|
-
// Paths are the EMPIRICALLY-verified shapes: bun → real /private/... file; compiled → /$bunfs/root/...
|
|
11
|
-
test('source (real on-disk module) → re-run the module directly via the runtime (bun)', () => {
|
|
12
|
-
expect(resolveDaemonSelfArgv('/Users/x/.bun/bin/bun', '/Users/x/iapeer/src/supervisor/index.ts'))
|
|
13
|
-
.toEqual(['/Users/x/.bun/bin/bun', '/Users/x/iapeer/src/supervisor/index.ts'])
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test('compiled standalone (embedded /$bunfs path) → installed binary + `supervisor` verb', () => {
|
|
17
|
-
expect(resolveDaemonSelfArgv('/Users/x/.local/bin/iapeer', '/$bunfs/root/src/supervisor/index.ts'))
|
|
18
|
-
.toEqual(['/Users/x/.local/bin/iapeer', 'supervisor'])
|
|
19
|
-
// The caller appends `daemon <session> <runtime> --run-dir <dir>` → `iapeer supervisor daemon …`,
|
|
20
|
-
// which the CLI routes to runSupervisorCli('daemon', …). NOT a bare bunfs-path (→ usage dump).
|
|
21
|
-
})
|
|
22
|
-
})
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// Scrollback spinner strip (serve-side repaint snapshot). claude's TUI redraws its status block without
|
|
2
|
-
// a scroll-region, so stale spinner frames accumulate in the @xterm scrollback ring and pollute a
|
|
3
|
-
// reattaching operator's scroll-up. stripSpinnerScrollback drops those frames from the SCROLLBACK part
|
|
4
|
-
// of the serialized snapshot while keeping the live viewport (incl. the current spinner) verbatim,
|
|
5
|
-
// matched by STRUCTURE (gerund + … + elapsed/token or esc-to-interrupt tail), not a word-list.
|
|
6
|
-
import { describe, expect, test } from 'bun:test'
|
|
7
|
-
import { stripSpinnerScrollback } from './daemon.ts'
|
|
8
|
-
|
|
9
|
-
describe('stripSpinnerScrollback — drop stale spinner frames from served scrollback', () => {
|
|
10
|
-
test('strips scrollback spinner-status lines (elapsed + tokens), keeps real history + the live viewport', () => {
|
|
11
|
-
const lines = [
|
|
12
|
-
'real history line 1',
|
|
13
|
-
'\x1b[38;2;215;119;87m✶\x1b[39m Metamorphosing… (4m 24s · ↓ 18.4k tokens)', // scrollback spinner → drop
|
|
14
|
-
'real history line 2',
|
|
15
|
-
'· Quantumizing… (6m 57s · ↓ 30.0k tokens)', // scrollback spinner → drop
|
|
16
|
-
// last 3 = viewport (the LIVE status block, kept verbatim incl. the current spinner)
|
|
17
|
-
'real history line 3',
|
|
18
|
-
'────────────',
|
|
19
|
-
'✽ Brewing… (1m 59s · ↓ 6.5k tokens)',
|
|
20
|
-
].join('\r\n')
|
|
21
|
-
const out = stripSpinnerScrollback(lines, 3).split('\r\n')
|
|
22
|
-
expect(out).toContain('real history line 1')
|
|
23
|
-
expect(out).toContain('real history line 2')
|
|
24
|
-
expect(out).toContain('real history line 3')
|
|
25
|
-
expect(out).toContain('✽ Brewing… (1m 59s · ↓ 6.5k tokens)') // viewport live spinner KEPT
|
|
26
|
-
expect(out.some(l => /Metamorphosing/.test(l))).toBe(false)
|
|
27
|
-
expect(out.some(l => /Quantumizing/.test(l))).toBe(false)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('strips the (esc to interrupt) tail variant in scrollback', () => {
|
|
31
|
-
const lines = ['history', '· Running… (esc to interrupt · 1.2k tokens)', 'vp1', 'vp2', 'vp3'].join('\r\n')
|
|
32
|
-
const out = stripSpinnerScrollback(lines, 3)
|
|
33
|
-
expect(out.includes('Running…')).toBe(false)
|
|
34
|
-
expect(out.includes('history')).toBe(true)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('does NOT over-strip — real content with … or (…) but no spinner tail is kept', () => {
|
|
38
|
-
const lines = [
|
|
39
|
-
'мы обсудили тайминг… он занял время', // … but no elapsed/token tail
|
|
40
|
-
'итого вышло (5 долларов)', // (…) but no elapsed/esc tail
|
|
41
|
-
'vp1', 'vp2', 'vp3',
|
|
42
|
-
].join('\r\n')
|
|
43
|
-
const out = stripSpinnerScrollback(lines, 3).split('\r\n')
|
|
44
|
-
expect(out).toContain('мы обсудили тайминг… он занял время')
|
|
45
|
-
expect(out).toContain('итого вышло (5 долларов)')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('the LIVE spinner in the viewport is never stripped (no scrollback → unchanged)', () => {
|
|
49
|
-
const lines = ['✶ Vibing… (3s · ↓ 1 tokens)', 'vp2', 'vp3'].join('\r\n')
|
|
50
|
-
expect(stripSpinnerScrollback(lines, 3)).toBe(lines) // segs <= viewportRows → returned verbatim
|
|
51
|
-
})
|
|
52
|
-
})
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync, appendFileSync } from 'fs'
|
|
3
|
-
import { tmpdir } from 'os'
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import { compactDoneBaseline, compactTranscriptHasDone, waitForCompactDone, type DeliveryTarget } from './index.ts'
|
|
6
|
-
|
|
7
|
-
function claudeTarget(): DeliveryTarget {
|
|
8
|
-
return { runtime: 'claude', personality: 'tester', address: 'claude-tester', socketPath: '/tmp/iapeer-test-noop.sock' }
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const CLAUDE_IDLE_PANE = '❯ \n bypass permissions on'
|
|
12
|
-
const CLAUDE_BUSY_PANE = '· Compacting conversation… (esc to interrupt)\n reloading context'
|
|
13
|
-
|
|
14
|
-
function withHome(fn: (home: string, env: NodeJS.ProcessEnv) => void): void {
|
|
15
|
-
const home = mkdtempSync(join(tmpdir(), 'iapeer-compact-gate-home-'))
|
|
16
|
-
try {
|
|
17
|
-
fn(home, { ...process.env, HOME: home })
|
|
18
|
-
} finally {
|
|
19
|
-
rmSync(home, { recursive: true, force: true })
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function claudeSlug(cwd: string): string {
|
|
24
|
-
return realpathSync(cwd).replace(/[^a-zA-Z0-9]/g, '-')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('compact done transcript gate', () => {
|
|
28
|
-
test('codex: ignores old markers and detects a new context_compacted event', () => withHome((home, env) => {
|
|
29
|
-
const cwd = mkdtempSync(join(tmpdir(), 'iapeer-compact-codex-cwd-'))
|
|
30
|
-
try {
|
|
31
|
-
const dir = join(home, '.codex', 'sessions', '2026', '06', '13')
|
|
32
|
-
mkdirSync(dir, { recursive: true })
|
|
33
|
-
const file = join(dir, 'rollout-test.jsonl')
|
|
34
|
-
writeFileSync(file,
|
|
35
|
-
JSON.stringify({ type: 'session_meta', payload: { cwd } }) + '\n' +
|
|
36
|
-
JSON.stringify({ type: 'event_msg', payload: { type: 'context_compacted' } }) + '\n',
|
|
37
|
-
)
|
|
38
|
-
const baseline = compactDoneBaseline('codex', cwd, { env })
|
|
39
|
-
expect(compactTranscriptHasDone(baseline, { env })).toBe(false)
|
|
40
|
-
appendFileSync(file, JSON.stringify({ type: 'event_msg', payload: { type: 'context_compacted' } }) + '\n')
|
|
41
|
-
expect(compactTranscriptHasDone(baseline, { env })).toBe(true)
|
|
42
|
-
} finally {
|
|
43
|
-
rmSync(cwd, { recursive: true, force: true })
|
|
44
|
-
}
|
|
45
|
-
}))
|
|
46
|
-
|
|
47
|
-
test('claude: detects a new compact_boundary and ignores raw text mentions', () => withHome((home, env) => {
|
|
48
|
-
const cwd = mkdtempSync(join(tmpdir(), 'iapeer-compact-claude-cwd-'))
|
|
49
|
-
try {
|
|
50
|
-
const dir = join(home, '.claude', 'projects', claudeSlug(cwd))
|
|
51
|
-
mkdirSync(dir, { recursive: true })
|
|
52
|
-
const file = join(dir, 'session.jsonl')
|
|
53
|
-
writeFileSync(file,
|
|
54
|
-
JSON.stringify({ type: 'user', message: { role: 'user', content: 'context_compacted is only text' } }) + '\n' +
|
|
55
|
-
JSON.stringify({ type: 'system', subtype: 'compact_boundary', content: 'Conversation compacted' }) + '\n',
|
|
56
|
-
)
|
|
57
|
-
const baseline = compactDoneBaseline('claude', cwd, { env })
|
|
58
|
-
expect(compactTranscriptHasDone(baseline, { env })).toBe(false)
|
|
59
|
-
appendFileSync(file, JSON.stringify({ type: 'user', message: { role: 'user', content: 'Conversation compacted text only' } }) + '\n')
|
|
60
|
-
expect(compactTranscriptHasDone(baseline, { env })).toBe(false)
|
|
61
|
-
appendFileSync(file, JSON.stringify({ type: 'system', subtype: 'compact_boundary', content: 'Conversation compacted', compactMetadata: { durationMs: 12 } }) + '\n')
|
|
62
|
-
expect(compactTranscriptHasDone(baseline, { env })).toBe(true)
|
|
63
|
-
} finally {
|
|
64
|
-
rmSync(cwd, { recursive: true, force: true })
|
|
65
|
-
}
|
|
66
|
-
}))
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
describe('waitForCompactDone — marker is authoritative, idle is a fast-path', () => {
|
|
70
|
-
function withClaudeMarker(
|
|
71
|
-
fn: (args: { cwd: string; env: NodeJS.ProcessEnv; writeMarker: () => void; baseline: ReturnType<typeof compactDoneBaseline> }) => void,
|
|
72
|
-
): void {
|
|
73
|
-
withHome((home, env) => {
|
|
74
|
-
const cwd = mkdtempSync(join(tmpdir(), 'iapeer-compact-wait-cwd-'))
|
|
75
|
-
try {
|
|
76
|
-
const dir = join(home, '.claude', 'projects', claudeSlug(cwd))
|
|
77
|
-
mkdirSync(dir, { recursive: true })
|
|
78
|
-
const file = join(dir, 'session.jsonl')
|
|
79
|
-
writeFileSync(file, JSON.stringify({ type: 'user', message: { role: 'user', content: 'hi' } }) + '\n')
|
|
80
|
-
const baseline = compactDoneBaseline('claude', cwd, { env })
|
|
81
|
-
const writeMarker = () =>
|
|
82
|
-
appendFileSync(file, JSON.stringify({ type: 'system', subtype: 'compact_boundary', compactMetadata: { durationMs: 140708, preTokens: 535706, postTokens: 12524 } }) + '\n')
|
|
83
|
-
fn({ cwd, env, writeMarker, baseline })
|
|
84
|
-
} finally {
|
|
85
|
-
rmSync(cwd, { recursive: true, force: true })
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// THE FIX: a large-context session that auto-continues into a working turn after compact
|
|
91
|
-
// keeps the composer busy (never idle). The marker proves completion → SUCCESS, not failure.
|
|
92
|
-
test('marker seen + composer never idle → success on the marker (signal: transcript)', () => withClaudeMarker(({ cwd, env, writeMarker, baseline }) => {
|
|
93
|
-
writeMarker()
|
|
94
|
-
const r = waitForCompactDone(claudeTarget(), cwd, baseline, {
|
|
95
|
-
env,
|
|
96
|
-
timeoutMs: 5000,
|
|
97
|
-
pollMs: 5,
|
|
98
|
-
graceMs: 20,
|
|
99
|
-
seam: { sessionAlive: () => true, capturePane: () => CLAUDE_BUSY_PANE },
|
|
100
|
-
})
|
|
101
|
-
expect(r.ok).toBe(true)
|
|
102
|
-
if (r.ok) expect(r.value.signal).toBe('transcript')
|
|
103
|
-
}))
|
|
104
|
-
|
|
105
|
-
// Clean common case unchanged: marker + idle composer → the historical 'transcript+ready'.
|
|
106
|
-
test('marker seen + composer idle → fast-path success (signal: transcript+ready)', () => withClaudeMarker(({ cwd, env, writeMarker, baseline }) => {
|
|
107
|
-
writeMarker()
|
|
108
|
-
const r = waitForCompactDone(claudeTarget(), cwd, baseline, {
|
|
109
|
-
env,
|
|
110
|
-
timeoutMs: 5000,
|
|
111
|
-
pollMs: 5,
|
|
112
|
-
graceMs: 5000,
|
|
113
|
-
seam: { sessionAlive: () => true, capturePane: () => CLAUDE_IDLE_PANE },
|
|
114
|
-
})
|
|
115
|
-
expect(r.ok).toBe(true)
|
|
116
|
-
if (r.ok) expect(r.value.signal).toBe('transcript+ready')
|
|
117
|
-
}))
|
|
118
|
-
|
|
119
|
-
// BOUNDARY preserved: no structured marker ever → honest failure (hung / never completed).
|
|
120
|
-
test('no marker → honest failure', () => withClaudeMarker(({ cwd, env, baseline }) => {
|
|
121
|
-
const r = waitForCompactDone(claudeTarget(), cwd, baseline, {
|
|
122
|
-
env,
|
|
123
|
-
timeoutMs: 40,
|
|
124
|
-
pollMs: 5,
|
|
125
|
-
graceMs: 20,
|
|
126
|
-
seam: { sessionAlive: () => true, capturePane: () => CLAUDE_BUSY_PANE },
|
|
127
|
-
})
|
|
128
|
-
expect(r.ok).toBe(false)
|
|
129
|
-
if (!r.ok) expect(r.error.message).toContain('no transcript compact marker')
|
|
130
|
-
}))
|
|
131
|
-
|
|
132
|
-
// Session death during the wait still fails (liveness boundary).
|
|
133
|
-
test('session vanished → failure', () => withClaudeMarker(({ cwd, env, baseline }) => {
|
|
134
|
-
const r = waitForCompactDone(claudeTarget(), cwd, baseline, {
|
|
135
|
-
env,
|
|
136
|
-
timeoutMs: 5000,
|
|
137
|
-
pollMs: 5,
|
|
138
|
-
graceMs: 20,
|
|
139
|
-
seam: { sessionAlive: () => false, capturePane: () => CLAUDE_BUSY_PANE },
|
|
140
|
-
})
|
|
141
|
-
expect(r.ok).toBe(false)
|
|
142
|
-
if (!r.ok) expect(r.error.message).toContain('session vanished')
|
|
143
|
-
}))
|
|
144
|
-
})
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { ok } from '../core/errors.ts'
|
|
3
|
-
import { getAdapter } from '../launch/index.ts'
|
|
4
|
-
import type { PeerRecord } from '../registry/index.ts'
|
|
5
|
-
import type { ResolvedCaller } from '../identity/index.ts'
|
|
6
|
-
import {
|
|
7
|
-
composerCaptureHasHumanInput,
|
|
8
|
-
createComposerDeliveryQueue,
|
|
9
|
-
type ComposerQueueTryEnqueueArgs,
|
|
10
|
-
type DeliveryTarget,
|
|
11
|
-
} from './index.ts'
|
|
12
|
-
|
|
13
|
-
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
14
|
-
|
|
15
|
-
describe('composerCaptureHasHumanInput (SGR color gate)', () => {
|
|
16
|
-
test('codex: default-fg text after › is human input; faint/grey ghost text is not', () => {
|
|
17
|
-
const markers = getAdapter('codex').deliveryMarkers
|
|
18
|
-
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m hello', markers)).toBe(true)
|
|
19
|
-
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m \x1b[2mghost suggestion\x1b[0m', markers)).toBe(false)
|
|
20
|
-
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m \x1b[38;5;246mplaceholder\x1b[0m', markers)).toBe(false)
|
|
21
|
-
// A suggestion accepted into the composer is no longer grey/faint; it must be held.
|
|
22
|
-
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m accepted suggestion', markers)).toBe(true)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('claude: same color rule is runtime-owned behind the ❯ prompt glyph', () => {
|
|
26
|
-
const markers = getAdapter('claude').deliveryMarkers
|
|
27
|
-
expect(composerCaptureHasHumanInput('\x1b[35m❯\x1b[0m human', markers)).toBe(true)
|
|
28
|
-
expect(composerCaptureHasHumanInput('\x1b[35m❯\x1b[0m \x1b[2mghost\x1b[0m', markers)).toBe(false)
|
|
29
|
-
expect(composerCaptureHasHumanInput('\x1b[35m❯\x1b[0m \x1b[38;5;246mplaceholder\x1b[0m', markers)).toBe(false)
|
|
30
|
-
})
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const callerRecord = {
|
|
34
|
-
personality: 'sender',
|
|
35
|
-
runtime: 'codex',
|
|
36
|
-
runtimes: ['codex'],
|
|
37
|
-
description: '',
|
|
38
|
-
intelligence: 'artificial',
|
|
39
|
-
cwd: '/tmp/sender',
|
|
40
|
-
} as unknown as PeerRecord
|
|
41
|
-
const caller: ResolvedCaller = {
|
|
42
|
-
personality: 'sender',
|
|
43
|
-
runtime: 'codex',
|
|
44
|
-
address: 'codex-sender',
|
|
45
|
-
description: '',
|
|
46
|
-
intelligence: 'artificial',
|
|
47
|
-
cwd: '/tmp/sender',
|
|
48
|
-
record: callerRecord,
|
|
49
|
-
}
|
|
50
|
-
const peer = {
|
|
51
|
-
personality: 'target',
|
|
52
|
-
runtime: 'codex',
|
|
53
|
-
runtimes: ['codex'],
|
|
54
|
-
description: '',
|
|
55
|
-
intelligence: 'artificial',
|
|
56
|
-
cwd: '/tmp/target',
|
|
57
|
-
} as unknown as PeerRecord
|
|
58
|
-
const target: DeliveryTarget = {
|
|
59
|
-
personality: 'target',
|
|
60
|
-
runtime: 'codex',
|
|
61
|
-
address: 'codex-target',
|
|
62
|
-
socketPath: '/tmp/no-such-sock',
|
|
63
|
-
}
|
|
64
|
-
function args(envelope = '<iap>hello</iap>'): ComposerQueueTryEnqueueArgs {
|
|
65
|
-
return { caller, peer, target, envelope, topic: 'q-test' }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
describe('createComposerDeliveryQueue', () => {
|
|
69
|
-
test('queued ack is immediate; drain waits until composer clears, then delivers silently', async () => {
|
|
70
|
-
let held = true
|
|
71
|
-
const delivered: string[] = []
|
|
72
|
-
const failed: string[] = []
|
|
73
|
-
const q = createComposerDeliveryQueue({
|
|
74
|
-
pollMs: 5,
|
|
75
|
-
forceTimeoutMs: 1000,
|
|
76
|
-
shouldQueue: () => true,
|
|
77
|
-
hasHumanInput: () => held,
|
|
78
|
-
sessionToken: () => 'session-1:pane-1',
|
|
79
|
-
sessionAlive: () => true,
|
|
80
|
-
deliver: (_target, envelope) => {
|
|
81
|
-
delivered.push(envelope)
|
|
82
|
-
return ok(undefined)
|
|
83
|
-
},
|
|
84
|
-
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const accepted = await q.tryEnqueue(args('first'))
|
|
88
|
-
expect(accepted?.ok).toBe(true)
|
|
89
|
-
if (!accepted?.ok) return
|
|
90
|
-
expect(accepted.value).toMatchObject({ queued: true, queuedBy: 'composer', queueDepth: 1 })
|
|
91
|
-
await sleep(20)
|
|
92
|
-
expect(delivered).toEqual([])
|
|
93
|
-
held = false
|
|
94
|
-
await sleep(30)
|
|
95
|
-
expect(delivered).toEqual(['first'])
|
|
96
|
-
expect(failed).toEqual([])
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('120s ceiling (env-tunable in tests) force-delivers instead of waiting forever', async () => {
|
|
100
|
-
const delivered: string[] = []
|
|
101
|
-
const failed: string[] = []
|
|
102
|
-
const q = createComposerDeliveryQueue({
|
|
103
|
-
pollMs: 5,
|
|
104
|
-
forceTimeoutMs: 20,
|
|
105
|
-
shouldQueue: () => true,
|
|
106
|
-
hasHumanInput: () => true,
|
|
107
|
-
sessionToken: () => 'session-1:pane-1',
|
|
108
|
-
sessionAlive: () => true,
|
|
109
|
-
deliver: (_target, envelope) => {
|
|
110
|
-
delivered.push(envelope)
|
|
111
|
-
return ok(undefined)
|
|
112
|
-
},
|
|
113
|
-
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
await q.tryEnqueue(args('force-me'))
|
|
117
|
-
await sleep(70)
|
|
118
|
-
expect(delivered).toEqual(['force-me'])
|
|
119
|
-
expect(failed).toEqual([])
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
test('target death/session replacement fails the queued sender instead of delivering to a corpse/new session', async () => {
|
|
123
|
-
let alive = true
|
|
124
|
-
const delivered: string[] = []
|
|
125
|
-
const failed: string[] = []
|
|
126
|
-
const q = createComposerDeliveryQueue({
|
|
127
|
-
pollMs: 5,
|
|
128
|
-
forceTimeoutMs: 1000,
|
|
129
|
-
shouldQueue: () => true,
|
|
130
|
-
hasHumanInput: () => true,
|
|
131
|
-
sessionToken: () => 'session-1:pane-1',
|
|
132
|
-
sessionAlive: () => alive,
|
|
133
|
-
deliver: (_target, envelope) => {
|
|
134
|
-
delivered.push(envelope)
|
|
135
|
-
return ok(undefined)
|
|
136
|
-
},
|
|
137
|
-
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
await q.tryEnqueue(args('must-fail'))
|
|
141
|
-
alive = false
|
|
142
|
-
await sleep(30)
|
|
143
|
-
expect(delivered).toEqual([])
|
|
144
|
-
expect(failed.join('\n')).toMatch(/vanished|replaced/)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
test('async-correctness: concurrent enqueue with ASYNC predicates → each delivered exactly once, no dup/loss (Ф0b-3 3c-2)', async () => {
|
|
148
|
-
// The host-aware refactor makes shouldQueue/hasHumanInput/deliver awaitable. Inject ASYNC seams
|
|
149
|
-
// (a real yield window) and fire concurrent enqueues — the FIFO + per-identity drain guard must
|
|
150
|
-
// still deliver each envelope EXACTLY once (no race-doubled, no race-lost).
|
|
151
|
-
let held = true
|
|
152
|
-
const delivered: string[] = []
|
|
153
|
-
const q = createComposerDeliveryQueue({
|
|
154
|
-
pollMs: 5,
|
|
155
|
-
forceTimeoutMs: 2000,
|
|
156
|
-
shouldQueue: async () => { await sleep(4); return true },
|
|
157
|
-
hasHumanInput: async () => { await sleep(2); return held },
|
|
158
|
-
sessionToken: () => 'session-1:pane-1',
|
|
159
|
-
sessionAlive: () => true,
|
|
160
|
-
deliver: async (_target, envelope) => { await sleep(1); delivered.push(envelope); return ok(undefined) },
|
|
161
|
-
notifyFailed: () => { /* */ },
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const acks = await Promise.all([
|
|
165
|
-
q.tryEnqueue(args('msg-A')),
|
|
166
|
-
q.tryEnqueue(args('msg-B')),
|
|
167
|
-
q.tryEnqueue(args('msg-C')),
|
|
168
|
-
])
|
|
169
|
-
expect(acks.every(a => a?.ok)).toBe(true)
|
|
170
|
-
await sleep(40)
|
|
171
|
-
expect(delivered).toEqual([]) // all three held while busy
|
|
172
|
-
held = false
|
|
173
|
-
await sleep(80)
|
|
174
|
-
expect(delivered.slice().sort()).toEqual(['msg-A', 'msg-B', 'msg-C']) // each ONCE — no dup, no loss
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
test('daemon close/restart failAll re-fails every queued-but-undelivered envelope', async () => {
|
|
178
|
-
const delivered: string[] = []
|
|
179
|
-
const failed: string[] = []
|
|
180
|
-
const q = createComposerDeliveryQueue({
|
|
181
|
-
pollMs: 50,
|
|
182
|
-
forceTimeoutMs: 1000,
|
|
183
|
-
shouldQueue: () => true,
|
|
184
|
-
hasHumanInput: () => true,
|
|
185
|
-
sessionToken: () => 'session-1:pane-1',
|
|
186
|
-
sessionAlive: () => true,
|
|
187
|
-
deliver: (_target, envelope) => {
|
|
188
|
-
delivered.push(envelope)
|
|
189
|
-
return ok(undefined)
|
|
190
|
-
},
|
|
191
|
-
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
await q.tryEnqueue(args('queued-a'))
|
|
195
|
-
await q.tryEnqueue(args('queued-b'))
|
|
196
|
-
await q.failAll?.('daemon shutting down/restarting before queued delivery completed')
|
|
197
|
-
await sleep(10)
|
|
198
|
-
expect(delivered).toEqual([])
|
|
199
|
-
expect(failed).toEqual([
|
|
200
|
-
'daemon shutting down/restarting before queued delivery completed',
|
|
201
|
-
'daemon shutting down/restarting before queued delivery completed',
|
|
202
|
-
])
|
|
203
|
-
})
|
|
204
|
-
})
|