@agfpd/iapeer 0.2.77 → 0.2.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/package.json +21 -2
- package/src/cli/index.ts +47 -48
- package/src/connect/index.ts +4 -4
- package/src/core/constants.ts +3 -3
- package/src/core/normalize.ts +2 -2
- package/src/create/index.ts +1 -1
- package/src/daemon/index.ts +5 -5
- package/src/daemon/main.ts +9 -9
- package/src/enable/index.ts +19 -18
- package/src/enable/provisionCommand.ts +10 -10
- package/src/identity/index.ts +2 -2
- package/src/identity/profileStandard.ts +8 -8
- package/src/init/index.ts +10 -10
- package/src/install/index.ts +2 -2
- package/src/install/signing.ts +3 -4
- package/src/launch/adapters/claude.ts +7 -7
- package/src/launch/adapters/codex.ts +5 -6
- package/src/launch/canary.ts +8 -8
- package/src/launch/cmdlog.ts +3 -3
- package/src/launch/composeSystemPrompt.ts +2 -1
- package/src/launch/index.ts +5 -6
- package/src/launch/invocation.ts +1 -1
- package/src/launch/launchd.ts +14 -15
- package/src/launch/launchdRun.ts +6 -6
- package/src/launch/nativeMemory.ts +9 -9
- package/src/launch/ptyHost.ts +1 -1
- package/src/launch/readyGateModel.ts +3 -3
- package/src/launch/types.ts +7 -7
- package/src/lifecycle/eventlog.ts +2 -2
- package/src/lifecycle/index.ts +21 -21
- package/src/lifecycle/queue.ts +1 -1
- package/src/onboard/index.ts +19 -13
- package/src/onboard/memory.ts +5 -5
- package/src/onboard/steps.ts +8 -9
- package/src/provision/index.ts +2 -3
- package/src/runtime/deploy.ts +2 -2
- package/src/runtime/index.ts +4 -4
- package/src/shadow/gate.ts +6 -6
- package/src/shadow/index.ts +2 -2
- package/src/status/index.ts +11 -11
- package/src/storage/index.ts +1 -1
- package/src/supervisor/daemon.ts +14 -8
- package/src/transport/index.ts +11 -11
- package/src/update/index.ts +4 -4
- package/src/cli/cli.test.ts +0 -603
- package/src/cli/listTui.test.ts +0 -71
- package/src/codec/codec.test.ts +0 -271
- package/src/connect/connect.test.ts +0 -163
- package/src/core/constants.test.ts +0 -32
- package/src/core/normalize.test.ts +0 -98
- package/src/create/create.test.ts +0 -143
- package/src/daemon/daemon-http.test.ts +0 -114
- package/src/daemon/daemon.test.ts +0 -267
- package/src/daemon/main.test.ts +0 -413
- package/src/enable/enable.test.ts +0 -95
- package/src/enable/provisionCommand.test.ts +0 -107
- package/src/identity/identity.test.ts +0 -279
- package/src/identity/profileStandard.test.ts +0 -204
- package/src/init/init.test.ts +0 -215
- package/src/init/runtime-resolve.test.ts +0 -49
- package/src/install/install.test.ts +0 -66
- package/src/install/signing.test.ts +0 -84
- package/src/launch/bootdeliver.test.ts +0 -108
- package/src/launch/bootstrap.test.ts +0 -56
- package/src/launch/canary.test.ts +0 -346
- package/src/launch/cmdlog.test.ts +0 -94
- package/src/launch/codexHooksTrust.test.ts +0 -248
- package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
- package/src/launch/composeSystemPrompt.test.ts +0 -145
- package/src/launch/hostComposerOccupied.test.ts +0 -63
- package/src/launch/invocation.test.ts +0 -174
- package/src/launch/launch.test.ts +0 -359
- package/src/launch/launchd.test.ts +0 -499
- package/src/launch/nativeMemory.test.ts +0 -193
- package/src/launch/ptyHost.test.ts +0 -38
- package/src/launch/readyGateModel.test.ts +0 -72
- package/src/launch/sockdir.test.ts +0 -74
- package/src/lifecycle/eventlog.test.ts +0 -114
- package/src/lifecycle/lifecycle.test.ts +0 -1056
- package/src/lifecycle/queue.test.ts +0 -185
- package/src/onboard/memory.test.ts +0 -157
- package/src/onboard/onboard.test.ts +0 -39
- package/src/onboard/steps.test.ts +0 -201
- package/src/provision/provision.test.ts +0 -288
- package/src/registry/registry.test.ts +0 -432
- package/src/runtime/runtime.test.ts +0 -226
- package/src/runtime/update.test.ts +0 -192
- package/src/shadow/enumerate-skip.test.ts +0 -67
- package/src/shadow/gate.test.ts +0 -286
- package/src/shadow/install.test.ts +0 -153
- package/src/shadow/shadow.test.ts +0 -42
- package/src/status/status.test.ts +0 -208
- package/src/storage/peers-home.test.ts +0 -34
- package/src/storage/rotatelog.test.ts +0 -44
- package/src/storage/storage.test.ts +0 -65
- package/src/supervisor/attached-marker.test.ts +0 -84
- package/src/supervisor/boot.test.ts +0 -66
- package/src/supervisor/client.attach.test.ts +0 -107
- package/src/supervisor/daemon.test.ts +0 -213
- package/src/supervisor/deliver.test.ts +0 -58
- package/src/supervisor/panelog.test.ts +0 -95
- package/src/supervisor/protocol.test.ts +0 -222
- package/src/supervisor/selfargv.test.ts +0 -22
- package/src/supervisor/spinner-strip.test.ts +0 -52
- package/src/transport/compactDone.test.ts +0 -144
- package/src/transport/composerQueue.test.ts +0 -204
- package/src/transport/hostControl.test.ts +0 -87
- package/src/transport/hostDeliver.test.ts +0 -137
- package/src/transport/hostLive.test.ts +0 -50
- package/src/transport/routesend-wake.test.ts +0 -357
- package/src/update/update.test.ts +0 -208
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Spawn-flip cutover Block 2, Ф0b-2 slice 3 — host-aware in-session control (interrupt / compact).
|
|
2
|
-
//
|
|
3
|
-
// executeControlOnTarget becomes host-aware: a supervisor-HOSTED target has no `tmux send-keys`, so
|
|
4
|
-
// each control plan step is translated to pty bytes by keysToBytes (REUSED from the boot-driver) and
|
|
5
|
-
// sent over the supervisor socket. These suites cover the host branch hermetically (a seam injects the
|
|
6
|
-
// host-detect + the socket control-send) — asserting the BYTE TRANSLATION + dispatch. The live
|
|
7
|
-
// "interrupt really interrupts a hosted turn" effect is proven separately against a real hosted codex.
|
|
8
|
-
import { describe, expect, test } from 'bun:test'
|
|
9
|
-
import { executeControlOnTarget, type ControlHostSeam, type DeliveryTarget } from './index.ts'
|
|
10
|
-
|
|
11
|
-
const hostedCodex: DeliveryTarget = {
|
|
12
|
-
runtime: 'codex',
|
|
13
|
-
personality: 'ptyx',
|
|
14
|
-
address: 'codex-ptyx',
|
|
15
|
-
socketPath: '/tmp/nonexistent-iap-test.sock',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface Captured {
|
|
19
|
-
session: string
|
|
20
|
-
chunks: Buffer[]
|
|
21
|
-
stepDelayMs?: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function capturingSeam(captured: Captured[]): ControlHostSeam {
|
|
25
|
-
return {
|
|
26
|
-
hostAlive: () => true,
|
|
27
|
-
sendControl: async (_runDir, session, chunks, opts) => {
|
|
28
|
-
captured.push({ session, chunks, stepDelayMs: opts.stepDelayMs })
|
|
29
|
-
return { ok: true }
|
|
30
|
-
},
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('executeControlOnTarget — hosted control over the socket (slice 3)', () => {
|
|
35
|
-
test('interrupt → a single bare ESC byte (\\x1b), no step delay', async () => {
|
|
36
|
-
const cap: Captured[] = []
|
|
37
|
-
const r = await executeControlOnTarget(hostedCodex, { name: 'interrupt' }, capturingSeam(cap))
|
|
38
|
-
expect(r.ok).toBe(true)
|
|
39
|
-
expect(cap).toHaveLength(1)
|
|
40
|
-
expect(cap[0].session).toBe('codex-ptyx')
|
|
41
|
-
expect(cap[0].chunks.map(b => b.toString('latin1'))).toEqual(['\x1b'])
|
|
42
|
-
expect(cap[0].stepDelayMs).toBeUndefined()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('compact → "/compact" literal then Enter (CR), with the 300ms step delay', async () => {
|
|
46
|
-
const cap: Captured[] = []
|
|
47
|
-
const r = await executeControlOnTarget(hostedCodex, { name: 'compact' }, capturingSeam(cap))
|
|
48
|
-
expect(r.ok).toBe(true)
|
|
49
|
-
expect(cap[0].chunks.map(b => b.toString('latin1'))).toEqual(['/compact', '\r'])
|
|
50
|
-
expect(cap[0].stepDelayMs).toBe(300)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('flag-off (hostAlive=false) → the socket control-send is NEVER touched (tmux path)', async () => {
|
|
54
|
-
let sendCalls = 0
|
|
55
|
-
const seam: ControlHostSeam = {
|
|
56
|
-
hostAlive: () => false,
|
|
57
|
-
sendControl: async () => {
|
|
58
|
-
sendCalls++
|
|
59
|
-
return { ok: true }
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
// tmux send-keys will fail against the bogus socket — incidental; the contract is that flag-off
|
|
63
|
-
// never reaches the host branch.
|
|
64
|
-
await executeControlOnTarget(hostedCodex, { name: 'interrupt' }, seam)
|
|
65
|
-
expect(sendCalls).toBe(0)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('socket control-send fails → loud error, no false ok', async () => {
|
|
69
|
-
const seam: ControlHostSeam = {
|
|
70
|
-
hostAlive: () => true,
|
|
71
|
-
sendControl: async () => ({ ok: false, error: 'socket dead during control send' }),
|
|
72
|
-
}
|
|
73
|
-
const r = await executeControlOnTarget(hostedCodex, { name: 'interrupt' }, seam)
|
|
74
|
-
expect(r.ok).toBe(false)
|
|
75
|
-
if (!r.ok) {
|
|
76
|
-
expect(r.error.message).toContain('hosted control "interrupt" failed')
|
|
77
|
-
expect(r.error.message).toContain('socket dead')
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('unsupported command for the runtime → explicit refusal (router runtime, no control plan)', async () => {
|
|
82
|
-
const router: DeliveryTarget = { runtime: 'telegram', personality: 'tg', address: 'telegram-tg', socketPath: '/tmp/x.sock' }
|
|
83
|
-
const r = await executeControlOnTarget(router, { name: 'interrupt' }, capturingSeam([]))
|
|
84
|
-
expect(r.ok).toBe(false)
|
|
85
|
-
if (!r.ok) expect(r.error.message).toContain('does not support control command')
|
|
86
|
-
})
|
|
87
|
-
})
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// Spawn-flip cutover Block 2, Ф0b-2 — host-aware warm delivery (`deliverWarm`).
|
|
2
|
-
//
|
|
3
|
-
// The warm-deliver path becomes host-aware: a supervisor-HOSTED target is delivered over its socket
|
|
4
|
-
// (Ф0a leaf) and CONFIRMED by the SAME final arbiter as the tmux busy path — a transcript-mtime
|
|
5
|
-
// advance, NOT the socket-ack. These suites cover the host branch hermetically (injected seams: a
|
|
6
|
-
// fake host-detect + host-deliver + a transcript-mtime proxy) so no live supervisor daemon is needed.
|
|
7
|
-
// The flag-off (tmux) routing is asserted by the host seam staying UNTOUCHED — the byte-identical
|
|
8
|
-
// guarantee that lets this ship dark.
|
|
9
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
10
|
-
import { deliverWarm, type DeliveryTarget, type WarmDeliverSeam } from './index.ts'
|
|
11
|
-
|
|
12
|
-
const hostedTarget: DeliveryTarget = {
|
|
13
|
-
runtime: 'codex',
|
|
14
|
-
personality: 'ptyx',
|
|
15
|
-
address: 'codex-ptyx',
|
|
16
|
-
socketPath: '/tmp/nonexistent-iap-test.sock',
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const savedGrace = process.env.IAP_HOST_LIVENESS_GRACE_MS
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
if (savedGrace === undefined) delete process.env.IAP_HOST_LIVENESS_GRACE_MS
|
|
22
|
-
else process.env.IAP_HOST_LIVENESS_GRACE_MS = savedGrace
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
describe('deliverWarm — flag-off (no live supervisor session) keeps the tmux path', () => {
|
|
26
|
-
test('hostAlive=false → the host-deliver seam is NEVER touched (routes to deliverViaTmux)', async () => {
|
|
27
|
-
let hostedCalls = 0
|
|
28
|
-
const seam: WarmDeliverSeam = {
|
|
29
|
-
hostAlive: () => false,
|
|
30
|
-
deliverHosted: async () => {
|
|
31
|
-
hostedCalls++
|
|
32
|
-
return { ok: true }
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
// The tmux path will fail against a bogus socket — but that is incidental; the contract under
|
|
36
|
-
// test is that flag-off NEVER reaches the host branch. We assert the host seam stayed cold.
|
|
37
|
-
await deliverWarm(hostedTarget, 'hi', '/tmp', seam)
|
|
38
|
-
expect(hostedCalls).toBe(0)
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('deliverWarm — hosted target delivers over the socket + confirms by transcript advance', () => {
|
|
43
|
-
test('socket deliver ok AND transcript advances → ok', async () => {
|
|
44
|
-
let mtime = 100
|
|
45
|
-
const seam: WarmDeliverSeam = {
|
|
46
|
-
hostAlive: () => true,
|
|
47
|
-
deliverHosted: async () => {
|
|
48
|
-
mtime = 200 // the session took our message and started a model turn → transcript moved
|
|
49
|
-
return { ok: true }
|
|
50
|
-
},
|
|
51
|
-
newestActivityMtime: () => mtime,
|
|
52
|
-
}
|
|
53
|
-
const r = await deliverWarm(hostedTarget, 'task', '/peer/cwd', seam)
|
|
54
|
-
expect(r.ok).toBe(true)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('socket-ack is NOT landed: deliver ok but NO transcript advance → loud fail (treated as dead)', async () => {
|
|
58
|
-
process.env.IAP_HOST_LIVENESS_GRACE_MS = '0' // expire the HOST grace immediately — deterministic fail
|
|
59
|
-
let hostedCalls = 0
|
|
60
|
-
const seam: WarmDeliverSeam = {
|
|
61
|
-
hostAlive: () => true,
|
|
62
|
-
deliverHosted: async () => {
|
|
63
|
-
hostedCalls++
|
|
64
|
-
return { ok: true } // CR flushed to the socket — but the session never advanced
|
|
65
|
-
},
|
|
66
|
-
newestActivityMtime: () => 100, // constant → never advances past baseline
|
|
67
|
-
}
|
|
68
|
-
const r = await deliverWarm(hostedTarget, 'task', '/peer/cwd', seam)
|
|
69
|
-
expect(hostedCalls).toBe(1)
|
|
70
|
-
expect(r.ok).toBe(false)
|
|
71
|
-
if (!r.ok) expect(r.error.message).toContain('no transcript advance')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('socket deliver fails (socket dead/stalled) → loud fail, no false ok', async () => {
|
|
75
|
-
const seam: WarmDeliverSeam = {
|
|
76
|
-
hostAlive: () => true,
|
|
77
|
-
deliverHosted: async () => ({ ok: false, error: 'socket dead during submit' }),
|
|
78
|
-
newestActivityMtime: () => 100,
|
|
79
|
-
}
|
|
80
|
-
const r = await deliverWarm(hostedTarget, 'task', '/peer/cwd', seam)
|
|
81
|
-
expect(r.ok).toBe(false)
|
|
82
|
-
if (!r.ok) {
|
|
83
|
-
expect(r.error.message).toContain('deliver failed')
|
|
84
|
-
expect(r.error.message).toContain('socket dead')
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('no cwd (direct caller, no activity proxy) → confirmed-only (socket-ack), no mtime probe', async () => {
|
|
89
|
-
let mtimeProbes = 0
|
|
90
|
-
const seam: WarmDeliverSeam = {
|
|
91
|
-
hostAlive: () => true,
|
|
92
|
-
deliverHosted: async () => ({ ok: true }),
|
|
93
|
-
newestActivityMtime: () => {
|
|
94
|
-
mtimeProbes++
|
|
95
|
-
return 100
|
|
96
|
-
},
|
|
97
|
-
}
|
|
98
|
-
const r = await deliverWarm(hostedTarget, 'task', undefined, seam)
|
|
99
|
-
expect(r.ok).toBe(true)
|
|
100
|
-
expect(mtimeProbes).toBe(0) // no cwd → the transcript proxy is never consulted
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// Cutover infra-track — a hosted ROUTER (telegram/notifier) has NO transcript proxy
|
|
105
|
-
// (adapter.newestActivityMtime=null), so the mtime-advance confirm a TUI uses can never be satisfied
|
|
106
|
-
// and would false-FAIL every router delivery. The host path confirms a router by the socket-ack —
|
|
107
|
-
// PARITY with deliverViaTmux's router C-j path (delivery-level confirm; router liveness is structural
|
|
108
|
-
// via launchd, not a model turn). These cases pin that behavior.
|
|
109
|
-
const routerTarget: DeliveryTarget = {
|
|
110
|
-
runtime: 'notifier', // notifierAdapter.kind === 'router'
|
|
111
|
-
personality: 'timer',
|
|
112
|
-
address: 'notifier-timer',
|
|
113
|
-
socketPath: '/tmp/nonexistent-iap-test.sock',
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
describe('deliverWarm — hosted ROUTER confirms by socket-ack (no transcript proxy)', () => {
|
|
117
|
-
test('router deliver ok → ok EVEN THOUGH mtime never advances (the exact scenario a TUI fails on)', async () => {
|
|
118
|
-
process.env.IAP_HOST_LIVENESS_GRACE_MS = '0' // a mtime-gated path would fail at once here
|
|
119
|
-
const seam: WarmDeliverSeam = {
|
|
120
|
-
hostAlive: () => true,
|
|
121
|
-
deliverHosted: async () => ({ ok: true }),
|
|
122
|
-
newestActivityMtime: () => 100, // constant — a router writes no transcript; a TUI here → loud fail
|
|
123
|
-
}
|
|
124
|
-
const r = await deliverWarm(routerTarget, '<iap>x</iap>', '/peer/cwd', seam)
|
|
125
|
-
expect(r.ok).toBe(true) // socket-ack IS the confirm for a router; the non-advancing transcript is irrelevant
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test('router socket deliver fails (dead/stalled) → loud fail, no false ok', async () => {
|
|
129
|
-
const seam: WarmDeliverSeam = {
|
|
130
|
-
hostAlive: () => true,
|
|
131
|
-
deliverHosted: async () => ({ ok: false, error: 'socket dead during paste' }),
|
|
132
|
-
}
|
|
133
|
-
const r = await deliverWarm(routerTarget, '<iap>x</iap>', '/peer/cwd', seam)
|
|
134
|
-
expect(r.ok).toBe(false)
|
|
135
|
-
if (!r.ok) expect(r.error.message).toContain('deliver failed')
|
|
136
|
-
})
|
|
137
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// Spawn-flip Ф0b-3 — transport liveness/resolution is HOST-AWARE. The canary exposed it: a genuinely
|
|
2
|
-
// supervisor-HOSTED peer (live supervisor .pid, NO tmux) was seen as OFFLINE by resolveDeliveryTarget/
|
|
3
|
-
// isPeerLive (transport had its OWN tmux-only sessionAlive, separate from the Ф0b-1 lifecycle fix), so
|
|
4
|
-
// routeSend mis-routed a warm hit to a wake and the post-wake verify-before-act false-failed. This
|
|
5
|
-
// binds the fix: a live supervisor .pid → live, even with no tmux.
|
|
6
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
7
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { tmpdir } from 'node:os'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
import { hostRunDir } from '../launch/ptyHost.ts'
|
|
11
|
-
import { isPeerLive, listOnlinePeers } from './index.ts'
|
|
12
|
-
|
|
13
|
-
describe('isPeerLive — host-aware (spawn-flip Ф0b-3)', () => {
|
|
14
|
-
const savedRoot = process.env.IAPEER_ROOT
|
|
15
|
-
let root: string | null = null
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
if (savedRoot === undefined) delete process.env.IAPEER_ROOT
|
|
18
|
-
else process.env.IAPEER_ROOT = savedRoot
|
|
19
|
-
if (root) rmSync(root, { recursive: true, force: true })
|
|
20
|
-
root = null
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('a LIVE supervisor .pid → live, with NO tmux session (the canary fix)', () => {
|
|
24
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-hostlive-'))
|
|
25
|
-
process.env.IAPEER_ROOT = root
|
|
26
|
-
const runDir = hostRunDir() // resolves under IAPEER_ROOT
|
|
27
|
-
mkdirSync(runDir, { recursive: true })
|
|
28
|
-
writeFileSync(join(runDir, 'codex-hostpeer.pid'), String(process.pid)) // a live pid = a live hosted daemon
|
|
29
|
-
expect(isPeerLive('codex', 'hostpeer')).toBe(true) // host-aware: no tmux, but the supervisor session is alive
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('no supervisor .pid and no tmux → offline', () => {
|
|
33
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-hostlive-'))
|
|
34
|
-
process.env.IAPEER_ROOT = root
|
|
35
|
-
mkdirSync(hostRunDir(), { recursive: true })
|
|
36
|
-
expect(isPeerLive('codex', 'ghostpeer')).toBe(false)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('listOnlinePeers includes a live HOSTED peer (no tmux socket)', () => {
|
|
40
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-hostlive-'))
|
|
41
|
-
process.env.IAPEER_ROOT = root
|
|
42
|
-
const runDir = hostRunDir()
|
|
43
|
-
mkdirSync(runDir, { recursive: true })
|
|
44
|
-
// supervisor listSessions enumerates by `<identity>.sock`; alive = a live `<identity>.pid`.
|
|
45
|
-
writeFileSync(join(runDir, 'codex-hostpeer.sock'), '')
|
|
46
|
-
writeFileSync(join(runDir, 'codex-hostpeer.pid'), String(process.pid))
|
|
47
|
-
const online = listOnlinePeers()
|
|
48
|
-
expect(online.some(p => p.runtime === 'codex' && p.personality === 'hostpeer')).toBe(true)
|
|
49
|
-
})
|
|
50
|
-
})
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
// routeSend wake-path delivery guarantee — the concurrent-sender fast-path hole.
|
|
2
|
-
//
|
|
3
|
-
// The race (found by code-read 09.06, vault «Находка — wake-on-miss fast-path …»):
|
|
4
|
-
// sender A's wake boots the session (A's envelope = boot first-message); sender B,
|
|
5
|
-
// blocked on wake.lock, enters AFTER the boot and takes the idempotent live-session
|
|
6
|
-
// fast path — which delivers NOTHING. Pre-fix, routeSend then reported {ok:true,
|
|
7
|
-
// woke:true} for B while B's envelope was silently lost (the class the delivery
|
|
8
|
-
// contract forbids). Post-fix, wakeOrSpawn marks the fast path taskDelivered:false
|
|
9
|
-
// and routeSend delivers B's envelope itself via the LIVE path.
|
|
10
|
-
//
|
|
11
|
-
// The fake WakeFn here reproduces the loser-of-the-lock outcome exactly: it brings
|
|
12
|
-
// the session up itself (= what the winning concurrent wake did) and returns READY
|
|
13
|
-
// without having delivered the caller's task.
|
|
14
|
-
|
|
15
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
16
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
17
|
-
import { tmpdir } from 'os'
|
|
18
|
-
import { join } from 'path'
|
|
19
|
-
import { spawnSync } from 'child_process'
|
|
20
|
-
import { hasTelegramPresence, routeSend, type WakeFn } from './index.ts'
|
|
21
|
-
import type { ResolvedCaller } from '../identity/index.ts'
|
|
22
|
-
import type { PeerRecord } from '../registry/index.ts'
|
|
23
|
-
|
|
24
|
-
let root: string
|
|
25
|
-
let sockDir: string
|
|
26
|
-
const prevRoot = process.env.IAPEER_ROOT
|
|
27
|
-
|
|
28
|
-
// These suites spawn a REAL tmux session (the live-delivery path). Skip where
|
|
29
|
-
// tmux is absent — a clean CI runner has none, and an unguarded live-tmux test
|
|
30
|
-
// is what kept main's CI red (fail: "tmux new-session failed: null"). The
|
|
31
|
-
// foundation's preversion gate runs the full suite locally (this machine has
|
|
32
|
-
// tmux), so the release path keeps the live coverage; CI gates the rest.
|
|
33
|
-
const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
|
|
34
|
-
|
|
35
|
-
// Target peer: NON-TUI runtime (telegram) → the C-j delivery path lands into a
|
|
36
|
-
// plain `cat` pane with no TUI prompt machinery; capture-pane then shows the
|
|
37
|
-
// envelope text verbatim.
|
|
38
|
-
// NB: keep names SHORT — the tmux socket lives under mkdtemp and the unix
|
|
39
|
-
// socket path limit on macOS is 104 bytes.
|
|
40
|
-
const TARGET = 'fpw'
|
|
41
|
-
const SESSION = `telegram-${TARGET}`
|
|
42
|
-
const sockPath = () => join(sockDir, `tmux-iap-telegram-${TARGET}.sock`)
|
|
43
|
-
const TUI_TARGET = 'tq'
|
|
44
|
-
const TUI_SESSION = `codex-${TUI_TARGET}`
|
|
45
|
-
const tuiSockPath = () => join(sockDir, `tmux-iap-codex-${TUI_TARGET}.sock`)
|
|
46
|
-
|
|
47
|
-
const callerRecord = {
|
|
48
|
-
personality: 'boris',
|
|
49
|
-
runtime: 'claude',
|
|
50
|
-
runtimes: ['claude'],
|
|
51
|
-
description: '',
|
|
52
|
-
intelligence: 'artificial',
|
|
53
|
-
cwd: '/tmp/boris',
|
|
54
|
-
// Telegram sender policy: the target of these suites is a telegram peer, so the
|
|
55
|
-
// caller must declare a telegram presence (the bot machine-key) — these suites
|
|
56
|
-
// double as the "bot-faced sender delivers as before" half of the policy criterion.
|
|
57
|
-
interfaces: { telegram: { bot: 'boris' } },
|
|
58
|
-
} as unknown as PeerRecord
|
|
59
|
-
const caller: ResolvedCaller = {
|
|
60
|
-
personality: 'boris',
|
|
61
|
-
runtime: 'claude',
|
|
62
|
-
address: 'claude-boris',
|
|
63
|
-
description: '',
|
|
64
|
-
intelligence: 'artificial',
|
|
65
|
-
cwd: '/tmp/boris',
|
|
66
|
-
record: callerRecord,
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function startTargetSession(): void {
|
|
70
|
-
const r = spawnSync('tmux', ['-S', sockPath(), 'new-session', '-d', '-x', '200', '-y', '50', '-s', SESSION, 'cat'], {
|
|
71
|
-
encoding: 'utf8',
|
|
72
|
-
})
|
|
73
|
-
if (r.status !== 0) throw new Error(`tmux new-session failed: ${r.stderr}`)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function killTargetSession(): void {
|
|
77
|
-
spawnSync('tmux', ['-S', sockPath(), 'kill-server'], { encoding: 'utf8' })
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function capturePane(): string {
|
|
81
|
-
const r = spawnSync('tmux', ['-S', sockPath(), 'capture-pane', '-p', '-t', SESSION], { encoding: 'utf8' })
|
|
82
|
-
return r.stdout ?? ''
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function startTuiSession(): void {
|
|
86
|
-
const r = spawnSync('tmux', ['-S', tuiSockPath(), 'new-session', '-d', '-x', '200', '-y', '50', '-s', TUI_SESSION, 'cat'], {
|
|
87
|
-
encoding: 'utf8',
|
|
88
|
-
})
|
|
89
|
-
if (r.status !== 0) throw new Error(`tmux new-session failed: ${r.stderr}`)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function killTuiSession(): void {
|
|
93
|
-
spawnSync('tmux', ['-S', tuiSockPath(), 'kill-server'], { encoding: 'utf8' })
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
beforeAll(() => {
|
|
97
|
-
root = mkdtempSync(join(tmpdir(), 'fpw-'))
|
|
98
|
-
sockDir = join(root, 'socks')
|
|
99
|
-
mkdirSync(sockDir, { recursive: true })
|
|
100
|
-
writeFileSync(
|
|
101
|
-
join(root, 'peers-profiles.json'),
|
|
102
|
-
JSON.stringify({
|
|
103
|
-
version: 2,
|
|
104
|
-
peers: [
|
|
105
|
-
{ personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial', cwd: '/tmp/boris' },
|
|
106
|
-
{ personality: TARGET, runtime: 'telegram', runtimes: ['telegram'], description: '', intelligence: 'artificial', cwd: root },
|
|
107
|
-
{ personality: TUI_TARGET, runtime: 'codex', runtimes: ['codex'], description: '', intelligence: 'artificial', cwd: root },
|
|
108
|
-
],
|
|
109
|
-
}),
|
|
110
|
-
)
|
|
111
|
-
process.env.IAPEER_ROOT = root
|
|
112
|
-
})
|
|
113
|
-
afterAll(() => {
|
|
114
|
-
killTargetSession()
|
|
115
|
-
killTuiSession()
|
|
116
|
-
if (prevRoot === undefined) delete process.env.IAPEER_ROOT
|
|
117
|
-
else process.env.IAPEER_ROOT = prevRoot
|
|
118
|
-
rmSync(root, { recursive: true, force: true })
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
describe.if(tmuxAvailable)('routeSend wake fast-path (concurrent-sender envelope loss)', () => {
|
|
122
|
-
test('taskDelivered:false → routeSend delivers the envelope itself via the live path', async () => {
|
|
123
|
-
const marker = 'SECOND-SENDER-ENVELOPE-7741'
|
|
124
|
-
// The loser-of-the-lock outcome: the session is up (the WINNING wake booted it),
|
|
125
|
-
// READY came back, but THIS caller's task was never delivered.
|
|
126
|
-
const wake: WakeFn = async () => {
|
|
127
|
-
startTargetSession()
|
|
128
|
-
return { status: 'READY', woke: false, runtime: 'telegram', taskDelivered: false }
|
|
129
|
-
}
|
|
130
|
-
const r = await routeSend(caller, { personality: TARGET, message: marker }, { wake })
|
|
131
|
-
expect(r.ok).toBe(true)
|
|
132
|
-
if (!r.ok) return
|
|
133
|
-
// honest woke flag: the envelope went the LIVE path, not a boot first-message
|
|
134
|
-
expect(r.value.woke).toBe(false)
|
|
135
|
-
expect(r.value.delivered_to).toEqual({ personality: TARGET, runtime: 'telegram' })
|
|
136
|
-
// the envelope actually LANDED in the session (pre-fix: silently lost)
|
|
137
|
-
expect(capturePane()).toContain(marker)
|
|
138
|
-
killTargetSession()
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
test('legacy WakeFn without taskDelivered → prior behavior (no redelivery, woke:true)', async () => {
|
|
142
|
-
const marker = 'LEGACY-IMPL-ENVELOPE-9962'
|
|
143
|
-
const wake: WakeFn = async () => {
|
|
144
|
-
startTargetSession()
|
|
145
|
-
return { status: 'READY', woke: true, runtime: 'telegram' } // field absent → assumed delivered
|
|
146
|
-
}
|
|
147
|
-
const r = await routeSend(caller, { personality: TARGET, message: marker }, { wake })
|
|
148
|
-
expect(r.ok).toBe(true)
|
|
149
|
-
if (!r.ok) return
|
|
150
|
-
expect(r.value.woke).toBe(true)
|
|
151
|
-
// assumed delivered as the boot first-message → routeSend must NOT double-deliver
|
|
152
|
-
expect(capturePane()).not.toContain(marker)
|
|
153
|
-
killTargetSession()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test('taskDelivered:false but the session died right after wake → loud failure, not false ok', async () => {
|
|
157
|
-
const wake: WakeFn = async () => {
|
|
158
|
-
// READY without a live session: verify-before-act must catch it
|
|
159
|
-
return { status: 'READY', woke: false, runtime: 'telegram', taskDelivered: false }
|
|
160
|
-
}
|
|
161
|
-
const r = await routeSend(caller, { personality: TARGET, message: 'never lands' }, { wake })
|
|
162
|
-
expect(r.ok).toBe(false)
|
|
163
|
-
if (r.ok) return
|
|
164
|
-
expect(r.error.message).toMatch(/verify-before-act|not live/)
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
-
// noteLiveTopic seam — a LIVE delivery carrying a topic must surface it to the
|
|
170
|
-
// injected hook (lifecycle's .topic marker write), so the fresh-vs-resume
|
|
171
|
-
// discriminator compares against the topic the session last WORKED ON, not the
|
|
172
|
-
// one it woke with (defect 11.06: stale wake-time marker → false fresh).
|
|
173
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
describe.if(tmuxAvailable)('routeSend noteLiveTopic seam (live-delivered topic)', () => {
|
|
176
|
-
test('live hit with a topic → hook fired ONCE with the TARGET identity + topic', async () => {
|
|
177
|
-
startTargetSession()
|
|
178
|
-
const notes: Array<[string, string]> = []
|
|
179
|
-
const r = await routeSend(
|
|
180
|
-
caller,
|
|
181
|
-
{ personality: TARGET, message: 'm', topic: 'fix-resume' },
|
|
182
|
-
{ noteLiveTopic: (id, t) => notes.push([id, t]) },
|
|
183
|
-
)
|
|
184
|
-
expect(r.ok).toBe(true)
|
|
185
|
-
expect(notes).toEqual([[`telegram-${TARGET}`, 'fix-resume']])
|
|
186
|
-
killTargetSession()
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test('no topic → hook NOT fired; a throwing hook never fails the delivered message', async () => {
|
|
190
|
-
startTargetSession()
|
|
191
|
-
const notes: string[] = []
|
|
192
|
-
const r1 = await routeSend(caller, { personality: TARGET, message: 'm1' }, { noteLiveTopic: () => { notes.push('x') } })
|
|
193
|
-
expect(r1.ok).toBe(true)
|
|
194
|
-
expect(notes).toEqual([])
|
|
195
|
-
const r2 = await routeSend(
|
|
196
|
-
caller,
|
|
197
|
-
{ personality: TARGET, message: 'm2', topic: 't' },
|
|
198
|
-
{ noteLiveTopic: () => { throw new Error('boom') } },
|
|
199
|
-
)
|
|
200
|
-
expect(r2.ok).toBe(true)
|
|
201
|
-
killTargetSession()
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('post-wake fast-path redelivery (taskDelivered:false) also notes the topic — it is a LIVE delivery', async () => {
|
|
205
|
-
const notes: Array<[string, string]> = []
|
|
206
|
-
const wake: WakeFn = async () => {
|
|
207
|
-
startTargetSession()
|
|
208
|
-
return { status: 'READY', woke: false, runtime: 'telegram', taskDelivered: false }
|
|
209
|
-
}
|
|
210
|
-
const r = await routeSend(
|
|
211
|
-
caller,
|
|
212
|
-
{ personality: TARGET, message: 'm', topic: 'tt' },
|
|
213
|
-
{ wake, noteLiveTopic: (id, t) => notes.push([id, t]) },
|
|
214
|
-
)
|
|
215
|
-
expect(r.ok).toBe(true)
|
|
216
|
-
expect(notes).toEqual([[`telegram-${TARGET}`, 'tt']])
|
|
217
|
-
killTargetSession()
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
test('boot first-message delivery (woke:true) does NOT fire the hook — the wake records its own topic', async () => {
|
|
221
|
-
const notes: string[] = []
|
|
222
|
-
const wake: WakeFn = async () => {
|
|
223
|
-
startTargetSession()
|
|
224
|
-
return { status: 'READY', woke: true, runtime: 'telegram', taskDelivered: true }
|
|
225
|
-
}
|
|
226
|
-
const r = await routeSend(
|
|
227
|
-
caller,
|
|
228
|
-
{ personality: TARGET, message: 'm', topic: 'tb' },
|
|
229
|
-
{ wake, noteLiveTopic: () => { notes.push('x') } },
|
|
230
|
-
)
|
|
231
|
-
expect(r.ok).toBe(true)
|
|
232
|
-
expect(notes).toEqual([])
|
|
233
|
-
killTargetSession()
|
|
234
|
-
})
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
describe.if(tmuxAvailable)('routeSend busy-composer queue seam', () => {
|
|
238
|
-
test('live local TUI hit can return fast queued ack instead of delivering synchronously', async () => {
|
|
239
|
-
startTuiSession()
|
|
240
|
-
const seen: string[] = []
|
|
241
|
-
const r = await routeSend(
|
|
242
|
-
caller,
|
|
243
|
-
{ personality: TUI_TARGET, message: 'hold until operator submits', topic: 'composer-q' },
|
|
244
|
-
{
|
|
245
|
-
composerQueue: {
|
|
246
|
-
tryEnqueue: async ({ target, envelope, topic }) => {
|
|
247
|
-
seen.push(`${target.runtime}-${target.personality}:${topic}:${envelope.includes('hold until operator submits')}`)
|
|
248
|
-
return {
|
|
249
|
-
ok: true,
|
|
250
|
-
value: {
|
|
251
|
-
ok: true as const,
|
|
252
|
-
delivered_to: { personality: target.personality, runtime: target.runtime },
|
|
253
|
-
woke: false,
|
|
254
|
-
queued: true,
|
|
255
|
-
queuedBy: 'composer',
|
|
256
|
-
queueDepth: 1,
|
|
257
|
-
ts: 'now',
|
|
258
|
-
},
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
)
|
|
264
|
-
expect(r.ok).toBe(true)
|
|
265
|
-
if (!r.ok) return
|
|
266
|
-
expect(r.value.queued).toBe(true)
|
|
267
|
-
expect(r.value.queuedBy).toBe('composer')
|
|
268
|
-
expect(seen).toEqual([`codex-${TUI_TARGET}:composer-q:true`])
|
|
269
|
-
killTuiSession()
|
|
270
|
-
})
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
// ─── Telegram sender policy (defect track 11.06) ─────────────────────────────
|
|
274
|
-
// A sender with NO declared telegram presence (interfaces.telegram.bot|user_id)
|
|
275
|
-
// must get a SYNCHRONOUS refusal when the delivery channel is telegram — the
|
|
276
|
-
// incident class: the bridge silently dropped such envelopes while the router
|
|
277
|
-
// reported ok:true. The bot-faced "delivers as before" half of the criterion is
|
|
278
|
-
// carried by the suites above (their caller declares interfaces.telegram.bot).
|
|
279
|
-
|
|
280
|
-
const facelessCaller: ResolvedCaller = {
|
|
281
|
-
personality: 'ghost',
|
|
282
|
-
runtime: 'claude',
|
|
283
|
-
address: 'claude-ghost',
|
|
284
|
-
description: '',
|
|
285
|
-
intelligence: 'artificial',
|
|
286
|
-
cwd: '/tmp/ghost',
|
|
287
|
-
record: {
|
|
288
|
-
personality: 'ghost',
|
|
289
|
-
runtime: 'claude',
|
|
290
|
-
runtimes: ['claude'],
|
|
291
|
-
description: '',
|
|
292
|
-
intelligence: 'artificial',
|
|
293
|
-
cwd: '/tmp/ghost',
|
|
294
|
-
} as unknown as PeerRecord,
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
describe('telegram sender policy — hasTelegramPresence', () => {
|
|
298
|
-
const rec = (tg: unknown): PeerRecord =>
|
|
299
|
-
({ personality: 'x', runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial', cwd: '/tmp/x', ...(tg !== undefined ? { interfaces: { telegram: tg } } : {}) }) as unknown as PeerRecord
|
|
300
|
-
test('bot machine-key (agent peers) → sender face present', () => {
|
|
301
|
-
expect(hasTelegramPresence(rec({ bot: 'boris' }))).toBe(true)
|
|
302
|
-
})
|
|
303
|
-
test('user_id alone (natural peers — arthur) → NO sender face: the bridge sends FROM the bot key; user_id is a receive/operator identity (owner fact, bot-only predicate)', () => {
|
|
304
|
-
expect(hasTelegramPresence(rec({ user_id: '409502965' }))).toBe(false)
|
|
305
|
-
expect(hasTelegramPresence(rec({ user_id: 409502965 }))).toBe(false)
|
|
306
|
-
})
|
|
307
|
-
test('no interfaces / no telegram / empty bot → absent', () => {
|
|
308
|
-
expect(hasTelegramPresence(rec(undefined))).toBe(false)
|
|
309
|
-
expect(hasTelegramPresence(rec({}))).toBe(false)
|
|
310
|
-
expect(hasTelegramPresence(rec({ bot: '' }))).toBe(false)
|
|
311
|
-
expect(hasTelegramPresence(rec({ bot: ' ' }))).toBe(false)
|
|
312
|
-
expect(hasTelegramPresence(rec({ aliases: { '/alias-new': 'x' } }))).toBe(false)
|
|
313
|
-
})
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
describe('telegram sender policy — routeSend refusal', () => {
|
|
317
|
-
test('INTENDED channel (target default = telegram, peer offline): refused BEFORE the wake — wake never invoked', async () => {
|
|
318
|
-
let wakeCalled = false
|
|
319
|
-
const wake: WakeFn = async () => {
|
|
320
|
-
wakeCalled = true
|
|
321
|
-
return { status: 'READY', woke: true, runtime: 'telegram', taskDelivered: true }
|
|
322
|
-
}
|
|
323
|
-
const r = await routeSend(facelessCaller, { personality: TARGET, message: 'm' }, { wake })
|
|
324
|
-
expect(r.ok).toBe(false)
|
|
325
|
-
if (!r.ok) {
|
|
326
|
-
expect(r.error.message).toContain('telegram policy')
|
|
327
|
-
expect(r.error.message).toContain('"ghost"')
|
|
328
|
-
expect(r.error.message).toContain('NOT delivered')
|
|
329
|
-
}
|
|
330
|
-
expect(wakeCalled).toBe(false)
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
test('explicit runtime=telegram override: refused with the policy line, not "offline"', async () => {
|
|
334
|
-
const r = await routeSend(facelessCaller, { personality: TARGET, runtime: 'telegram', message: 'm' })
|
|
335
|
-
expect(r.ok).toBe(false)
|
|
336
|
-
if (!r.ok) expect(r.error.message).toContain('telegram policy')
|
|
337
|
-
})
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
describe.if(tmuxAvailable)('telegram sender policy — live telegram session', () => {
|
|
341
|
-
test('faceless sender + LIVE telegram target → refused, envelope NEVER reaches the pane', async () => {
|
|
342
|
-
startTargetSession()
|
|
343
|
-
const r = await routeSend(facelessCaller, { personality: TARGET, message: 'POLICY-MUST-BLOCK' })
|
|
344
|
-
expect(r.ok).toBe(false)
|
|
345
|
-
if (!r.ok) expect(r.error.message).toContain('telegram policy')
|
|
346
|
-
expect(capturePane()).not.toContain('POLICY-MUST-BLOCK')
|
|
347
|
-
killTargetSession()
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
test('bot-faced sender (boris) + LIVE telegram target → delivered as before', async () => {
|
|
351
|
-
startTargetSession()
|
|
352
|
-
const r = await routeSend(caller, { personality: TARGET, message: 'BOT-FACED-OK' })
|
|
353
|
-
expect(r.ok).toBe(true)
|
|
354
|
-
expect(capturePane()).toContain('BOT-FACED-OK')
|
|
355
|
-
killTargetSession()
|
|
356
|
-
})
|
|
357
|
-
})
|