@agfpd/iapeer 0.1.0
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/bin/iapeer +25 -0
- package/package.json +37 -0
- package/src/cli/cli.test.ts +130 -0
- package/src/cli/index.ts +608 -0
- package/src/cli/listTui.test.ts +70 -0
- package/src/cli/listTui.ts +165 -0
- package/src/codec/codec.test.ts +271 -0
- package/src/codec/index.ts +217 -0
- package/src/core/constants.test.ts +21 -0
- package/src/core/constants.ts +180 -0
- package/src/core/errors.ts +20 -0
- package/src/core/index.ts +3 -0
- package/src/core/normalize.test.ts +98 -0
- package/src/core/normalize.ts +89 -0
- package/src/core/socket.ts +63 -0
- package/src/create/create.test.ts +143 -0
- package/src/create/index.ts +178 -0
- package/src/daemon/daemon-http.test.ts +114 -0
- package/src/daemon/daemon.test.ts +103 -0
- package/src/daemon/index.ts +439 -0
- package/src/daemon/main.test.ts +194 -0
- package/src/daemon/main.ts +230 -0
- package/src/enable/enable.test.ts +92 -0
- package/src/enable/index.ts +381 -0
- package/src/identity/identity.test.ts +262 -0
- package/src/identity/index.ts +603 -0
- package/src/index.ts +27 -0
- package/src/init/index.ts +408 -0
- package/src/init/init.test.ts +171 -0
- package/src/init/runtime-resolve.test.ts +49 -0
- package/src/install/index.ts +84 -0
- package/src/install/install.test.ts +31 -0
- package/src/launch/adapters/claude.ts +250 -0
- package/src/launch/adapters/codex.ts +329 -0
- package/src/launch/adapters/notifier.ts +90 -0
- package/src/launch/adapters/telegram.ts +130 -0
- package/src/launch/bootstrap.test.ts +56 -0
- package/src/launch/composeSystemPrompt.layers.test.ts +319 -0
- package/src/launch/composeSystemPrompt.test.ts +98 -0
- package/src/launch/composeSystemPrompt.ts +261 -0
- package/src/launch/index.ts +253 -0
- package/src/launch/launch.test.ts +233 -0
- package/src/launch/launchd.test.ts +363 -0
- package/src/launch/launchd.ts +375 -0
- package/src/launch/launchdRun.ts +168 -0
- package/src/launch/sockdir.test.ts +70 -0
- package/src/launch/types.ts +300 -0
- package/src/lifecycle/index.ts +840 -0
- package/src/lifecycle/lifecycle.test.ts +496 -0
- package/src/onboard/index.ts +135 -0
- package/src/onboard/onboard.test.ts +39 -0
- package/src/provision/index.ts +170 -0
- package/src/provision/provision.test.ts +104 -0
- package/src/registry/index.ts +453 -0
- package/src/registry/registry.test.ts +400 -0
- package/src/runtime/deploy.ts +230 -0
- package/src/runtime/index.ts +191 -0
- package/src/runtime/runtime.test.ts +226 -0
- package/src/storage/index.ts +331 -0
- package/src/storage/peers-home.test.ts +34 -0
- package/src/storage/storage.test.ts +65 -0
- package/src/transport/index.ts +522 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,319 @@
|
|
|
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)', () => {
|
|
101
|
+
const out = composeSystemPrompt({
|
|
102
|
+
...base,
|
|
103
|
+
pluginDomains: [dom({ domain: 'MM', global: 'mm-global', local: 'mm-local' })],
|
|
104
|
+
})
|
|
105
|
+
expect(out).toContain('mm-global\nmm-local')
|
|
106
|
+
expect(out.indexOf('mm-global')).toBeLessThan(out.indexOf('mm-local'))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('layer 4 sits after layer 3; domains kept in the order given', () => {
|
|
110
|
+
const out = composeSystemPrompt({
|
|
111
|
+
...base,
|
|
112
|
+
peers: [peerA],
|
|
113
|
+
pluginDomains: [dom({ domain: 'AAA', global: 'aaa' }), dom({ domain: 'ZZZ', local: 'zzz' })],
|
|
114
|
+
})
|
|
115
|
+
expect(out.indexOf('## Known peers')).toBeLessThan(out.indexOf('aaa'))
|
|
116
|
+
expect(out.indexOf('aaa')).toBeLessThan(out.indexOf('zzz'))
|
|
117
|
+
expect(out).toContain('aaa\n\nzzz')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('full 4-layer ordering: YAML → doctrine → registry → domains', () => {
|
|
121
|
+
const out = composeSystemPrompt({ ...base, peers: [peerA], pluginDomains: [dom({ global: 'domain-text' })] })
|
|
122
|
+
const iYaml = out.indexOf('personality:')
|
|
123
|
+
const iDoctrine = out.indexOf('Be the implementer.')
|
|
124
|
+
const iReg = out.indexOf('## Known peers')
|
|
125
|
+
const iDom = out.indexOf('domain-text')
|
|
126
|
+
expect(iYaml).toBeGreaterThanOrEqual(0)
|
|
127
|
+
expect(iYaml).toBeLessThan(iDoctrine)
|
|
128
|
+
expect(iDoctrine).toBeLessThan(iReg)
|
|
129
|
+
expect(iReg).toBeLessThan(iDom)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// gatherPromptInput — FS discovery over ~/.iapeer + <cwd>/.iapeer
|
|
135
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe('gatherPromptInput — FS discovery of all 4 layers', () => {
|
|
138
|
+
let root: string
|
|
139
|
+
let globalRoot: string
|
|
140
|
+
let cwd: string
|
|
141
|
+
let localRoot: string
|
|
142
|
+
|
|
143
|
+
beforeAll(() => {
|
|
144
|
+
root = mkdtempSync(join(tmpdir(), 'iapeer-gather-'))
|
|
145
|
+
globalRoot = join(root, 'home', '.iapeer')
|
|
146
|
+
cwd = join(root, 'peer')
|
|
147
|
+
localRoot = join(cwd, '.iapeer')
|
|
148
|
+
mkdirSync(globalRoot, { recursive: true })
|
|
149
|
+
mkdirSync(localRoot, { recursive: true })
|
|
150
|
+
|
|
151
|
+
// Layer 2 — IAPEER.md, global + local
|
|
152
|
+
writeFileSync(join(globalRoot, 'IAPEER.md'), 'GLOBAL DOCTRINE\n')
|
|
153
|
+
writeFileSync(join(localRoot, 'IAPEER.md'), 'LOCAL ROLE\n')
|
|
154
|
+
|
|
155
|
+
// Layer 4 — domains: both halves / global-only / local-only / 0-byte marker
|
|
156
|
+
writeFileSync(join(globalRoot, 'MERGEMIND.md'), 'mm-global\n')
|
|
157
|
+
writeFileSync(join(localRoot, 'MERGEMIND.md'), 'mm-local\n')
|
|
158
|
+
writeFileSync(join(globalRoot, 'AAA.md'), 'aaa-global-only\n')
|
|
159
|
+
writeFileSync(join(localRoot, 'ZZZ.md'), 'zzz-local-only\n')
|
|
160
|
+
writeFileSync(join(localRoot, 'MARKER.md'), '') // presence marker, must not merge
|
|
161
|
+
// a non-.md file must be ignored
|
|
162
|
+
writeFileSync(join(localRoot, 'peer-profile.json'), '{}')
|
|
163
|
+
|
|
164
|
+
// Layer 3 — registry at the global root (deliberately UNSORTED on disk)
|
|
165
|
+
const index = {
|
|
166
|
+
version: 2,
|
|
167
|
+
peers: [
|
|
168
|
+
{ personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: 'PM', intelligence: 'artificial', cwd: '/p/boris' },
|
|
169
|
+
{ personality: 'arthur', runtime: 'telegram', runtimes: ['telegram', 'claude'], description: 'owner', intelligence: 'natural', cwd: '/p/arthur' },
|
|
170
|
+
],
|
|
171
|
+
}
|
|
172
|
+
writeFileSync(join(globalRoot, 'peers-profiles.json'), JSON.stringify(index))
|
|
173
|
+
})
|
|
174
|
+
afterAll(() => rmSync(root, { recursive: true, force: true }))
|
|
175
|
+
|
|
176
|
+
function gather() {
|
|
177
|
+
return gatherPromptInput({
|
|
178
|
+
personality: 'iapeer',
|
|
179
|
+
description: 'impl',
|
|
180
|
+
cwd,
|
|
181
|
+
platform: 'darwin',
|
|
182
|
+
osVersion: '15.5',
|
|
183
|
+
user: 'u',
|
|
184
|
+
hostname: 'h',
|
|
185
|
+
today: '2026-06-06',
|
|
186
|
+
globalRoot,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test('layer 2: IAPEER.md read as global + local', () => {
|
|
191
|
+
const input = gather()
|
|
192
|
+
expect(input.globalDoctrine).toBe('GLOBAL DOCTRINE\n')
|
|
193
|
+
expect(input.peerDoctrine).toBe('LOCAL ROLE\n')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('layer 4: IAPEER.md excluded; other domains discovered, sorted, non-.md ignored', () => {
|
|
197
|
+
const input = gather()
|
|
198
|
+
expect((input.pluginDomains ?? []).map(d => d.domain)).toEqual(['AAA', 'MARKER', 'MERGEMIND', 'ZZZ'])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('layer 4: per-domain global/local halves resolved correctly', () => {
|
|
202
|
+
const input = gather()
|
|
203
|
+
const byName = Object.fromEntries((input.pluginDomains ?? []).map(d => [d.domain, d]))
|
|
204
|
+
expect(byName.MERGEMIND).toEqual({ domain: 'MERGEMIND', global: 'mm-global\n', local: 'mm-local\n' })
|
|
205
|
+
expect(byName.AAA).toEqual({ domain: 'AAA', global: 'aaa-global-only\n' })
|
|
206
|
+
expect(byName.ZZZ).toEqual({ domain: 'ZZZ', local: 'zzz-local-only\n' })
|
|
207
|
+
expect(byName.MARKER).toEqual({ domain: 'MARKER', local: '' }) // present but empty
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('layer 3: registry projected to EXACTLY 5 fields, sorted by personality', () => {
|
|
211
|
+
const input = gather()
|
|
212
|
+
expect((input.peers ?? []).map(p => p.personality)).toEqual(['arthur', 'boris'])
|
|
213
|
+
for (const p of input.peers ?? []) {
|
|
214
|
+
expect(Object.keys(p).sort()).toEqual(['description', 'intelligence', 'personality', 'runtime', 'runtimes'])
|
|
215
|
+
}
|
|
216
|
+
expect((input.peers ?? [])[0]).toEqual({
|
|
217
|
+
personality: 'arthur',
|
|
218
|
+
runtime: 'telegram',
|
|
219
|
+
runtimes: ['telegram', 'claude'],
|
|
220
|
+
description: 'owner',
|
|
221
|
+
intelligence: 'natural',
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('CHECKPOINT — the assembled prompt of a real peer contains all 4 layers, in order', () => {
|
|
226
|
+
const prompt = composeSystemPrompt(gather())
|
|
227
|
+
// Layer 1
|
|
228
|
+
expect(prompt).toContain('---\npersonality: "iapeer"')
|
|
229
|
+
// Layer 2
|
|
230
|
+
expect(prompt).toContain('GLOBAL DOCTRINE')
|
|
231
|
+
expect(prompt).toContain('LOCAL ROLE')
|
|
232
|
+
// Layer 3 (with descriptions)
|
|
233
|
+
expect(prompt).toContain('## Known peers')
|
|
234
|
+
expect(prompt).toContain('- arthur — owner')
|
|
235
|
+
expect(prompt).toContain('- boris — PM')
|
|
236
|
+
// Layer 4 (markers excluded, others merged, sorted)
|
|
237
|
+
expect(prompt).toContain('aaa-global-only')
|
|
238
|
+
expect(prompt).toContain('mm-global\nmm-local')
|
|
239
|
+
expect(prompt).toContain('zzz-local-only')
|
|
240
|
+
|
|
241
|
+
const order = [
|
|
242
|
+
prompt.indexOf('personality: "iapeer"'),
|
|
243
|
+
prompt.indexOf('LOCAL ROLE'),
|
|
244
|
+
prompt.indexOf('## Known peers'),
|
|
245
|
+
prompt.indexOf('aaa-global-only'),
|
|
246
|
+
]
|
|
247
|
+
expect(order).toEqual([...order].sort((a, b) => a - b))
|
|
248
|
+
// marker file produced no content and no blank-block artifact
|
|
249
|
+
expect(prompt).not.toContain('MARKER')
|
|
250
|
+
expect(prompt).not.toMatch(/\n{4,}/)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('absent IAPEER.md → peerDoctrine "" (does not throw); empty dirs → no domains/peers', () => {
|
|
254
|
+
const bareCwd = join(root, 'bare')
|
|
255
|
+
const bareGlobal = join(root, 'bareglobal', '.iapeer')
|
|
256
|
+
mkdirSync(join(bareCwd, '.iapeer'), { recursive: true })
|
|
257
|
+
mkdirSync(bareGlobal, { recursive: true })
|
|
258
|
+
const input = gatherPromptInput({
|
|
259
|
+
personality: 'bare', description: '', cwd: bareCwd,
|
|
260
|
+
platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
|
|
261
|
+
globalRoot: bareGlobal,
|
|
262
|
+
})
|
|
263
|
+
expect(input.peerDoctrine).toBe('')
|
|
264
|
+
expect(input.globalDoctrine).toBeUndefined()
|
|
265
|
+
expect(input.pluginDomains).toEqual([])
|
|
266
|
+
expect(input.peers).toEqual([])
|
|
267
|
+
// composes to just the YAML block (+ empty doctrine), no extra layers
|
|
268
|
+
const prompt = composeSystemPrompt(input)
|
|
269
|
+
expect(prompt).not.toContain('## Known peers')
|
|
270
|
+
expect(prompt.endsWith('---\n\n')).toBe(true)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('doctrine excluded from Layer 4 case-INSENSITIVELY; .MD ext picked up (verify edge-lens fix)', () => {
|
|
274
|
+
// A separate tree so a lowercase iapeer.md cannot collide with the uppercase
|
|
275
|
+
// IAPEER.md of the main fixture on a case-insensitive volume.
|
|
276
|
+
const ciCwd = join(root, 'ci')
|
|
277
|
+
const ciLocal = join(ciCwd, '.iapeer')
|
|
278
|
+
const ciGlobal = join(root, 'ciglobal', '.iapeer')
|
|
279
|
+
mkdirSync(ciLocal, { recursive: true })
|
|
280
|
+
mkdirSync(ciGlobal, { recursive: true })
|
|
281
|
+
writeFileSync(join(ciLocal, 'iapeer.md'), 'lowercase doctrine\n') // doctrine, lowercase name
|
|
282
|
+
writeFileSync(join(ciLocal, 'OTHER.md'), 'other domain\n')
|
|
283
|
+
writeFileSync(join(ciLocal, 'UP.MD'), 'upper ext domain\n') // uppercase extension
|
|
284
|
+
|
|
285
|
+
const input = gatherPromptInput({
|
|
286
|
+
personality: 'ci', description: '', cwd: ciCwd,
|
|
287
|
+
platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
|
|
288
|
+
globalRoot: ciGlobal, peers: [],
|
|
289
|
+
})
|
|
290
|
+
const domains = (input.pluginDomains ?? []).map(d => d.domain)
|
|
291
|
+
// The doctrine (whatever its on-disk case) must NEVER be a Layer-4 domain —
|
|
292
|
+
// before the fix, on a case-sensitive FS 'iapeer' would leak in here.
|
|
293
|
+
expect(domains).not.toContain('iapeer')
|
|
294
|
+
expect(domains).toContain('OTHER')
|
|
295
|
+
expect(domains).toContain('UP') // .MD upper-ext now included
|
|
296
|
+
|
|
297
|
+
// And the doctrine text is never duplicated: at most once (Layer 2 on a
|
|
298
|
+
// case-insensitive FS; zero on a case-sensitive FS where IAPEER.md is absent).
|
|
299
|
+
const prompt = composeSystemPrompt(input)
|
|
300
|
+
expect(prompt.split('lowercase doctrine').length - 1).toBeLessThanOrEqual(1)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('gatherPromptInput honors a passed-in peers projection without a second registry read', () => {
|
|
304
|
+
// Point globalRoot at a dir with NO peers-profiles.json — if gather tried to
|
|
305
|
+
// read the registry it would just get []. We pass peers explicitly and expect
|
|
306
|
+
// them through (sorted), proving the thread-through path works.
|
|
307
|
+
const provided: PublicPeerSummary[] = [
|
|
308
|
+
{ personality: 'zeta', runtime: 'claude', runtimes: ['claude'], description: 'z', intelligence: 'artificial' },
|
|
309
|
+
{ personality: 'alpha', runtime: 'codex', runtimes: ['codex'], description: 'a', intelligence: 'artificial' },
|
|
310
|
+
]
|
|
311
|
+
const input = gatherPromptInput({
|
|
312
|
+
personality: 'x', description: '', cwd: join(root, 'noreg'),
|
|
313
|
+
platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
|
|
314
|
+
globalRoot: join(root, 'noreg-global'), peers: provided,
|
|
315
|
+
})
|
|
316
|
+
expect((input.peers ?? []).map(p => p.personality)).toEqual(['alpha', 'zeta']) // sorted, not mutated input
|
|
317
|
+
expect(provided.map(p => p.personality)).toEqual(['zeta', 'alpha']) // caller array untouched
|
|
318
|
+
})
|
|
319
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Golden-equivalence test: composeSystemPrompt MUST produce byte-for-byte the
|
|
2
|
+
// same output as the real jq doctrine-merge in claude-start.sh:264-290. Each
|
|
3
|
+
// case runs the ACTUAL jq pipeline (extracted verbatim) and the TS function,
|
|
4
|
+
// then byte-compares. This is the durable form of the Ф3 adversarial-verify
|
|
5
|
+
// (which caught a 1-byte divergence on the empty-global-file case).
|
|
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.
|
|
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
|
+
/** Run the exact bash/jq pipeline; `globalDoctrine` undefined = file absent. */
|
|
43
|
+
function bashReference(input: ComposePromptInput): string {
|
|
44
|
+
const peerPath = join(dir, 'peer.md')
|
|
45
|
+
writeFileSync(peerPath, input.peerDoctrine)
|
|
46
|
+
let globalBlock = ''
|
|
47
|
+
if (input.globalDoctrine !== undefined) {
|
|
48
|
+
const gp = join(dir, 'global.md')
|
|
49
|
+
writeFileSync(gp, input.globalDoctrine)
|
|
50
|
+
globalBlock = `if [ -f ${shq(gp)} ]; then cat ${shq(gp)}; printf "\\n"; fi`
|
|
51
|
+
}
|
|
52
|
+
const script = `set -e
|
|
53
|
+
jq -n --raw-output \
|
|
54
|
+
--arg personality ${shq(input.personality)} \
|
|
55
|
+
--arg description ${shq(input.description)} \
|
|
56
|
+
--arg peer_cwd ${shq(input.cwd)} \
|
|
57
|
+
--arg platform ${shq(input.platform)} \
|
|
58
|
+
--arg os_version ${shq(input.osVersion)} \
|
|
59
|
+
--arg user ${shq(input.user)} \
|
|
60
|
+
--arg hostname ${shq(input.hostname)} \
|
|
61
|
+
--arg today ${shq(input.today)} \
|
|
62
|
+
-f ${shq(jqProg)}
|
|
63
|
+
${globalBlock}
|
|
64
|
+
cat ${shq(peerPath)}`
|
|
65
|
+
const r = spawnSync('bash', ['-c', script], { encoding: 'utf8' })
|
|
66
|
+
if (r.status !== 0) throw new Error(`bash reference failed: ${r.stderr}`)
|
|
67
|
+
return r.stdout
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const base = {
|
|
71
|
+
personality: 'arthur',
|
|
72
|
+
cwd: '/Users/macmini/Peers/arthur',
|
|
73
|
+
platform: 'darwin',
|
|
74
|
+
osVersion: '15.5',
|
|
75
|
+
user: 'macmini',
|
|
76
|
+
hostname: 'macmini',
|
|
77
|
+
today: '2026-06-06',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cases: Array<{ name: string; input: ComposePromptInput }> = [
|
|
81
|
+
{ name: 'basic, no global', input: { ...base, description: 'owner', peerDoctrine: 'Be helpful.\n' } },
|
|
82
|
+
{ name: 'present non-empty global', input: { ...base, description: 'owner', peerDoctrine: 'peer\n', globalDoctrine: 'GLOBAL\n' } },
|
|
83
|
+
{ name: 'EMPTY-but-present global (the 1-byte divergence the verify caught)', input: { ...base, description: 'owner', peerDoctrine: 'peer', globalDoctrine: '' } },
|
|
84
|
+
{ name: 'hostile description: colon + quote + newline', input: { ...base, description: 'a: "b"\nc', peerDoctrine: 'doc\n' } },
|
|
85
|
+
{ name: 'unicode + emoji description', input: { ...base, description: 'Артур 🦊 №2', peerDoctrine: 'док\n', globalDoctrine: 'гл\n' } },
|
|
86
|
+
{ name: 'multiline peer doctrine, no trailing newline', input: { ...base, description: 'd', peerDoctrine: 'line1\nline2' } },
|
|
87
|
+
{ name: 'description with backslash and slash', input: { ...base, description: 'a\\b/c', peerDoctrine: 'x\n' } },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
describe('composeSystemPrompt golden-equivalence vs claude-start.sh jq', () => {
|
|
91
|
+
for (const c of cases) {
|
|
92
|
+
test(c.name, () => {
|
|
93
|
+
const ref = bashReference(c.input)
|
|
94
|
+
const got = composeSystemPrompt(c.input)
|
|
95
|
+
expect(got).toBe(ref)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|