@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.
@@ -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
+ })