@agfpd/iapeer 0.2.77 → 0.2.79

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