@agfpd/iapeer 0.2.51 → 0.2.53
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/package.json +1 -1
- package/src/cli/cli.test.ts +23 -1
- package/src/cli/index.ts +42 -6
- package/src/daemon/daemon.test.ts +2 -0
- package/src/daemon/index.ts +24 -6
- package/src/daemon/main.test.ts +19 -0
- package/src/daemon/main.ts +98 -4
- package/src/launch/adapters/claude.ts +1 -0
- package/src/launch/adapters/codex.ts +1 -0
- package/src/launch/launch.test.ts +2 -0
- package/src/launch/types.ts +8 -0
- package/src/transport/compactDone.test.ts +60 -0
- package/src/transport/composerQueue.test.ts +174 -0
- package/src/transport/index.ts +600 -1
- package/src/transport/routesend-wake.test.ts +52 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { ok } from '../core/errors.ts'
|
|
3
|
+
import { getAdapter } from '../launch/index.ts'
|
|
4
|
+
import type { PeerRecord } from '../registry/index.ts'
|
|
5
|
+
import type { ResolvedCaller } from '../identity/index.ts'
|
|
6
|
+
import {
|
|
7
|
+
composerCaptureHasHumanInput,
|
|
8
|
+
createComposerDeliveryQueue,
|
|
9
|
+
type ComposerQueueTryEnqueueArgs,
|
|
10
|
+
type DeliveryTarget,
|
|
11
|
+
} from './index.ts'
|
|
12
|
+
|
|
13
|
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
14
|
+
|
|
15
|
+
describe('composerCaptureHasHumanInput (SGR color gate)', () => {
|
|
16
|
+
test('codex: default-fg text after › is human input; faint/grey ghost text is not', () => {
|
|
17
|
+
const markers = getAdapter('codex').deliveryMarkers
|
|
18
|
+
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m hello', markers)).toBe(true)
|
|
19
|
+
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m \x1b[2mghost suggestion\x1b[0m', markers)).toBe(false)
|
|
20
|
+
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m \x1b[38;5;246mplaceholder\x1b[0m', markers)).toBe(false)
|
|
21
|
+
// A suggestion accepted into the composer is no longer grey/faint; it must be held.
|
|
22
|
+
expect(composerCaptureHasHumanInput('\x1b[32m›\x1b[0m accepted suggestion', markers)).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('claude: same color rule is runtime-owned behind the ❯ prompt glyph', () => {
|
|
26
|
+
const markers = getAdapter('claude').deliveryMarkers
|
|
27
|
+
expect(composerCaptureHasHumanInput('\x1b[35m❯\x1b[0m human', markers)).toBe(true)
|
|
28
|
+
expect(composerCaptureHasHumanInput('\x1b[35m❯\x1b[0m \x1b[2mghost\x1b[0m', markers)).toBe(false)
|
|
29
|
+
expect(composerCaptureHasHumanInput('\x1b[35m❯\x1b[0m \x1b[38;5;246mplaceholder\x1b[0m', markers)).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const callerRecord = {
|
|
34
|
+
personality: 'sender',
|
|
35
|
+
runtime: 'codex',
|
|
36
|
+
runtimes: ['codex'],
|
|
37
|
+
description: '',
|
|
38
|
+
intelligence: 'artificial',
|
|
39
|
+
cwd: '/tmp/sender',
|
|
40
|
+
} as unknown as PeerRecord
|
|
41
|
+
const caller: ResolvedCaller = {
|
|
42
|
+
personality: 'sender',
|
|
43
|
+
runtime: 'codex',
|
|
44
|
+
address: 'codex-sender',
|
|
45
|
+
description: '',
|
|
46
|
+
intelligence: 'artificial',
|
|
47
|
+
cwd: '/tmp/sender',
|
|
48
|
+
record: callerRecord,
|
|
49
|
+
}
|
|
50
|
+
const peer = {
|
|
51
|
+
personality: 'target',
|
|
52
|
+
runtime: 'codex',
|
|
53
|
+
runtimes: ['codex'],
|
|
54
|
+
description: '',
|
|
55
|
+
intelligence: 'artificial',
|
|
56
|
+
cwd: '/tmp/target',
|
|
57
|
+
} as unknown as PeerRecord
|
|
58
|
+
const target: DeliveryTarget = {
|
|
59
|
+
personality: 'target',
|
|
60
|
+
runtime: 'codex',
|
|
61
|
+
address: 'codex-target',
|
|
62
|
+
socketPath: '/tmp/no-such-sock',
|
|
63
|
+
}
|
|
64
|
+
function args(envelope = '<iap>hello</iap>'): ComposerQueueTryEnqueueArgs {
|
|
65
|
+
return { caller, peer, target, envelope, topic: 'q-test' }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('createComposerDeliveryQueue', () => {
|
|
69
|
+
test('queued ack is immediate; drain waits until composer clears, then delivers silently', async () => {
|
|
70
|
+
let held = true
|
|
71
|
+
const delivered: string[] = []
|
|
72
|
+
const failed: string[] = []
|
|
73
|
+
const q = createComposerDeliveryQueue({
|
|
74
|
+
pollMs: 5,
|
|
75
|
+
forceTimeoutMs: 1000,
|
|
76
|
+
shouldQueue: () => true,
|
|
77
|
+
hasHumanInput: () => held,
|
|
78
|
+
sessionToken: () => 'session-1:pane-1',
|
|
79
|
+
sessionAlive: () => true,
|
|
80
|
+
deliver: (_target, envelope) => {
|
|
81
|
+
delivered.push(envelope)
|
|
82
|
+
return ok(undefined)
|
|
83
|
+
},
|
|
84
|
+
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const accepted = await q.tryEnqueue(args('first'))
|
|
88
|
+
expect(accepted?.ok).toBe(true)
|
|
89
|
+
if (!accepted?.ok) return
|
|
90
|
+
expect(accepted.value).toMatchObject({ queued: true, queuedBy: 'composer', queueDepth: 1 })
|
|
91
|
+
await sleep(20)
|
|
92
|
+
expect(delivered).toEqual([])
|
|
93
|
+
held = false
|
|
94
|
+
await sleep(30)
|
|
95
|
+
expect(delivered).toEqual(['first'])
|
|
96
|
+
expect(failed).toEqual([])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('120s ceiling (env-tunable in tests) force-delivers instead of waiting forever', async () => {
|
|
100
|
+
const delivered: string[] = []
|
|
101
|
+
const failed: string[] = []
|
|
102
|
+
const q = createComposerDeliveryQueue({
|
|
103
|
+
pollMs: 5,
|
|
104
|
+
forceTimeoutMs: 20,
|
|
105
|
+
shouldQueue: () => true,
|
|
106
|
+
hasHumanInput: () => true,
|
|
107
|
+
sessionToken: () => 'session-1:pane-1',
|
|
108
|
+
sessionAlive: () => true,
|
|
109
|
+
deliver: (_target, envelope) => {
|
|
110
|
+
delivered.push(envelope)
|
|
111
|
+
return ok(undefined)
|
|
112
|
+
},
|
|
113
|
+
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
await q.tryEnqueue(args('force-me'))
|
|
117
|
+
await sleep(70)
|
|
118
|
+
expect(delivered).toEqual(['force-me'])
|
|
119
|
+
expect(failed).toEqual([])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('target death/session replacement fails the queued sender instead of delivering to a corpse/new session', async () => {
|
|
123
|
+
let alive = true
|
|
124
|
+
const delivered: string[] = []
|
|
125
|
+
const failed: string[] = []
|
|
126
|
+
const q = createComposerDeliveryQueue({
|
|
127
|
+
pollMs: 5,
|
|
128
|
+
forceTimeoutMs: 1000,
|
|
129
|
+
shouldQueue: () => true,
|
|
130
|
+
hasHumanInput: () => true,
|
|
131
|
+
sessionToken: () => 'session-1:pane-1',
|
|
132
|
+
sessionAlive: () => alive,
|
|
133
|
+
deliver: (_target, envelope) => {
|
|
134
|
+
delivered.push(envelope)
|
|
135
|
+
return ok(undefined)
|
|
136
|
+
},
|
|
137
|
+
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await q.tryEnqueue(args('must-fail'))
|
|
141
|
+
alive = false
|
|
142
|
+
await sleep(30)
|
|
143
|
+
expect(delivered).toEqual([])
|
|
144
|
+
expect(failed.join('\n')).toMatch(/vanished|replaced/)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('daemon close/restart failAll re-fails every queued-but-undelivered envelope', async () => {
|
|
148
|
+
const delivered: string[] = []
|
|
149
|
+
const failed: string[] = []
|
|
150
|
+
const q = createComposerDeliveryQueue({
|
|
151
|
+
pollMs: 50,
|
|
152
|
+
forceTimeoutMs: 1000,
|
|
153
|
+
shouldQueue: () => true,
|
|
154
|
+
hasHumanInput: () => true,
|
|
155
|
+
sessionToken: () => 'session-1:pane-1',
|
|
156
|
+
sessionAlive: () => true,
|
|
157
|
+
deliver: (_target, envelope) => {
|
|
158
|
+
delivered.push(envelope)
|
|
159
|
+
return ok(undefined)
|
|
160
|
+
},
|
|
161
|
+
notifyFailed: (_job, reason) => { failed.push(reason) },
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
await q.tryEnqueue(args('queued-a'))
|
|
165
|
+
await q.tryEnqueue(args('queued-b'))
|
|
166
|
+
await q.failAll?.('daemon shutting down/restarting before queued delivery completed')
|
|
167
|
+
await sleep(10)
|
|
168
|
+
expect(delivered).toEqual([])
|
|
169
|
+
expect(failed).toEqual([
|
|
170
|
+
'daemon shutting down/restarting before queued delivery completed',
|
|
171
|
+
'daemon shutting down/restarting before queued delivery completed',
|
|
172
|
+
])
|
|
173
|
+
})
|
|
174
|
+
})
|