@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,445 +0,0 @@
|
|
|
1
|
-
// Layer 3 (registry) + Layer 4 (other domains) tests for composeSystemPrompt,
|
|
2
|
-
// plus the FS-discovery gatherPromptInput. The golden file (composeSystemPrompt
|
|
3
|
-
// .test.ts) proves layers 1+2 stay byte-for-byte; THIS file proves the new
|
|
4
|
-
// layers render correctly, in order (general → specific), and that empty/absent
|
|
5
|
-
// inputs add nothing (backward-compat with the legacy bytes).
|
|
6
|
-
|
|
7
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
8
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
9
|
-
import { tmpdir } from 'os'
|
|
10
|
-
import { join } from 'path'
|
|
11
|
-
import { composeSystemPrompt, gatherPromptInput } from './composeSystemPrompt.ts'
|
|
12
|
-
import type { ComposePromptInput, PromptDomainBlock, PublicPeerSummary } from './types.ts'
|
|
13
|
-
|
|
14
|
-
const base: Omit<ComposePromptInput, 'peers' | 'pluginDomains'> = {
|
|
15
|
-
personality: 'iapeer',
|
|
16
|
-
description: 'impl',
|
|
17
|
-
cwd: '/Users/macmini/Peers/iapeer',
|
|
18
|
-
platform: 'darwin',
|
|
19
|
-
osVersion: '15.5',
|
|
20
|
-
user: 'macmini',
|
|
21
|
-
hostname: 'macmini',
|
|
22
|
-
today: '2026-06-06',
|
|
23
|
-
peerDoctrine: 'Be the implementer.\n',
|
|
24
|
-
globalDoctrine: 'GLOBAL\n',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const peerA: PublicPeerSummary = {
|
|
28
|
-
personality: 'boris',
|
|
29
|
-
runtime: 'claude',
|
|
30
|
-
runtimes: ['claude'],
|
|
31
|
-
description: 'PM partner',
|
|
32
|
-
intelligence: 'artificial',
|
|
33
|
-
}
|
|
34
|
-
const peerB: PublicPeerSummary = {
|
|
35
|
-
personality: 'arthur',
|
|
36
|
-
runtime: 'telegram',
|
|
37
|
-
runtimes: ['telegram', 'claude'],
|
|
38
|
-
description: '',
|
|
39
|
-
intelligence: 'natural',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
-
// Pure renderer — Layers 3 + 4
|
|
44
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
describe('composeSystemPrompt — layer 3 (registry)', () => {
|
|
47
|
-
test('empty/omitted peers add NOTHING (legacy bytes preserved)', () => {
|
|
48
|
-
const legacy = composeSystemPrompt(base)
|
|
49
|
-
expect(composeSystemPrompt({ ...base, peers: [] })).toBe(legacy)
|
|
50
|
-
expect(composeSystemPrompt({ ...base, peers: undefined })).toBe(legacy)
|
|
51
|
-
expect(legacy).not.toContain('## Known peers')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('renders WITH descriptions + all 5 normalized fields (boris refinement #1)', () => {
|
|
55
|
-
const out = composeSystemPrompt({ ...base, peers: [peerA] })
|
|
56
|
-
expect(out).toContain('## Known peers')
|
|
57
|
-
expect(out).toContain('- boris — PM partner')
|
|
58
|
-
expect(out).toContain(' runtime: claude')
|
|
59
|
-
expect(out).toContain(' runtimes: claude')
|
|
60
|
-
expect(out).toContain(' intelligence: artificial')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('a peer with an empty description renders no " — " suffix', () => {
|
|
64
|
-
const out = composeSystemPrompt({ ...base, peers: [peerB] })
|
|
65
|
-
expect(out).toContain('- arthur\n')
|
|
66
|
-
expect(out).not.toContain('- arthur — ')
|
|
67
|
-
expect(out).toContain(' runtimes: telegram, claude')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('layer 3 sits AFTER the doctrine (layer 2), separated by a blank line', () => {
|
|
71
|
-
const out = composeSystemPrompt({ ...base, peers: [peerA] })
|
|
72
|
-
expect(out.indexOf('Be the implementer.')).toBeLessThan(out.indexOf('## Known peers'))
|
|
73
|
-
expect(out).toContain('Be the implementer.\n\n## Known peers')
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('composeSystemPrompt — layer 4 (other domains)', () => {
|
|
78
|
-
const dom = (over: Partial<PromptDomainBlock>): PromptDomainBlock => ({ domain: 'X', ...over })
|
|
79
|
-
|
|
80
|
-
test('empty/omitted domains add NOTHING (legacy bytes preserved)', () => {
|
|
81
|
-
const legacy = composeSystemPrompt(base)
|
|
82
|
-
expect(composeSystemPrompt({ ...base, pluginDomains: [] })).toBe(legacy)
|
|
83
|
-
expect(composeSystemPrompt({ ...base, pluginDomains: undefined })).toBe(legacy)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('a presence-marker domain (empty halves) emits no text and no dangling separator (boris refinement #2)', () => {
|
|
87
|
-
const legacy = composeSystemPrompt(base)
|
|
88
|
-
// a 0-byte marker → both halves '' → contributes nothing
|
|
89
|
-
expect(composeSystemPrompt({ ...base, pluginDomains: [dom({ local: '' })] })).toBe(legacy)
|
|
90
|
-
expect(composeSystemPrompt({ ...base, pluginDomains: [dom({ global: '', local: '' })] })).toBe(legacy)
|
|
91
|
-
// a marker next to a real domain must not inject a blank block between them
|
|
92
|
-
const out = composeSystemPrompt({
|
|
93
|
-
...base,
|
|
94
|
-
pluginDomains: [dom({ domain: 'MARKER', local: '' }), dom({ domain: 'REAL', global: 'real-content\n' })],
|
|
95
|
-
})
|
|
96
|
-
expect(out).toContain('real-content')
|
|
97
|
-
expect(out).not.toMatch(/\n{4,}/) // no 3+ blank lines = no empty block injected
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test('within a domain, global precedes local (general → specific), each its own section', () => {
|
|
101
|
-
const out = composeSystemPrompt({
|
|
102
|
-
...base,
|
|
103
|
-
pluginDomains: [dom({ domain: 'MM', global: 'mm-global', local: 'mm-local' })],
|
|
104
|
-
})
|
|
105
|
-
// per-file now: global and local are SEPARATE sections (blank line between),
|
|
106
|
-
// not a tight single-newline merge
|
|
107
|
-
expect(out).toContain('mm-global\n\nmm-local')
|
|
108
|
-
expect(out.indexOf('mm-global')).toBeLessThan(out.indexOf('mm-local'))
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('per-file path markers: each half carries its <!-- path --> marker', () => {
|
|
112
|
-
const out = composeSystemPrompt({
|
|
113
|
-
...base,
|
|
114
|
-
pluginDomains: [
|
|
115
|
-
dom({ domain: 'MM', global: 'mm-global\n', globalPath: '~/.iapeer/MERGEMIND.md', local: 'mm-local\n', localPath: '~/p/.iapeer/MERGEMIND.md' }),
|
|
116
|
-
],
|
|
117
|
-
})
|
|
118
|
-
expect(out).toContain('<!-- ~/.iapeer/MERGEMIND.md -->\nmm-global')
|
|
119
|
-
expect(out).toContain('<!-- ~/p/.iapeer/MERGEMIND.md -->\nmm-local')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
test('layer 4 sits after layer 3; domains kept in the order given', () => {
|
|
123
|
-
const out = composeSystemPrompt({
|
|
124
|
-
...base,
|
|
125
|
-
peers: [peerA],
|
|
126
|
-
pluginDomains: [dom({ domain: 'AAA', global: 'aaa' }), dom({ domain: 'ZZZ', local: 'zzz' })],
|
|
127
|
-
})
|
|
128
|
-
expect(out.indexOf('## Known peers')).toBeLessThan(out.indexOf('aaa'))
|
|
129
|
-
expect(out.indexOf('aaa')).toBeLessThan(out.indexOf('zzz'))
|
|
130
|
-
expect(out).toContain('aaa\n\nzzz')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('full 4-layer ordering: YAML → doctrine → registry → domains', () => {
|
|
134
|
-
const out = composeSystemPrompt({ ...base, peers: [peerA], pluginDomains: [dom({ global: 'domain-text' })] })
|
|
135
|
-
const iYaml = out.indexOf('personality:')
|
|
136
|
-
const iDoctrine = out.indexOf('Be the implementer.')
|
|
137
|
-
const iReg = out.indexOf('## Known peers')
|
|
138
|
-
const iDom = out.indexOf('domain-text')
|
|
139
|
-
expect(iYaml).toBeGreaterThanOrEqual(0)
|
|
140
|
-
expect(iYaml).toBeLessThan(iDoctrine)
|
|
141
|
-
expect(iDoctrine).toBeLessThan(iReg)
|
|
142
|
-
expect(iReg).toBeLessThan(iDom)
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
describe('composeSystemPrompt — layer 5 (doctrine fragments)', () => {
|
|
147
|
-
const frag = (over: Partial<PromptDomainBlock>): PromptDomainBlock => ({ domain: 'F', ...over })
|
|
148
|
-
|
|
149
|
-
test('empty/omitted fragments add NOTHING (legacy bytes preserved)', () => {
|
|
150
|
-
const legacy = composeSystemPrompt(base)
|
|
151
|
-
expect(composeSystemPrompt({ ...base, promptFragments: [] })).toBe(legacy)
|
|
152
|
-
expect(composeSystemPrompt({ ...base, promptFragments: undefined })).toBe(legacy)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test('a presence-marker fragment (empty halves) emits no text and no dangling separator', () => {
|
|
156
|
-
const legacy = composeSystemPrompt(base)
|
|
157
|
-
expect(composeSystemPrompt({ ...base, promptFragments: [frag({ local: '' })] })).toBe(legacy)
|
|
158
|
-
expect(composeSystemPrompt({ ...base, promptFragments: [frag({ global: '', local: '' })] })).toBe(legacy)
|
|
159
|
-
const out = composeSystemPrompt({
|
|
160
|
-
...base,
|
|
161
|
-
promptFragments: [frag({ domain: 'EMPTY', local: '' }), frag({ domain: 'REAL', local: 'idx-content\n' })],
|
|
162
|
-
})
|
|
163
|
-
expect(out).toContain('idx-content')
|
|
164
|
-
expect(out).not.toMatch(/\n{4,}/)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
test('within a fragment stem, global (host-wide guide) precedes local (per-peer index)', () => {
|
|
168
|
-
const out = composeSystemPrompt({
|
|
169
|
-
...base,
|
|
170
|
-
promptFragments: [frag({ domain: 'MEM', global: 'mem-guide', local: 'mem-index' })],
|
|
171
|
-
})
|
|
172
|
-
expect(out).toContain('mem-guide\n\nmem-index') // per-file: separate sections
|
|
173
|
-
expect(out.indexOf('mem-guide')).toBeLessThan(out.indexOf('mem-index'))
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
test('layer 5 sits AFTER layer 4 (most-volatile last), separated by a blank line', () => {
|
|
177
|
-
const out = composeSystemPrompt({
|
|
178
|
-
...base,
|
|
179
|
-
pluginDomains: [{ domain: 'D', global: 'domain-text' }],
|
|
180
|
-
promptFragments: [{ domain: 'MEM', local: 'fragment-text' }],
|
|
181
|
-
})
|
|
182
|
-
expect(out.indexOf('domain-text')).toBeLessThan(out.indexOf('fragment-text'))
|
|
183
|
-
expect(out).toContain('domain-text\n\nfragment-text')
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
test('full 5-layer ordering: YAML → doctrine → registry → domains → fragments', () => {
|
|
187
|
-
const out = composeSystemPrompt({
|
|
188
|
-
...base,
|
|
189
|
-
peers: [peerA],
|
|
190
|
-
pluginDomains: [{ domain: 'D', global: 'domain-text' }],
|
|
191
|
-
promptFragments: [{ domain: 'MEM', global: 'fragment-text' }],
|
|
192
|
-
})
|
|
193
|
-
const order = [
|
|
194
|
-
out.indexOf('personality:'),
|
|
195
|
-
out.indexOf('Be the implementer.'),
|
|
196
|
-
out.indexOf('## Known peers'),
|
|
197
|
-
out.indexOf('domain-text'),
|
|
198
|
-
out.indexOf('fragment-text'),
|
|
199
|
-
]
|
|
200
|
-
expect(order.every(i => i >= 0)).toBe(true)
|
|
201
|
-
expect(order).toEqual([...order].sort((a, b) => a - b))
|
|
202
|
-
})
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
-
// gatherPromptInput — FS discovery over ~/.iapeer + <cwd>/.iapeer
|
|
207
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
describe('gatherPromptInput — FS discovery of all 4 layers', () => {
|
|
210
|
-
let root: string
|
|
211
|
-
let globalRoot: string
|
|
212
|
-
let cwd: string
|
|
213
|
-
let localRoot: string
|
|
214
|
-
|
|
215
|
-
beforeAll(() => {
|
|
216
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-gather-'))
|
|
217
|
-
globalRoot = join(root, 'home', '.iapeer')
|
|
218
|
-
cwd = join(root, 'peer')
|
|
219
|
-
localRoot = join(cwd, '.iapeer')
|
|
220
|
-
mkdirSync(globalRoot, { recursive: true })
|
|
221
|
-
mkdirSync(localRoot, { recursive: true })
|
|
222
|
-
|
|
223
|
-
// Layer 2 — IAPEER.md, global + local
|
|
224
|
-
writeFileSync(join(globalRoot, 'IAPEER.md'), 'GLOBAL DOCTRINE\n')
|
|
225
|
-
writeFileSync(join(localRoot, 'IAPEER.md'), 'LOCAL ROLE\n')
|
|
226
|
-
|
|
227
|
-
// Layer 4 — domains: both halves / global-only / local-only / 0-byte marker
|
|
228
|
-
writeFileSync(join(globalRoot, 'MERGEMIND.md'), 'mm-global\n')
|
|
229
|
-
writeFileSync(join(localRoot, 'MERGEMIND.md'), 'mm-local\n')
|
|
230
|
-
writeFileSync(join(globalRoot, 'AAA.md'), 'aaa-global-only\n')
|
|
231
|
-
writeFileSync(join(localRoot, 'ZZZ.md'), 'zzz-local-only\n')
|
|
232
|
-
writeFileSync(join(localRoot, 'MARKER.md'), '') // presence marker, must not merge
|
|
233
|
-
// a non-.md file must be ignored
|
|
234
|
-
writeFileSync(join(localRoot, 'peer-profile.json'), '{}')
|
|
235
|
-
|
|
236
|
-
// Layer 5 — fragments/ subdir of both roots: a paired stem (host-wide guide +
|
|
237
|
-
// per-peer index) and a global-only fragment; a non-.md file must be ignored.
|
|
238
|
-
const globalFrags = join(globalRoot, 'fragments')
|
|
239
|
-
const localFrags = join(localRoot, 'fragments')
|
|
240
|
-
mkdirSync(globalFrags, { recursive: true })
|
|
241
|
-
mkdirSync(localFrags, { recursive: true })
|
|
242
|
-
writeFileSync(join(globalFrags, 'iapeer-memory.md'), 'mem-guide\n') // host-wide guide
|
|
243
|
-
writeFileSync(join(localFrags, 'iapeer-memory.md'), 'mem-index\n') // per-peer index
|
|
244
|
-
writeFileSync(join(globalFrags, 'other-primitive.md'), 'other-frag\n')
|
|
245
|
-
writeFileSync(join(localFrags, 'notes.txt'), 'ignored') // non-.md, must be ignored
|
|
246
|
-
|
|
247
|
-
// Layer 3 — registry at the global root (deliberately UNSORTED on disk)
|
|
248
|
-
const index = {
|
|
249
|
-
version: 2,
|
|
250
|
-
peers: [
|
|
251
|
-
{ personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: 'PM', intelligence: 'artificial', cwd: '/p/boris' },
|
|
252
|
-
{ personality: 'arthur', runtime: 'telegram', runtimes: ['telegram', 'claude'], description: 'owner', intelligence: 'natural', cwd: '/p/arthur' },
|
|
253
|
-
],
|
|
254
|
-
}
|
|
255
|
-
writeFileSync(join(globalRoot, 'peers-profiles.json'), JSON.stringify(index))
|
|
256
|
-
})
|
|
257
|
-
afterAll(() => rmSync(root, { recursive: true, force: true }))
|
|
258
|
-
|
|
259
|
-
function gather() {
|
|
260
|
-
return gatherPromptInput({
|
|
261
|
-
personality: 'iapeer',
|
|
262
|
-
description: 'impl',
|
|
263
|
-
cwd,
|
|
264
|
-
platform: 'darwin',
|
|
265
|
-
osVersion: '15.5',
|
|
266
|
-
user: 'u',
|
|
267
|
-
hostname: 'h',
|
|
268
|
-
today: '2026-06-06',
|
|
269
|
-
globalRoot,
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
test('layer 2: IAPEER.md read as global + local', () => {
|
|
274
|
-
const input = gather()
|
|
275
|
-
expect(input.globalDoctrine).toBe('GLOBAL DOCTRINE\n')
|
|
276
|
-
expect(input.peerDoctrine).toBe('LOCAL ROLE\n')
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
test('layer 4: IAPEER.md excluded; other domains discovered, sorted, non-.md ignored', () => {
|
|
280
|
-
const input = gather()
|
|
281
|
-
expect((input.pluginDomains ?? []).map(d => d.domain)).toEqual(['AAA', 'MARKER', 'MERGEMIND', 'ZZZ'])
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
test('layer 4: per-domain global/local halves resolved correctly, each with its display path', () => {
|
|
285
|
-
const input = gather()
|
|
286
|
-
const byName = Object.fromEntries((input.pluginDomains ?? []).map(d => [d.domain, d]))
|
|
287
|
-
expect(byName.MERGEMIND).toEqual({
|
|
288
|
-
domain: 'MERGEMIND',
|
|
289
|
-
global: 'mm-global\n',
|
|
290
|
-
globalPath: join(globalRoot, 'MERGEMIND.md'),
|
|
291
|
-
local: 'mm-local\n',
|
|
292
|
-
localPath: join(localRoot, 'MERGEMIND.md'),
|
|
293
|
-
})
|
|
294
|
-
expect(byName.AAA).toEqual({ domain: 'AAA', global: 'aaa-global-only\n', globalPath: join(globalRoot, 'AAA.md') })
|
|
295
|
-
expect(byName.ZZZ).toEqual({ domain: 'ZZZ', local: 'zzz-local-only\n', localPath: join(localRoot, 'ZZZ.md') })
|
|
296
|
-
expect(byName.MARKER).toEqual({ domain: 'MARKER', local: '', localPath: join(localRoot, 'MARKER.md') }) // present but empty
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
test('layer 5: fragments/ subdir scanned, paired by stem (guide global + index local), sorted', () => {
|
|
300
|
-
const input = gather()
|
|
301
|
-
const frags = input.promptFragments ?? []
|
|
302
|
-
expect(frags.map(f => f.domain)).toEqual(['iapeer-memory', 'other-primitive'])
|
|
303
|
-
const byName = Object.fromEntries(frags.map(f => [f.domain, f]))
|
|
304
|
-
const gfrag = join(globalRoot, 'fragments')
|
|
305
|
-
const lfrag = join(localRoot, 'fragments')
|
|
306
|
-
expect(byName['iapeer-memory']).toEqual({
|
|
307
|
-
domain: 'iapeer-memory',
|
|
308
|
-
global: 'mem-guide\n',
|
|
309
|
-
globalPath: join(gfrag, 'iapeer-memory.md'),
|
|
310
|
-
local: 'mem-index\n',
|
|
311
|
-
localPath: join(lfrag, 'iapeer-memory.md'),
|
|
312
|
-
})
|
|
313
|
-
expect(byName['other-primitive']).toEqual({ domain: 'other-primitive', global: 'other-frag\n', globalPath: join(gfrag, 'other-primitive.md') })
|
|
314
|
-
// the non-.md notes.txt never became a fragment
|
|
315
|
-
expect(frags.map(f => f.domain)).not.toContain('notes')
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
test('layer 5: fragments are NOT confused with the root-level Layer-4 domains', () => {
|
|
319
|
-
const input = gather()
|
|
320
|
-
// the fragments dir itself is a directory, not a `.md` file → never a Layer-4 domain
|
|
321
|
-
expect((input.pluginDomains ?? []).map(d => d.domain)).not.toContain('fragments')
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
test('layer 3: registry projected to EXACTLY 5 fields, sorted by personality', () => {
|
|
325
|
-
const input = gather()
|
|
326
|
-
expect((input.peers ?? []).map(p => p.personality)).toEqual(['arthur', 'boris'])
|
|
327
|
-
for (const p of input.peers ?? []) {
|
|
328
|
-
expect(Object.keys(p).sort()).toEqual(['description', 'intelligence', 'personality', 'runtime', 'runtimes'])
|
|
329
|
-
}
|
|
330
|
-
expect((input.peers ?? [])[0]).toEqual({
|
|
331
|
-
personality: 'arthur',
|
|
332
|
-
runtime: 'telegram',
|
|
333
|
-
runtimes: ['telegram', 'claude'],
|
|
334
|
-
description: 'owner',
|
|
335
|
-
intelligence: 'natural',
|
|
336
|
-
})
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
test('CHECKPOINT — the assembled prompt of a real peer contains all 4 layers, in order', () => {
|
|
340
|
-
const prompt = composeSystemPrompt(gather())
|
|
341
|
-
// Layer 1
|
|
342
|
-
expect(prompt).toContain('---\npersonality: "iapeer"')
|
|
343
|
-
// Layer 2
|
|
344
|
-
expect(prompt).toContain('GLOBAL DOCTRINE')
|
|
345
|
-
expect(prompt).toContain('LOCAL ROLE')
|
|
346
|
-
// Layer 3 (with descriptions)
|
|
347
|
-
expect(prompt).toContain('## Known peers')
|
|
348
|
-
expect(prompt).toContain('- arthur — owner')
|
|
349
|
-
expect(prompt).toContain('- boris — PM')
|
|
350
|
-
// Layer 4 (presence-marker file excluded, others per-file with path markers, sorted)
|
|
351
|
-
expect(prompt).toContain('aaa-global-only')
|
|
352
|
-
expect(prompt).toContain('zzz-local-only')
|
|
353
|
-
// global half then a fresh marker then the local half (each its own marked section)
|
|
354
|
-
expect(prompt).toContain(`<!-- ${join(globalRoot, 'MERGEMIND.md')} -->\nmm-global`)
|
|
355
|
-
expect(prompt).toContain(`mm-global\n\n<!-- ${join(localRoot, 'MERGEMIND.md')} -->\nmm-local`)
|
|
356
|
-
// Layer 5 (fragments: guide+index paired as separate marked sections, global-only fragment)
|
|
357
|
-
expect(prompt).toContain(`<!-- ${join(globalRoot, 'fragments', 'iapeer-memory.md')} -->\nmem-guide`)
|
|
358
|
-
expect(prompt).toContain(`mem-guide\n\n<!-- ${join(localRoot, 'fragments', 'iapeer-memory.md')} -->\nmem-index`)
|
|
359
|
-
expect(prompt).toContain('other-frag')
|
|
360
|
-
|
|
361
|
-
const order = [
|
|
362
|
-
prompt.indexOf('personality: "iapeer"'),
|
|
363
|
-
prompt.indexOf('LOCAL ROLE'),
|
|
364
|
-
prompt.indexOf('## Known peers'),
|
|
365
|
-
prompt.indexOf('aaa-global-only'),
|
|
366
|
-
prompt.indexOf('mem-guide'), // Layer 5 — after every Layer-4 domain
|
|
367
|
-
]
|
|
368
|
-
expect(order).toEqual([...order].sort((a, b) => a - b))
|
|
369
|
-
expect(prompt.indexOf('zzz-local-only')).toBeLessThan(prompt.indexOf('mem-guide'))
|
|
370
|
-
// marker file produced no content and no blank-block artifact
|
|
371
|
-
expect(prompt).not.toContain('MARKER')
|
|
372
|
-
expect(prompt).not.toMatch(/\n{4,}/)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
test('absent IAPEER.md → peerDoctrine "" (does not throw); empty dirs → no domains/peers', () => {
|
|
376
|
-
const bareCwd = join(root, 'bare')
|
|
377
|
-
const bareGlobal = join(root, 'bareglobal', '.iapeer')
|
|
378
|
-
mkdirSync(join(bareCwd, '.iapeer'), { recursive: true })
|
|
379
|
-
mkdirSync(bareGlobal, { recursive: true })
|
|
380
|
-
const input = gatherPromptInput({
|
|
381
|
-
personality: 'bare', description: '', cwd: bareCwd,
|
|
382
|
-
platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
|
|
383
|
-
globalRoot: bareGlobal,
|
|
384
|
-
})
|
|
385
|
-
expect(input.peerDoctrine).toBe('')
|
|
386
|
-
expect(input.globalDoctrine).toBeUndefined()
|
|
387
|
-
expect(input.pluginDomains).toEqual([])
|
|
388
|
-
expect(input.promptFragments).toEqual([]) // no fragments/ subdir → empty layer
|
|
389
|
-
expect(input.peers).toEqual([])
|
|
390
|
-
// composes to just the YAML block (no doctrine, no layers); the per-file
|
|
391
|
-
// assembly normalizes the single section's trailing newlines to exactly one.
|
|
392
|
-
const prompt = composeSystemPrompt(input)
|
|
393
|
-
expect(prompt).not.toContain('## Known peers')
|
|
394
|
-
expect(prompt).not.toContain('<!--') // no files → no markers
|
|
395
|
-
expect(prompt.endsWith('---\n')).toBe(true)
|
|
396
|
-
expect(prompt.endsWith('---\n\n')).toBe(false)
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
test('doctrine excluded from Layer 4 case-INSENSITIVELY; .MD ext picked up (verify edge-lens fix)', () => {
|
|
400
|
-
// A separate tree so a lowercase iapeer.md cannot collide with the uppercase
|
|
401
|
-
// IAPEER.md of the main fixture on a case-insensitive volume.
|
|
402
|
-
const ciCwd = join(root, 'ci')
|
|
403
|
-
const ciLocal = join(ciCwd, '.iapeer')
|
|
404
|
-
const ciGlobal = join(root, 'ciglobal', '.iapeer')
|
|
405
|
-
mkdirSync(ciLocal, { recursive: true })
|
|
406
|
-
mkdirSync(ciGlobal, { recursive: true })
|
|
407
|
-
writeFileSync(join(ciLocal, 'iapeer.md'), 'lowercase doctrine\n') // doctrine, lowercase name
|
|
408
|
-
writeFileSync(join(ciLocal, 'OTHER.md'), 'other domain\n')
|
|
409
|
-
writeFileSync(join(ciLocal, 'UP.MD'), 'upper ext domain\n') // uppercase extension
|
|
410
|
-
|
|
411
|
-
const input = gatherPromptInput({
|
|
412
|
-
personality: 'ci', description: '', cwd: ciCwd,
|
|
413
|
-
platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
|
|
414
|
-
globalRoot: ciGlobal, peers: [],
|
|
415
|
-
})
|
|
416
|
-
const domains = (input.pluginDomains ?? []).map(d => d.domain)
|
|
417
|
-
// The doctrine (whatever its on-disk case) must NEVER be a Layer-4 domain —
|
|
418
|
-
// before the fix, on a case-sensitive FS 'iapeer' would leak in here.
|
|
419
|
-
expect(domains).not.toContain('iapeer')
|
|
420
|
-
expect(domains).toContain('OTHER')
|
|
421
|
-
expect(domains).toContain('UP') // .MD upper-ext now included
|
|
422
|
-
|
|
423
|
-
// And the doctrine text is never duplicated: at most once (Layer 2 on a
|
|
424
|
-
// case-insensitive FS; zero on a case-sensitive FS where IAPEER.md is absent).
|
|
425
|
-
const prompt = composeSystemPrompt(input)
|
|
426
|
-
expect(prompt.split('lowercase doctrine').length - 1).toBeLessThanOrEqual(1)
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
test('gatherPromptInput honors a passed-in peers projection without a second registry read', () => {
|
|
430
|
-
// Point globalRoot at a dir with NO peers-profiles.json — if gather tried to
|
|
431
|
-
// read the registry it would just get []. We pass peers explicitly and expect
|
|
432
|
-
// them through (sorted), proving the thread-through path works.
|
|
433
|
-
const provided: PublicPeerSummary[] = [
|
|
434
|
-
{ personality: 'zeta', runtime: 'claude', runtimes: ['claude'], description: 'z', intelligence: 'artificial' },
|
|
435
|
-
{ personality: 'alpha', runtime: 'codex', runtimes: ['codex'], description: 'a', intelligence: 'artificial' },
|
|
436
|
-
]
|
|
437
|
-
const input = gatherPromptInput({
|
|
438
|
-
personality: 'x', description: '', cwd: join(root, 'noreg'),
|
|
439
|
-
platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
|
|
440
|
-
globalRoot: join(root, 'noreg-global'), peers: provided,
|
|
441
|
-
})
|
|
442
|
-
expect((input.peers ?? []).map(p => p.personality)).toEqual(['alpha', 'zeta']) // sorted, not mutated input
|
|
443
|
-
expect(provided.map(p => p.personality)).toEqual(['zeta', 'alpha']) // caller array untouched
|
|
444
|
-
})
|
|
445
|
-
})
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// composeSystemPrompt — the YAML identity block (Layer 1) stays byte-for-byte the
|
|
2
|
-
// claude-start.sh:264-290 jq layout (the durable golden); the file-sourced layers
|
|
3
|
-
// (2/4/5) are now rendered PER FILE, each prefixed by a `<!-- <path> -->` marker.
|
|
4
|
-
// This test pins BOTH: the jq-equivalent YAML prefix, and the marker format /
|
|
5
|
-
// empty-file-drops rules of the per-file assembly.
|
|
6
|
-
|
|
7
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
8
|
-
import { spawnSync } from 'child_process'
|
|
9
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
10
|
-
import { tmpdir } from 'os'
|
|
11
|
-
import { join } from 'path'
|
|
12
|
-
import { composeSystemPrompt } from './composeSystemPrompt.ts'
|
|
13
|
-
import type { ComposePromptInput } from './types.ts'
|
|
14
|
-
|
|
15
|
-
let dir: string
|
|
16
|
-
let jqProg: string
|
|
17
|
-
|
|
18
|
-
// The jq program verbatim from claude-start.sh:274-284 — the YAML identity block.
|
|
19
|
-
const JQ_PROGRAM = `"---",
|
|
20
|
-
"personality: \\($personality | @json)",
|
|
21
|
-
"description: \\($description | @json)",
|
|
22
|
-
"peer-cwd: \\($peer_cwd | @json)",
|
|
23
|
-
"platform: \\($platform | @json)",
|
|
24
|
-
"os_version: \\($os_version | @json)",
|
|
25
|
-
"user: \\($user | @json)",
|
|
26
|
-
"hostname: \\($hostname | @json)",
|
|
27
|
-
"today: \\($today | @json)",
|
|
28
|
-
"---",
|
|
29
|
-
""`
|
|
30
|
-
|
|
31
|
-
function shq(v: string): string {
|
|
32
|
-
return `'${v.replace(/'/g, `'\\''`)}'`
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
beforeAll(() => {
|
|
36
|
-
dir = mkdtempSync(join(tmpdir(), 'iapeer-csp-'))
|
|
37
|
-
jqProg = join(dir, 'prog.jq')
|
|
38
|
-
writeFileSync(jqProg, JQ_PROGRAM)
|
|
39
|
-
})
|
|
40
|
-
afterAll(() => rmSync(dir, { recursive: true, force: true }))
|
|
41
|
-
|
|
42
|
-
/** The jq YAML identity block alone (no doctrine appended) — the byte-for-byte
|
|
43
|
-
* Layer-1 golden every composed prompt must START with. */
|
|
44
|
-
function jqYamlBlock(input: ComposePromptInput): string {
|
|
45
|
-
const script = `set -e
|
|
46
|
-
jq -n --raw-output \
|
|
47
|
-
--arg personality ${shq(input.personality)} \
|
|
48
|
-
--arg description ${shq(input.description)} \
|
|
49
|
-
--arg peer_cwd ${shq(input.cwd)} \
|
|
50
|
-
--arg platform ${shq(input.platform)} \
|
|
51
|
-
--arg os_version ${shq(input.osVersion)} \
|
|
52
|
-
--arg user ${shq(input.user)} \
|
|
53
|
-
--arg hostname ${shq(input.hostname)} \
|
|
54
|
-
--arg today ${shq(input.today)} \
|
|
55
|
-
-f ${shq(jqProg)}`
|
|
56
|
-
const r = spawnSync('bash', ['-c', script], { encoding: 'utf8' })
|
|
57
|
-
if (r.status !== 0) throw new Error(`bash reference failed: ${r.stderr}`)
|
|
58
|
-
return r.stdout
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const base = {
|
|
62
|
-
personality: 'arthur',
|
|
63
|
-
cwd: '/Users/macmini/Peers/arthur',
|
|
64
|
-
platform: 'darwin',
|
|
65
|
-
osVersion: '15.5',
|
|
66
|
-
user: 'macmini',
|
|
67
|
-
hostname: 'macmini',
|
|
68
|
-
today: '2026-06-06',
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
-
// Layer 1 — the YAML identity block is byte-for-byte the jq layout
|
|
73
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
describe('composeSystemPrompt — Layer 1 YAML block is byte-for-byte the jq layout', () => {
|
|
76
|
-
const cases: Array<{ name: string; input: ComposePromptInput }> = [
|
|
77
|
-
{ name: 'basic', input: { ...base, description: 'owner', peerDoctrine: 'Be helpful.\n' } },
|
|
78
|
-
{ name: 'hostile description: colon + quote + newline', input: { ...base, description: 'a: "b"\nc', peerDoctrine: 'doc\n' } },
|
|
79
|
-
{ name: 'unicode + emoji description', input: { ...base, description: 'Артур 🦊 №2', peerDoctrine: 'док\n' } },
|
|
80
|
-
{ name: 'description with backslash and slash', input: { ...base, description: 'a\\b/c', peerDoctrine: 'x\n' } },
|
|
81
|
-
{ name: 'empty description', input: { ...base, description: '', peerDoctrine: 'x\n' } },
|
|
82
|
-
]
|
|
83
|
-
for (const c of cases) {
|
|
84
|
-
test(c.name, () => {
|
|
85
|
-
const yaml = jqYamlBlock(c.input)
|
|
86
|
-
// the composed prompt opens with the exact jq YAML block (the `---…---\n\n`)
|
|
87
|
-
expect(composeSystemPrompt(c.input).startsWith(yaml)).toBe(true)
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
-
// Layer 2 — IAPEER.md doctrine is rendered per-file with a path marker
|
|
94
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
describe('composeSystemPrompt — per-file path markers (Layer 2 doctrine)', () => {
|
|
97
|
-
const input: ComposePromptInput = {
|
|
98
|
-
...base,
|
|
99
|
-
description: 'owner',
|
|
100
|
-
peerDoctrine: 'Be the implementer.\n',
|
|
101
|
-
peerDoctrinePath: '~/Peers/arthur/.iapeer/IAPEER.md',
|
|
102
|
-
globalDoctrine: 'Host rules.\n',
|
|
103
|
-
globalDoctrinePath: '~/.iapeer/IAPEER.md',
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
test('each doctrine file is prefixed by its <!-- path --> marker, global before local', () => {
|
|
107
|
-
const out = composeSystemPrompt(input)
|
|
108
|
-
expect(out).toContain('<!-- ~/.iapeer/IAPEER.md -->\nHost rules.')
|
|
109
|
-
expect(out).toContain('<!-- ~/Peers/arthur/.iapeer/IAPEER.md -->\nBe the implementer.')
|
|
110
|
-
expect(out.indexOf('<!-- ~/.iapeer/IAPEER.md -->')).toBeLessThan(
|
|
111
|
-
out.indexOf('<!-- ~/Peers/arthur/.iapeer/IAPEER.md -->'),
|
|
112
|
-
)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test('marker sits AFTER the YAML block (one blank line between)', () => {
|
|
116
|
-
const out = composeSystemPrompt(input)
|
|
117
|
-
expect(out).toContain('---\n\n<!-- ~/.iapeer/IAPEER.md -->')
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('a path-less content (pure-renderer caller) renders WITHOUT a marker', () => {
|
|
121
|
-
const out = composeSystemPrompt({ ...base, description: 'd', peerDoctrine: 'no marker here\n' })
|
|
122
|
-
expect(out).toContain('no marker here')
|
|
123
|
-
expect(out).not.toContain('<!--')
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
test('an EMPTY doctrine file contributes no section and no dangling marker', () => {
|
|
127
|
-
const out = composeSystemPrompt({
|
|
128
|
-
...base,
|
|
129
|
-
description: 'd',
|
|
130
|
-
peerDoctrine: 'real\n',
|
|
131
|
-
peerDoctrinePath: '~/p/IAPEER.md',
|
|
132
|
-
globalDoctrine: '', // present but empty → dropped
|
|
133
|
-
globalDoctrinePath: '~/.iapeer/IAPEER.md',
|
|
134
|
-
})
|
|
135
|
-
expect(out).not.toContain('~/.iapeer/IAPEER.md') // empty global marker absent
|
|
136
|
-
expect(out).toContain('<!-- ~/p/IAPEER.md -->\nreal')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('output ends with exactly one newline; no 3+ blank-line gaps', () => {
|
|
140
|
-
const out = composeSystemPrompt(input)
|
|
141
|
-
expect(out.endsWith('\n')).toBe(true)
|
|
142
|
-
expect(out.endsWith('\n\n')).toBe(false)
|
|
143
|
-
expect(out).not.toMatch(/\n{3,}/)
|
|
144
|
-
})
|
|
145
|
-
})
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// Spawn-flip cutover Block 2, Ф0b-2 slice 2 — hosted composer-occupancy DETECTION.
|
|
2
|
-
//
|
|
3
|
-
// paneLogComposerOccupied reads a supervisor-HOSTED session's pane-log model and runs the burn-in-
|
|
4
|
-
// validated composerOccupancyFromModel (REUSED from the leaf, not reimplemented). These suites are
|
|
5
|
-
// boris's slice-2 acceptance: the guard FIRES on a busy-composer model-state and stays cold on an
|
|
6
|
-
// empty / dim-ghost composer — proven by injecting real rendered frames into a pane-log. The end-to-
|
|
7
|
-
// end hold (attached human + warm-deliver respects the guard) is validated with the attach client in
|
|
8
|
-
// Ф0b-3; here we bind the DETECTOR. Bun-native @xterm is loaded dynamically by the function under test.
|
|
9
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
10
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
11
|
-
import { tmpdir } from 'node:os'
|
|
12
|
-
import { join } from 'node:path'
|
|
13
|
-
import { paneLogComposerOccupied } from './readyGateModel.ts'
|
|
14
|
-
|
|
15
|
-
const COLS = 80
|
|
16
|
-
const ROWS = 24
|
|
17
|
-
|
|
18
|
-
let dir: string | null = null
|
|
19
|
-
function paneLogWith(frame: string): string {
|
|
20
|
-
dir = mkdtempSync(join(tmpdir(), 'iapeer-occ-'))
|
|
21
|
-
const log = join(dir, 'pane.log')
|
|
22
|
-
writeFileSync(log, Buffer.from(frame, 'utf8')) // raw bytes — preserve the multibyte ❯ / › glyphs
|
|
23
|
-
return log
|
|
24
|
-
}
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
if (dir) rmSync(dir, { recursive: true, force: true })
|
|
27
|
-
dir = null
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
describe('paneLogComposerOccupied — guard FIRES on a busy hosted composer (slice-2 acceptance)', () => {
|
|
31
|
-
test('claude ❯ with human text → BUSY (true)', async () => {
|
|
32
|
-
const log = paneLogWith('\x1b[24;1H❯ the quick brown fox')
|
|
33
|
-
expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(true)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('codex › with human text → BUSY (true)', async () => {
|
|
37
|
-
const log = paneLogWith('\x1b[24;1H› reviewer note')
|
|
38
|
-
expect(await paneLogComposerOccupied(log, COLS, ROWS, 'codex')).toBe(true)
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('paneLogComposerOccupied — guard stays COLD on a free composer', () => {
|
|
43
|
-
test('claude ❯ empty → free (false)', async () => {
|
|
44
|
-
const log = paneLogWith('\x1b[24;1H❯ ')
|
|
45
|
-
expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(false)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('dim-ghost placeholder (grey246 + dim) → NOT human → free (false)', async () => {
|
|
49
|
-
const log = paneLogWith('\x1b[24;1H❯ \x1b[2m\x1b[38;5;246mTry "fix typecheck errors"\x1b[0m')
|
|
50
|
-
expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('no prompt glyph rendered → free (false)', async () => {
|
|
54
|
-
const log = paneLogWith('\x1b[24;1Hjust some output, no composer')
|
|
55
|
-
expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(false)
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe('paneLogComposerOccupied — uncertainty is conservative (BUSY)', () => {
|
|
60
|
-
test('missing pane-log → BUSY (true): never paste into a maybe-occupied composer', async () => {
|
|
61
|
-
expect(await paneLogComposerOccupied(join(tmpdir(), 'iapeer-occ-nonexistent.log'), COLS, ROWS, 'claude')).toBe(true)
|
|
62
|
-
})
|
|
63
|
-
})
|