@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.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/package.json +21 -2
- package/src/cli/index.ts +95 -78
- package/src/connect/index.ts +4 -4
- package/src/core/constants.ts +3 -3
- package/src/core/normalize.ts +2 -2
- package/src/create/index.ts +1 -1
- package/src/daemon/index.ts +5 -5
- package/src/daemon/main.ts +9 -9
- package/src/enable/index.ts +19 -18
- package/src/enable/provisionCommand.ts +10 -10
- package/src/identity/index.ts +2 -2
- package/src/identity/profileStandard.ts +8 -8
- package/src/init/index.ts +10 -10
- package/src/install/index.ts +2 -2
- package/src/install/signing.ts +3 -4
- package/src/launch/adapters/claude.ts +7 -7
- package/src/launch/adapters/codex.ts +5 -6
- package/src/launch/canary.ts +8 -8
- package/src/launch/cmdlog.ts +3 -3
- package/src/launch/composeSystemPrompt.ts +2 -1
- package/src/launch/index.ts +5 -6
- package/src/launch/invocation.ts +1 -1
- package/src/launch/launchd.ts +14 -15
- package/src/launch/launchdRun.ts +6 -6
- package/src/launch/nativeMemory.ts +9 -9
- package/src/launch/ptyHost.ts +1 -1
- package/src/launch/readyGateModel.ts +3 -3
- package/src/launch/types.ts +7 -7
- package/src/lifecycle/eventlog.ts +2 -2
- package/src/lifecycle/index.ts +21 -21
- package/src/lifecycle/queue.ts +1 -1
- package/src/onboard/index.ts +19 -13
- package/src/onboard/memory.ts +5 -5
- package/src/onboard/steps.ts +8 -9
- package/src/provision/index.ts +2 -3
- package/src/runtime/deploy.ts +2 -2
- package/src/runtime/index.ts +4 -4
- package/src/shadow/gate.ts +6 -6
- package/src/shadow/index.ts +2 -2
- package/src/status/index.ts +11 -11
- package/src/storage/index.ts +1 -1
- package/src/supervisor/daemon.ts +20 -13
- package/src/transport/index.ts +11 -11
- package/src/update/index.ts +4 -4
- package/src/cli/cli.test.ts +0 -603
- package/src/cli/listTui.test.ts +0 -71
- package/src/codec/codec.test.ts +0 -271
- package/src/connect/connect.test.ts +0 -163
- package/src/core/constants.test.ts +0 -32
- package/src/core/normalize.test.ts +0 -98
- package/src/create/create.test.ts +0 -143
- package/src/daemon/daemon-http.test.ts +0 -114
- package/src/daemon/daemon.test.ts +0 -267
- package/src/daemon/main.test.ts +0 -413
- package/src/enable/enable.test.ts +0 -95
- package/src/enable/provisionCommand.test.ts +0 -107
- package/src/identity/identity.test.ts +0 -279
- package/src/identity/profileStandard.test.ts +0 -204
- package/src/init/init.test.ts +0 -215
- package/src/init/runtime-resolve.test.ts +0 -49
- package/src/install/install.test.ts +0 -66
- package/src/install/signing.test.ts +0 -84
- package/src/launch/bootdeliver.test.ts +0 -108
- package/src/launch/bootstrap.test.ts +0 -56
- package/src/launch/canary.test.ts +0 -346
- package/src/launch/cmdlog.test.ts +0 -94
- package/src/launch/codexHooksTrust.test.ts +0 -248
- package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
- package/src/launch/composeSystemPrompt.test.ts +0 -145
- package/src/launch/hostComposerOccupied.test.ts +0 -63
- package/src/launch/invocation.test.ts +0 -174
- package/src/launch/launch.test.ts +0 -359
- package/src/launch/launchd.test.ts +0 -499
- package/src/launch/nativeMemory.test.ts +0 -193
- package/src/launch/ptyHost.test.ts +0 -38
- package/src/launch/readyGateModel.test.ts +0 -72
- package/src/launch/sockdir.test.ts +0 -74
- package/src/lifecycle/eventlog.test.ts +0 -114
- package/src/lifecycle/lifecycle.test.ts +0 -1056
- package/src/lifecycle/queue.test.ts +0 -185
- package/src/onboard/memory.test.ts +0 -157
- package/src/onboard/onboard.test.ts +0 -39
- package/src/onboard/steps.test.ts +0 -201
- package/src/provision/provision.test.ts +0 -288
- package/src/registry/registry.test.ts +0 -432
- package/src/runtime/runtime.test.ts +0 -226
- package/src/runtime/update.test.ts +0 -192
- package/src/shadow/enumerate-skip.test.ts +0 -67
- package/src/shadow/gate.test.ts +0 -286
- package/src/shadow/install.test.ts +0 -153
- package/src/shadow/shadow.test.ts +0 -42
- package/src/status/status.test.ts +0 -208
- package/src/storage/peers-home.test.ts +0 -34
- package/src/storage/rotatelog.test.ts +0 -44
- package/src/storage/storage.test.ts +0 -65
- package/src/supervisor/attached-marker.test.ts +0 -84
- package/src/supervisor/boot.test.ts +0 -66
- package/src/supervisor/client.attach.test.ts +0 -107
- package/src/supervisor/daemon.test.ts +0 -213
- package/src/supervisor/deliver.test.ts +0 -58
- package/src/supervisor/panelog.test.ts +0 -95
- package/src/supervisor/protocol.test.ts +0 -222
- package/src/supervisor/selfargv.test.ts +0 -22
- package/src/supervisor/spinner-strip.test.ts +0 -52
- package/src/transport/compactDone.test.ts +0 -144
- package/src/transport/composerQueue.test.ts +0 -204
- package/src/transport/hostControl.test.ts +0 -87
- package/src/transport/hostDeliver.test.ts +0 -137
- package/src/transport/hostLive.test.ts +0 -50
- package/src/transport/routesend-wake.test.ts +0 -357
- package/src/update/update.test.ts +0 -208
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { codexAdapter } from '../launch/adapters/codex.ts'
|
|
3
|
-
import { claudeAdapter } from '../launch/adapters/claude.ts'
|
|
4
|
-
import { nextBootAction, type BootAction } from './boot.ts'
|
|
5
|
-
|
|
6
|
-
// Fed the REAL runtime adapters (codexAdapter/claudeAdapter) — not fakes — so the test proves the
|
|
7
|
-
// supervisor answers the exact dialogs the launch primitive answers, with byte-correct keys.
|
|
8
|
-
const bytesOf = (a: BootAction): Buffer => (a.kind === 'dialog' ? a.bytes : Buffer.alloc(0))
|
|
9
|
-
|
|
10
|
-
describe('nextBootAction — codex startup dialogs answered off the model', () => {
|
|
11
|
-
test('dir-trust → Enter (CR)', () => {
|
|
12
|
-
const a = nextBootAction(codexAdapter, 'Do you trust the contents of this directory?\n 1. Yes, continue')
|
|
13
|
-
expect(a.kind).toBe('dialog')
|
|
14
|
-
expect(bytesOf(a)).toEqual(Buffer.from('\r'))
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('update offer → decline [2,Enter], and NOT ready while "Press enter to continue" is up', () => {
|
|
18
|
-
const pane = 'Update available! 9.9.9\nPress enter to continue'
|
|
19
|
-
const a = nextBootAction(codexAdapter, pane)
|
|
20
|
-
expect(a.kind).toBe('dialog')
|
|
21
|
-
expect(bytesOf(a)).toEqual(Buffer.from('2\r'))
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('hooks-review → [Down,Enter]; the cursor byte follows the model cursor mode', () => {
|
|
25
|
-
const pane = 'Hooks need review\n 1. Review 2. Trust all and continue'
|
|
26
|
-
expect(bytesOf(nextBootAction(codexAdapter, pane))).toEqual(Buffer.from('\x1b[B\r')) // normal
|
|
27
|
-
expect(bytesOf(nextBootAction(codexAdapter, pane, { appCursorKeys: true }))).toEqual(Buffer.from('\x1bOB\r')) // DECCKM
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('composer rendered, no startup screen → ready', () => {
|
|
31
|
-
expect(nextBootAction(codexAdapter, 'a rotating tip line\n› ').kind).toBe('ready')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('splash up, composer not yet rendered → wait', () => {
|
|
35
|
-
expect(nextBootAction(codexAdapter, 'OpenAI Codex\n(loading session)').kind).toBe('wait')
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('nextBootAction — claude startup dialogs answered off the model', () => {
|
|
40
|
-
test('folder-trust → Enter (CR)', () => {
|
|
41
|
-
const a = nextBootAction(claudeAdapter, 'Do you trust this folder?\n 1. Yes')
|
|
42
|
-
expect(a.kind).toBe('dialog')
|
|
43
|
-
expect(bytesOf(a)).toEqual(Buffer.from('\r'))
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('resume-picker before the cursor reaches option 2 → Down (mode-correct cursor byte)', () => {
|
|
47
|
-
const pane = 'Resume from summary\n ❯ 1. Start fresh\n 2. Resume full session'
|
|
48
|
-
const a = nextBootAction(claudeAdapter, pane)
|
|
49
|
-
expect(a.kind).toBe('dialog')
|
|
50
|
-
expect(bytesOf(a)).toEqual(Buffer.from('\x1b[B')) // normal cursor mode
|
|
51
|
-
expect(bytesOf(nextBootAction(claudeAdapter, pane, { appCursorKeys: true }))).toEqual(Buffer.from('\x1bOB'))
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('resume-picker once the cursor IS on option 2 → Enter (never re-Downs past the proceed row)', () => {
|
|
55
|
-
const pane = 'Resume from summary\n 1. Start fresh\n ❯ 2. Resume full session'
|
|
56
|
-
expect(bytesOf(nextBootAction(claudeAdapter, pane))).toEqual(Buffer.from('\r'))
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('input row + bypass banner → ready', () => {
|
|
60
|
-
expect(nextBootAction(claudeAdapter, '❯ \n bypass permissions on').kind).toBe('ready')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('splash up, input row not yet rendered → wait', () => {
|
|
64
|
-
expect(nextBootAction(claudeAdapter, 'Welcome to Claude Code\n (starting)').kind).toBe('wait')
|
|
65
|
-
})
|
|
66
|
-
})
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { killSession, sessionAlive, startSupervisorDaemon } from './index.ts'
|
|
6
|
-
|
|
7
|
-
// SURFACE-5 regression — the interactive attach-and-stay loop. The other supervisor tests attach a
|
|
8
|
-
// RAW socket; none drives the real `runSupervisorClient` under a TTY, so the canary's first live
|
|
9
|
-
// `iapeer attach` exposed a bug invisible to them: runSupervisorClient resolved its Promise the
|
|
10
|
-
// instant Bun.connect returned (setup done), the CLI caller ran `await runSupervisorClient(); return
|
|
11
|
-
// 0` → runCli's `.then(process.exit)`, and the just-attached client was torn down within
|
|
12
|
-
// milliseconds (handshake-then-exit). The fix makes the client BLOCK (never-resolve) after setup so
|
|
13
|
-
// process.exit from a terminal path owns termination — exactly the detached `daemon` case's guard.
|
|
14
|
-
//
|
|
15
|
-
// This is the headless verifier of that fix AND the missing surface-5 automation: allocate a REAL
|
|
16
|
-
// Bun-native pty, run the actual client CLI in it, assert it STAYS attached + renders live frames +
|
|
17
|
-
// Ctrl-] detaches cleanly while the session keeps running. Arthur's live attach is the final human
|
|
18
|
-
// seal; this test makes every iteration of the fix verifiable WITHOUT pulling him in.
|
|
19
|
-
// Gated on Bun-native PTY (≥1.3.5) so a runner without it skips rather than reds.
|
|
20
|
-
const [maj, min, pat] = (Bun.version || '0.0.0').split('.').map(Number) as [number, number, number]
|
|
21
|
-
const bunPty = maj > 1 || (maj === 1 && (min > 3 || (min === 3 && pat >= 5)))
|
|
22
|
-
const d = bunPty ? describe : describe.skip
|
|
23
|
-
|
|
24
|
-
const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms))
|
|
25
|
-
async function waitFor(pred: () => boolean, timeoutMs = 4000, step = 50): Promise<boolean> {
|
|
26
|
-
const end = Date.now() + timeoutMs
|
|
27
|
-
while (Date.now() < end) {
|
|
28
|
-
if (pred()) return true
|
|
29
|
-
await sleep(step)
|
|
30
|
-
}
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Spawn the real attach client under a real pty (its stdin/stdout is a TTY → raw-mode path runs).
|
|
35
|
-
interface PtyClient {
|
|
36
|
-
out(): string
|
|
37
|
-
exited(): boolean
|
|
38
|
-
sendKey(byte: number): void
|
|
39
|
-
}
|
|
40
|
-
function spawnAttachClient(runDir: string, session: string): PtyClient {
|
|
41
|
-
let buf = ''
|
|
42
|
-
let done = false
|
|
43
|
-
const proc = Bun.spawn(['bun', 'src/supervisor/index.ts', 'attach', session, '--run-dir', runDir], {
|
|
44
|
-
cwd: process.cwd(),
|
|
45
|
-
env: { ...process.env },
|
|
46
|
-
// a real Bun-native pty pair: the client sees a TTY on stdin (isTTY → setRawMode runs).
|
|
47
|
-
terminal: {
|
|
48
|
-
cols: 80,
|
|
49
|
-
rows: 24,
|
|
50
|
-
name: 'xterm-256color',
|
|
51
|
-
data(_t: unknown, data: Uint8Array | string) {
|
|
52
|
-
buf += Buffer.from(data as Uint8Array).toString('latin1')
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Bun pty spawn opts not yet in @types
|
|
56
|
-
} as any)
|
|
57
|
-
proc.exited.then(() => {
|
|
58
|
-
done = true
|
|
59
|
-
})
|
|
60
|
-
return {
|
|
61
|
-
out: () => buf,
|
|
62
|
-
exited: () => done,
|
|
63
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- proc.terminal is the pty master writer
|
|
64
|
-
sendKey: (byte: number) => void (proc as any).terminal.write(Buffer.from([byte])),
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
d('runSupervisorClient interactive attach (surface-5, Bun-native pty)', () => {
|
|
69
|
-
let runDir: string
|
|
70
|
-
const session = 'ptsattach'
|
|
71
|
-
|
|
72
|
-
beforeAll(() => {
|
|
73
|
-
runDir = mkdtempSync(join(tmpdir(), 'iapeer-attach-'))
|
|
74
|
-
startSupervisorDaemon({ session, runtime: 'tick', runDir })
|
|
75
|
-
})
|
|
76
|
-
afterAll(() => {
|
|
77
|
-
try {
|
|
78
|
-
killSession(runDir, session)
|
|
79
|
-
} catch {
|
|
80
|
-
/* */
|
|
81
|
-
}
|
|
82
|
-
rmSync(runDir, { recursive: true, force: true })
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('the client STAYS attached after the handshake and keeps rendering live frames', async () => {
|
|
86
|
-
expect(sessionAlive(runDir, session)).toBe(true)
|
|
87
|
-
const c = spawnAttachClient(runDir, session)
|
|
88
|
-
|
|
89
|
-
// handshake: the attach line lands (connect + attach + terminal-setup succeeded).
|
|
90
|
-
expect(await waitFor(() => /\[supervisor\] attached to/.test(c.out()))).toBe(true)
|
|
91
|
-
|
|
92
|
-
// THE BUG was a handshake-then-exit within ~ms. Assert the client is STILL attached well past
|
|
93
|
-
// that window AND that live child output (STREAMTICK from the tick runtime) is reaching it —
|
|
94
|
-
// proving the attach-and-stay loop holds, not just that the process happens to linger.
|
|
95
|
-
await sleep(2000)
|
|
96
|
-
expect(c.exited()).toBe(false) // RED on the pre-fix early-resolve (exited ~ms after handshake)
|
|
97
|
-
expect(/STREAMTICK_\d+/.test(c.out())).toBe(true) // live frames flow through the held attach
|
|
98
|
-
|
|
99
|
-
// Ctrl-] (0x1d) detaches: the client exits cleanly, prints the DETACH message (not the
|
|
100
|
-
// misleading "session ended"), and the session keeps running.
|
|
101
|
-
c.sendKey(0x1d)
|
|
102
|
-
expect(await waitFor(() => c.exited())).toBe(true)
|
|
103
|
-
expect(/detached/.test(c.out())).toBe(true)
|
|
104
|
-
expect(/session ended/.test(c.out())).toBe(false) // detach must not claim the session ended
|
|
105
|
-
expect(sessionAlive(runDir, session)).toBe(true) // detach kept the daemon alive
|
|
106
|
-
})
|
|
107
|
-
})
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { FRAME_DATA, FRAME_RESIZE, frame, makeFramer, sizePayload } from './protocol.ts'
|
|
6
|
-
import { killSession, listSessions, sessionAlive, sockPath, startSupervisorDaemon } from './index.ts'
|
|
7
|
-
|
|
8
|
-
// Integration: a REAL Bun-native pty daemon over a real unix socket, driven by the deterministic
|
|
9
|
-
// `tick` runtime (no claude/codex needed) — hermetic (temp run dir, throwaway session). Proves
|
|
10
|
-
// boris's Block-2 acceptance points: spawn + responder-served boot + attach repaint + live
|
|
11
|
-
// forward + detach-survival + REATTACH FRESHNESS (the heavy-reattach bug the port-deps fixed).
|
|
12
|
-
// Gated on Bun-native PTY (≥1.3.5) so a runner without it skips rather than reds.
|
|
13
|
-
const [maj, min, pat] = (Bun.version || '0.0.0').split('.').map(Number) as [number, number, number]
|
|
14
|
-
const bunPty = maj > 1 || (maj === 1 && (min > 3 || (min === 3 && pat >= 5)))
|
|
15
|
-
const d = bunPty ? describe : describe.skip
|
|
16
|
-
|
|
17
|
-
const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms))
|
|
18
|
-
async function waitFor(pred: () => boolean, timeoutMs = 5000, step = 50): Promise<boolean> {
|
|
19
|
-
const end = Date.now() + timeoutMs
|
|
20
|
-
while (Date.now() < end) {
|
|
21
|
-
if (pred()) return true
|
|
22
|
-
await sleep(step)
|
|
23
|
-
}
|
|
24
|
-
return false
|
|
25
|
-
}
|
|
26
|
-
const maxTick = (s: string): number => {
|
|
27
|
-
let m = -1
|
|
28
|
-
for (const x of s.matchAll(/STREAMTICK_(\d+)/g)) m = Math.max(m, Number(x[1]))
|
|
29
|
-
return m
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface Client {
|
|
33
|
-
text(): string
|
|
34
|
-
close(): void
|
|
35
|
-
}
|
|
36
|
-
async function attach(runDir: string, session: string, cols = 80, rows = 24): Promise<Client> {
|
|
37
|
-
const frames: Buffer[] = []
|
|
38
|
-
const framer = makeFramer((t, p) => {
|
|
39
|
-
if (t === FRAME_DATA) frames.push(Buffer.from(p))
|
|
40
|
-
})
|
|
41
|
-
const sock = await Bun.connect({
|
|
42
|
-
unix: sockPath(runDir, session),
|
|
43
|
-
socket: {
|
|
44
|
-
open(s) {
|
|
45
|
-
s.write(frame(FRAME_RESIZE, sizePayload(cols, rows)))
|
|
46
|
-
},
|
|
47
|
-
data(_s, dd) {
|
|
48
|
-
framer(dd)
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
return {
|
|
53
|
-
text: () => Buffer.concat(frames).toString('latin1'),
|
|
54
|
-
close: () => {
|
|
55
|
-
try {
|
|
56
|
-
sock.end()
|
|
57
|
-
} catch {
|
|
58
|
-
/* */
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
d('supervisor daemon (Bun-native pty, tick runtime)', () => {
|
|
65
|
-
let runDir: string
|
|
66
|
-
const session = 'ptstest'
|
|
67
|
-
|
|
68
|
-
beforeAll(() => {
|
|
69
|
-
runDir = mkdtempSync(join(tmpdir(), 'iapeer-supervisor-'))
|
|
70
|
-
})
|
|
71
|
-
afterAll(() => {
|
|
72
|
-
try {
|
|
73
|
-
killSession(runDir, session)
|
|
74
|
-
} catch {
|
|
75
|
-
/* */
|
|
76
|
-
}
|
|
77
|
-
rmSync(runDir, { recursive: true, force: true })
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('spawns a detached daemon that comes up live', () => {
|
|
81
|
-
const r = startSupervisorDaemon({ session, runtime: 'tick', runDir })
|
|
82
|
-
expect(r.state).toBe('started')
|
|
83
|
-
expect(sessionAlive(runDir, session)).toBe(true)
|
|
84
|
-
expect(listSessions(runDir).some(s => s.session === session && s.alive)).toBe(true)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('attach forwards live child output (the pty conduit works end-to-end)', async () => {
|
|
88
|
-
const c = await attach(runDir, session)
|
|
89
|
-
const got = await waitFor(() => /STREAMTICK_\d+/.test(c.text()))
|
|
90
|
-
expect(got).toBe(true) // live STREAMTICK_N stream reached the client
|
|
91
|
-
c.close()
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('detach keeps the daemon alive (detach-persistence)', async () => {
|
|
95
|
-
await sleep(150)
|
|
96
|
-
expect(sessionAlive(runDir, session)).toBe(true)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('REATTACH repaints the CURRENT frame, not a stale one (port-dep #2 freshness)', async () => {
|
|
100
|
-
const first = await attach(runDir, session)
|
|
101
|
-
await waitFor(() => maxTick(first.text()) >= 0)
|
|
102
|
-
const base = maxTick(first.text())
|
|
103
|
-
first.close()
|
|
104
|
-
await sleep(800) // the stream advances while detached
|
|
105
|
-
const second = await attach(runDir, session)
|
|
106
|
-
// the reattach repaint (serialize of the CURRENT screen) must reflect ticks emitted AFTER the
|
|
107
|
-
// first detach — a stale close-moment frame would show base, not a higher index.
|
|
108
|
-
const fresh = await waitFor(() => maxTick(second.text()) > base, 5000)
|
|
109
|
-
expect(fresh).toBe(true)
|
|
110
|
-
second.close()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// Serving seam (slice b): a serve-spec is written before spawn, the detached daemon reads it, and
|
|
114
|
-
// the composed argv + env + cwd reach the child VERBATIM (with the pty-host TERM layered on top and
|
|
115
|
-
// TMUX stripped). A probe child dumps the markers it actually received — proving the supervisor
|
|
116
|
-
// serves a launch-composed invocation faithfully (the parity foundation; the env-identity ABI rides
|
|
117
|
-
// in here at serving time).
|
|
118
|
-
test('serving seam: composed argv + env + cwd reach the child verbatim (slice b)', async () => {
|
|
119
|
-
const servingSession = 'ptsserve'
|
|
120
|
-
const childCwd = realpathSync(mkdtempSync(join(tmpdir(), 'iapeer-serve-cwd-')))
|
|
121
|
-
const out = join(runDir, 'serve-probe.out')
|
|
122
|
-
try {
|
|
123
|
-
const serve = {
|
|
124
|
-
// dump what the child ACTUALLY received: a composed env var, its cwd, TERM, and TMUX-presence
|
|
125
|
-
argv: [
|
|
126
|
-
'sh',
|
|
127
|
-
'-c',
|
|
128
|
-
'printf "%s\\n%s\\n%s\\n%s\\n" "$SERVE_MARKER" "$(pwd -P)" "$TERM" "${TMUX:-no-tmux}" > "$OUTFILE"; exec sleep 30',
|
|
129
|
-
],
|
|
130
|
-
env: { SERVE_MARKER: 'identity-abi-xyz', OUTFILE: out, PATH: process.env.PATH ?? '', TMUX: 'should-be-stripped' },
|
|
131
|
-
cwd: childCwd,
|
|
132
|
-
}
|
|
133
|
-
const r = startSupervisorDaemon({ session: servingSession, runtime: 'claude', runDir, serve })
|
|
134
|
-
expect(r.state).toBe('started')
|
|
135
|
-
const wrote = await waitFor(() => existsSync(out) && readFileSync(out, 'utf8').trim().split('\n').length >= 4)
|
|
136
|
-
expect(wrote).toBe(true)
|
|
137
|
-
const [marker, pwd, term, tmux] = readFileSync(out, 'utf8').split('\n')
|
|
138
|
-
expect(marker).toBe('identity-abi-xyz') // composed env reached the child verbatim
|
|
139
|
-
expect(pwd).toBe(childCwd) // composed cwd honored
|
|
140
|
-
expect(term).toBe('xterm-256color') // pty-host TERM layered on top of the composed env
|
|
141
|
-
expect(tmux).toBe('no-tmux') // TMUX stripped — a served child never thinks it is inside tmux
|
|
142
|
-
} finally {
|
|
143
|
-
killSession(runDir, servingSession)
|
|
144
|
-
rmSync(childCwd, { recursive: true, force: true })
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
// Exit-cause (spawn-flip Ф0b-1 resilience-b): when the hosted child dies, the daemon records a
|
|
149
|
-
// `ev=session-exit … host=supervisor` line in exits.log — the cause the supervise-tick death
|
|
150
|
-
// classification + crash-loop accounting need (mirror of the tmux pane-died hook). Kill the child,
|
|
151
|
-
// assert the line lands.
|
|
152
|
-
test('exit-cause: the daemon records the hosted child death in exits.log (resilience-b)', async () => {
|
|
153
|
-
const exSession = 'ptsexit'
|
|
154
|
-
const exitLog = join(runDir, 'exits.log')
|
|
155
|
-
const childPidFile = join(runDir, 'exit-child.pid')
|
|
156
|
-
try {
|
|
157
|
-
const r = startSupervisorDaemon({
|
|
158
|
-
session: exSession,
|
|
159
|
-
runtime: 'tick',
|
|
160
|
-
runDir,
|
|
161
|
-
serve: { argv: ['sh', '-c', `echo $$ > '${childPidFile}'; exec sleep 30`], env: { PATH: process.env.PATH ?? '' }, cwd: runDir, exitLogPath: exitLog },
|
|
162
|
-
})
|
|
163
|
-
expect(r.state).toBe('started')
|
|
164
|
-
const got = await waitFor(() => existsSync(childPidFile))
|
|
165
|
-
expect(got).toBe(true)
|
|
166
|
-
process.kill(Number(readFileSync(childPidFile, 'utf8').trim()), 'SIGKILL')
|
|
167
|
-
const recorded = await waitFor(() => existsSync(exitLog) && /ev=session-exit identity=ptsexit.*host=supervisor/.test(readFileSync(exitLog, 'utf8')))
|
|
168
|
-
expect(recorded).toBe(true) // the death cause is in exits.log → crash-loop accounting has it
|
|
169
|
-
} finally {
|
|
170
|
-
try {
|
|
171
|
-
killSession(runDir, exSession)
|
|
172
|
-
} catch {
|
|
173
|
-
/* */
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// FINDING-2 — restore the SERVE geometry on last detach. The daemon resizes the child pty to a
|
|
179
|
-
// client's size on attach and (before this) never restored it, so an unattached session stuck at a
|
|
180
|
-
// departed client's size → the fixed-geometry warm-deliver/ready-gate viewport model mismatched the
|
|
181
|
-
// pty. Drive it with a child that prints its live pty winsize ("ROWS COLS") to the pane-log: attach
|
|
182
|
-
// small → pty shrinks; detach → pty restores to the serve geometry.
|
|
183
|
-
test('FINDING-2: last detach restores the serve geometry (no sticking at the client size)', async () => {
|
|
184
|
-
const gSession = 'ptsgeo'
|
|
185
|
-
const paneLog = join(runDir, 'geo-pane.log')
|
|
186
|
-
const lastSize = (): string => {
|
|
187
|
-
const t = existsSync(paneLog) ? readFileSync(paneLog, 'utf8') : ''
|
|
188
|
-
const m = [...t.matchAll(/(\d+) (\d+)/g)]
|
|
189
|
-
return m.length ? `${m[m.length - 1][1]}x${m[m.length - 1][2]}` : ''
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
const r = startSupervisorDaemon({
|
|
193
|
-
session: gSession,
|
|
194
|
-
runtime: 'tick',
|
|
195
|
-
runDir,
|
|
196
|
-
// serve geometry 200x50 (distinct from defaults); child reports its live pty winsize as "rows cols"
|
|
197
|
-
serve: { argv: ['sh', '-c', 'while :; do stty size 2>/dev/null; sleep 0.3; done'], env: { PATH: process.env.PATH ?? '' }, cwd: runDir, paneLogPath: paneLog, cols: 200, rows: 50 },
|
|
198
|
-
})
|
|
199
|
-
expect(r.state).toBe('started')
|
|
200
|
-
expect(await waitFor(() => lastSize() === '50 200'.replace(' ', 'x'), 6000)).toBe(true) // at serve geometry
|
|
201
|
-
const c = await attach(runDir, gSession, 90, 30) // attach at a DIFFERENT size → pty resizes to it
|
|
202
|
-
expect(await waitFor(() => lastSize() === '30x90', 6000)).toBe(true) // pty shrank to the client size
|
|
203
|
-
c.close() // last client gone
|
|
204
|
-
expect(await waitFor(() => lastSize() === '50x200', 6000)).toBe(true) // RESTORED to serve geometry
|
|
205
|
-
} finally {
|
|
206
|
-
try {
|
|
207
|
-
killSession(runDir, gSession)
|
|
208
|
-
} catch {
|
|
209
|
-
/* */
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { deliverToHost } from './deliver.ts'
|
|
6
|
-
import { killSession, startSupervisorDaemon } from './index.ts'
|
|
7
|
-
|
|
8
|
-
// The deliver leaf is the host equivalent of tmux load-buffer/paste-buffer/send-keys: it must put the
|
|
9
|
-
// message on the hosted child's pty input as a bracketed paste followed by a CR. Hermetic: a probe
|
|
10
|
-
// child captures its raw stdin (stty raw bypasses the line discipline) so we assert the EXACT bytes
|
|
11
|
-
// the child received. Guarded on Bun-native pty.
|
|
12
|
-
const [maj, min, pat] = (Bun.version || '0.0.0').split('.').map(Number) as [number, number, number]
|
|
13
|
-
const bunPty = maj > 1 || (maj === 1 && (min > 3 || (min === 3 && pat >= 5)))
|
|
14
|
-
const d = bunPty ? describe : describe.skip
|
|
15
|
-
const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms))
|
|
16
|
-
|
|
17
|
-
d('deliverToHost (supervisor-socket delivery — bracketed paste + CR)', () => {
|
|
18
|
-
test('the child receives exactly: ESC[200~ <message> ESC[201~ CR', async () => {
|
|
19
|
-
const runDir = mkdtempSync(join(tmpdir(), 'iapeer-deliver-'))
|
|
20
|
-
const out = join(runDir, 'stdin.bin')
|
|
21
|
-
const msg = 'hello-deliver-❯'
|
|
22
|
-
const expected = `\x1b[200~${msg}\x1b[201~\r`
|
|
23
|
-
const n = Buffer.byteLength(expected, 'utf8')
|
|
24
|
-
try {
|
|
25
|
-
// probe child: raw mode (no line-discipline cooking), read exactly n bytes, dump to a file
|
|
26
|
-
const argv = ['sh', '-c', `stty raw -echo; head -c ${n} > '${out}'; sleep 5`]
|
|
27
|
-
const r = startSupervisorDaemon({ session: 'dlv', runtime: 'tick', runDir, serve: { argv, env: { PATH: process.env.PATH ?? '' }, cwd: runDir } })
|
|
28
|
-
expect(r.state).toBe('started')
|
|
29
|
-
await sleep(400) // let the probe reach raw mode
|
|
30
|
-
const res = await deliverToHost(runDir, 'dlv', msg)
|
|
31
|
-
expect(res.ok).toBe(true)
|
|
32
|
-
// wait for the child to have captured n bytes
|
|
33
|
-
const deadline = Date.now() + 3000
|
|
34
|
-
while (Date.now() < deadline && !(existsSync(out) && readFileSync(out).length >= n)) await sleep(50)
|
|
35
|
-
const got = existsSync(out) ? readFileSync(out) : Buffer.alloc(0)
|
|
36
|
-
expect(got.equals(Buffer.from(expected, 'utf8'))).toBe(true) // bracketed paste + CR, byte-exact
|
|
37
|
-
} finally {
|
|
38
|
-
try {
|
|
39
|
-
killSession(runDir, 'dlv')
|
|
40
|
-
} catch {
|
|
41
|
-
/* */
|
|
42
|
-
}
|
|
43
|
-
await sleep(300)
|
|
44
|
-
rmSync(runDir, { recursive: true, force: true })
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('no supervisor socket → a clean error, not a throw', async () => {
|
|
49
|
-
const runDir = mkdtempSync(join(tmpdir(), 'iapeer-deliver-none-'))
|
|
50
|
-
try {
|
|
51
|
-
const res = await deliverToHost(runDir, 'nope', 'x')
|
|
52
|
-
expect(res.ok).toBe(false)
|
|
53
|
-
expect(res.error).toContain('no supervisor socket')
|
|
54
|
-
} finally {
|
|
55
|
-
rmSync(runDir, { recursive: true, force: true })
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
})
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { Terminal } from '@xterm/headless'
|
|
7
|
-
import { modelToPlainText } from '../shadow/render.ts'
|
|
8
|
-
import { killSession, startSupervisorDaemon } from './index.ts'
|
|
9
|
-
|
|
10
|
-
// Slice c — the supervisor pane-log must be a drop-in for tmux pipe-pane: the SAME raw child bytes to
|
|
11
|
-
// <identity>.log (so observer/shadow/readyGateViewport read a served session identically), AND a model
|
|
12
|
-
// built from it must render what tmux's capture-pane shows. Live tmux + Bun-native pty → guarded.
|
|
13
|
-
const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
|
|
14
|
-
const [maj, min, pat] = (Bun.version || '0.0.0').split('.').map(Number) as [number, number, number]
|
|
15
|
-
const bunPty = maj > 1 || (maj === 1 && (min > 3 || (min === 3 && pat >= 5)))
|
|
16
|
-
const d = tmuxAvailable && bunPty ? describe : describe.skip
|
|
17
|
-
|
|
18
|
-
const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms))
|
|
19
|
-
const readBuf = (p: string): Buffer => (existsSync(p) ? readFileSync(p) : Buffer.alloc(0))
|
|
20
|
-
const modelOf = (bytes: Buffer, cols: number, rows: number): Promise<string> => {
|
|
21
|
-
const t = new Terminal({ cols, rows, allowProposedApi: true, scrollback: 1000 })
|
|
22
|
-
return new Promise<string>(res => t.write(bytes, () => res(modelToPlainText(t, cols, rows))))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
d('supervisor pane-log ≡ tmux pipe-pane', () => {
|
|
26
|
-
test('RAW-BYTE-IDENTITY: the same child yields byte-identical logs (LF/CRLF + multibyte + ANSI)', async () => {
|
|
27
|
-
const dir = mkdtempSync(join(tmpdir(), 'panelog-bp-'))
|
|
28
|
-
const tmuxSock = join(dir, 't.sock')
|
|
29
|
-
const logTmux = join(dir, 'tmux.log')
|
|
30
|
-
const logSup = join(dir, 'sup.log')
|
|
31
|
-
const runDir = join(dir, 'run')
|
|
32
|
-
// bare \n, \r\n, multibyte ❯ › … é (the latin1 grabli), ANSI SGR — the byte classes that diverge
|
|
33
|
-
const child = ['sh', '-c', `sleep 0.5; printf 'L1\\nL2\\r\\ng:❯›…é\\n\\033[31mRED\\033[0m\\nTAB\\tEND\\n'; sleep 1`]
|
|
34
|
-
try {
|
|
35
|
-
spawnSync('tmux', ['-S', tmuxSock, 'new-session', '-d', '-s', 'p', '-x', '80', '-y', '24', ...child], { stdio: 'ignore' })
|
|
36
|
-
spawnSync('tmux', ['-S', tmuxSock, 'pipe-pane', '-o', '-t', 'p', `cat >> '${logTmux}'`], { stdio: 'ignore' })
|
|
37
|
-
startSupervisorDaemon({ session: 'bp', runtime: 'tick', runDir, serve: { argv: child, env: { PATH: process.env.PATH ?? '' }, cwd: dir, paneLogPath: logSup } })
|
|
38
|
-
await sleep(2300) // both children: sleep .5 + emit + sleep 1 ≈ exit ~1.5s; tmux's cat EOF-flushes
|
|
39
|
-
const a = readBuf(logTmux)
|
|
40
|
-
const b = readBuf(logSup)
|
|
41
|
-
expect(b.length).toBeGreaterThan(0)
|
|
42
|
-
expect(b.equals(a)).toBe(true) // byte-for-byte — raw Buffer passthrough, shared onlcr default
|
|
43
|
-
expect(b.includes(Buffer.from('❯›…é', 'utf8'))).toBe(true) // multibyte survived (no latin1 re-encode)
|
|
44
|
-
} finally {
|
|
45
|
-
spawnSync('tmux', ['-S', tmuxSock, 'kill-server'], { stdio: 'ignore' })
|
|
46
|
-
try {
|
|
47
|
-
killSession(runDir, 'bp')
|
|
48
|
-
} catch {
|
|
49
|
-
/* */
|
|
50
|
-
}
|
|
51
|
-
await sleep(300)
|
|
52
|
-
rmSync(dir, { recursive: true, force: true })
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('BINDING: a model from the supervisor pane-log renders what tmux capture-pane shows', async () => {
|
|
57
|
-
const dir = mkdtempSync(join(tmpdir(), 'panelog-bind-'))
|
|
58
|
-
const tmuxSock = join(dir, 't.sock')
|
|
59
|
-
const logSup = join(dir, 'sup.log')
|
|
60
|
-
const runDir = join(dir, 'run')
|
|
61
|
-
const COLS = 80,
|
|
62
|
-
ROWS = 24
|
|
63
|
-
// a static frame (clear+home, three lines incl. the ❯ ready glyph), then idle so capture-pane sees it
|
|
64
|
-
const child = ['sh', '-c', `printf '\\033[2J\\033[HLine one\\r\\n ❯ ready prompt\\r\\nthird line ok\\r\\n'; sleep 30`]
|
|
65
|
-
try {
|
|
66
|
-
spawnSync('tmux', ['-S', tmuxSock, 'new-session', '-d', '-s', 'b', '-x', String(COLS), '-y', String(ROWS), ...child], { stdio: 'ignore' })
|
|
67
|
-
startSupervisorDaemon({ session: 'bind', runtime: 'tick', runDir, serve: { argv: child, env: { PATH: process.env.PATH ?? '' }, cwd: dir, paneLogPath: logSup } })
|
|
68
|
-
// let the frame render in both
|
|
69
|
-
let cap = ''
|
|
70
|
-
for (let i = 0; i < 30 && !/❯ ready prompt/.test(cap); i++) {
|
|
71
|
-
await sleep(200)
|
|
72
|
-
cap = spawnSync('tmux', ['-S', tmuxSock, 'capture-pane', '-p', '-t', 'b'], { encoding: 'utf8' }).stdout || ''
|
|
73
|
-
}
|
|
74
|
-
const modelSup = await modelOf(readBuf(logSup), COLS, ROWS)
|
|
75
|
-
// tmux capture-pane render (its own emulator) == @xterm model of the SUPERVISOR's pane-log
|
|
76
|
-
const norm = (s: string): string =>
|
|
77
|
-
s
|
|
78
|
-
.split('\n')
|
|
79
|
-
.map(l => l.replace(/\s+$/, ''))
|
|
80
|
-
.join('\n')
|
|
81
|
-
.replace(/\n+$/, '')
|
|
82
|
-
expect(norm(modelSup)).toBe(norm(cap))
|
|
83
|
-
expect(modelSup).toContain('❯ ready prompt') // the ready glyph survived into the model
|
|
84
|
-
} finally {
|
|
85
|
-
spawnSync('tmux', ['-S', tmuxSock, 'kill-server'], { stdio: 'ignore' })
|
|
86
|
-
try {
|
|
87
|
-
killSession(runDir, 'bind')
|
|
88
|
-
} catch {
|
|
89
|
-
/* */
|
|
90
|
-
}
|
|
91
|
-
await sleep(300)
|
|
92
|
-
rmSync(dir, { recursive: true, force: true })
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
})
|