@agfpd/iapeer 0.2.77 → 0.2.78
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 +47 -48
- 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 +14 -8
- 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,279 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { existsSync, mkdtempSync, rmSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
3
|
-
import { tmpdir } from 'os'
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import {
|
|
6
|
-
ensurePeerProfile,
|
|
7
|
-
readPeerProfile,
|
|
8
|
-
writePeerProfileAtomic,
|
|
9
|
-
resolveCallerIdentity,
|
|
10
|
-
type PeerProfileWrite,
|
|
11
|
-
} from './index.ts'
|
|
12
|
-
import { peerProfilePath } from '../storage/index.ts'
|
|
13
|
-
import { isFoundationOwnedPlist, launchdPlistPath } from '../launch/index.ts'
|
|
14
|
-
import { defaultIntelligenceForRuntime } from '../core/constants.ts'
|
|
15
|
-
import type { PeersIndex } from '../registry/index.ts'
|
|
16
|
-
|
|
17
|
-
let cwd: string
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
cwd = mkdtempSync(join(tmpdir(), 'iapeer-identity-'))
|
|
20
|
-
})
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
rmSync(cwd, { recursive: true, force: true })
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
function readDisk(): Record<string, unknown> {
|
|
26
|
-
return JSON.parse(readFileSync(peerProfilePath(cwd), 'utf8'))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// WITNESS: old writePeerProfileAtomic merged-builder (identity.ts:247-263) wrote
|
|
30
|
-
// `intelligence: profile.intelligence` UNCONDITIONALLY. Reproduce the merge so a
|
|
31
|
-
// call site that lost the existing intelligence (passing the runtime default)
|
|
32
|
-
// would clobber a human profile.
|
|
33
|
-
function oldMerged(existing: Record<string, unknown>, profile: {
|
|
34
|
-
personality: string; runtime: string; runtimes: string[]; description: string; intelligence: string
|
|
35
|
-
}): Record<string, unknown> {
|
|
36
|
-
return {
|
|
37
|
-
...existing,
|
|
38
|
-
personality: profile.personality,
|
|
39
|
-
runtime: profile.runtime,
|
|
40
|
-
runtimes: profile.runtimes,
|
|
41
|
-
description: profile.description, // empty wipes
|
|
42
|
-
intelligence: profile.intelligence, // unconditional overwrite
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
-
// writePeerProfileAtomic — H1 preserve + unknown-field preservation
|
|
48
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
describe('writePeerProfileAtomic H1 preserve', () => {
|
|
51
|
-
test('write WITHOUT intelligence does NOT downgrade a human profile on disk', () => {
|
|
52
|
-
// seed a human profile with a foreign (persistent-peer) section + interfaces
|
|
53
|
-
mkdirSync(join(cwd, '.iapeer'), { recursive: true })
|
|
54
|
-
writeFileSync(
|
|
55
|
-
peerProfilePath(cwd),
|
|
56
|
-
JSON.stringify({
|
|
57
|
-
personality: 'arthur',
|
|
58
|
-
runtime: 'telegram',
|
|
59
|
-
runtimes: ['telegram', 'claude'],
|
|
60
|
-
description: 'Артур — владелец.',
|
|
61
|
-
intelligence: 'human',
|
|
62
|
-
'persistent-peer': { initial_prompt: 'hi', aliases: { '/new': 'x' } },
|
|
63
|
-
interfaces: { telegram: { user_id: '409502965' } },
|
|
64
|
-
}),
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
// a runtimes-only update built WITHOUT re-asserting intelligence
|
|
68
|
-
const update: PeerProfileWrite = {
|
|
69
|
-
personality: 'arthur',
|
|
70
|
-
runtime: 'claude',
|
|
71
|
-
runtimes: ['telegram', 'claude'],
|
|
72
|
-
}
|
|
73
|
-
writePeerProfileAtomic(cwd, update)
|
|
74
|
-
|
|
75
|
-
const disk = readDisk()
|
|
76
|
-
// H1 + NO-MIGRATION: the legacy on-disk value is preserved VERBATIM (not
|
|
77
|
-
// downgraded, and NOT silently rewritten to 'natural' — that migration is a
|
|
78
|
-
// separate coordinated step that must not happen on a routine write).
|
|
79
|
-
expect(disk.intelligence).toBe('human')
|
|
80
|
-
// READ-COMPAT: but reading it back normalizes legacy human → contract natural
|
|
81
|
-
expect(readPeerProfile(cwd)!.intelligence).toBe('natural')
|
|
82
|
-
// WITNESS: old builder fed the claude runtime-default would write artificial
|
|
83
|
-
expect(oldMerged(disk, {
|
|
84
|
-
personality: 'arthur', runtime: 'claude', runtimes: ['claude'],
|
|
85
|
-
description: '', intelligence: defaultIntelligenceForRuntime('claude'),
|
|
86
|
-
}).intelligence).toBe('artificial')
|
|
87
|
-
// unknown foreign section preserved
|
|
88
|
-
expect(disk['persistent-peer']).toEqual({ initial_prompt: 'hi', aliases: { '/new': 'x' } })
|
|
89
|
-
// interfaces preserved
|
|
90
|
-
expect(disk.interfaces).toEqual({ telegram: { user_id: '409502965' } })
|
|
91
|
-
// empty description not written over the existing one
|
|
92
|
-
expect(disk.description).toBe('Артур — владелец.')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('explicit intelligence in write IS applied', () => {
|
|
96
|
-
writePeerProfileAtomic(cwd, {
|
|
97
|
-
personality: 'p', runtime: 'webhook', runtimes: ['webhook'], intelligence: 'absent',
|
|
98
|
-
})
|
|
99
|
-
expect(readDisk().intelligence).toBe('absent')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('new profile (no disk) without intelligence → runtime default', () => {
|
|
103
|
-
writePeerProfileAtomic(cwd, { personality: 'p', runtime: 'telegram', runtimes: ['telegram'] })
|
|
104
|
-
expect(readDisk().intelligence).toBe('natural') // telegram default
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
test('round-trips through readPeerProfile preserving foreign fields', () => {
|
|
108
|
-
writePeerProfileAtomic(cwd, {
|
|
109
|
-
personality: 'p', runtime: 'claude', runtimes: ['claude'],
|
|
110
|
-
description: 'desc', intelligence: 'artificial',
|
|
111
|
-
})
|
|
112
|
-
// inject a foreign field, then a second write must keep it
|
|
113
|
-
const disk = readDisk()
|
|
114
|
-
disk['persistent-peer'] = { initial_prompt: 'keep me' }
|
|
115
|
-
writeFileSync(peerProfilePath(cwd), JSON.stringify(disk))
|
|
116
|
-
writePeerProfileAtomic(cwd, { personality: 'p', runtime: 'claude', runtimes: ['claude', 'codex'] })
|
|
117
|
-
const after = readDisk()
|
|
118
|
-
expect(after['persistent-peer']).toEqual({ initial_prompt: 'keep me' })
|
|
119
|
-
expect(after.intelligence).toBe('artificial') // still preserved
|
|
120
|
-
expect(after.runtimes).toEqual(['claude', 'codex'])
|
|
121
|
-
const parsed = readPeerProfile(cwd)!
|
|
122
|
-
expect(parsed.intelligence).toBe('artificial')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test('wake_policy "ephemeral" parsed; unknown/absent → omitted (never throws)', () => {
|
|
126
|
-
mkdirSync(join(cwd, '.iapeer'), { recursive: true })
|
|
127
|
-
// honored enum value
|
|
128
|
-
writeFileSync(peerProfilePath(cwd), JSON.stringify({
|
|
129
|
-
personality: 'p', runtime: 'claude', runtimes: ['claude'], wake_policy: 'ephemeral',
|
|
130
|
-
}))
|
|
131
|
-
expect(readPeerProfile(cwd)!.wake_policy).toBe('ephemeral')
|
|
132
|
-
// absent → undefined
|
|
133
|
-
writeFileSync(peerProfilePath(cwd), JSON.stringify({ personality: 'p', runtime: 'claude', runtimes: ['claude'] }))
|
|
134
|
-
expect(readPeerProfile(cwd)!.wake_policy).toBeUndefined()
|
|
135
|
-
// unknown value → omitted (forward-compatible, no throw)
|
|
136
|
-
writeFileSync(peerProfilePath(cwd), JSON.stringify({
|
|
137
|
-
personality: 'p', runtime: 'claude', runtimes: ['claude'], wake_policy: 'persistent-future',
|
|
138
|
-
}))
|
|
139
|
-
expect(readPeerProfile(cwd)!.wake_policy).toBeUndefined()
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
144
|
-
// resolveCallerIdentity — per-request, NO process.cwd()
|
|
145
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
const index: PeersIndex = {
|
|
148
|
-
version: 2,
|
|
149
|
-
peers: [
|
|
150
|
-
{
|
|
151
|
-
personality: 'arthur',
|
|
152
|
-
runtime: 'telegram',
|
|
153
|
-
runtimes: ['telegram', 'claude'],
|
|
154
|
-
description: 'Артур — владелец.',
|
|
155
|
-
intelligence: 'natural',
|
|
156
|
-
cwd: '/Users/macmini/Peers/arthur',
|
|
157
|
-
interfaces: { telegram: { user_id: '409502965' } },
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
personality: 'boris',
|
|
161
|
-
runtime: 'claude',
|
|
162
|
-
runtimes: ['claude'],
|
|
163
|
-
description: 'Напарник.',
|
|
164
|
-
intelligence: 'artificial',
|
|
165
|
-
cwd: '/Users/macmini/Peers/boris',
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
describe('resolveCallerIdentity (per-request)', () => {
|
|
171
|
-
test('resolves caller from registry record, builds address', () => {
|
|
172
|
-
const r = resolveCallerIdentity({ personality: 'boris', runtime: 'claude' }, index)
|
|
173
|
-
expect(r.address).toBe('claude-boris')
|
|
174
|
-
expect(r.cwd).toBe('/Users/macmini/Peers/boris')
|
|
175
|
-
expect(r.intelligence).toBe('artificial')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test('multi-runtime peer resolves each declared runtime to a distinct address', () => {
|
|
179
|
-
expect(resolveCallerIdentity({ personality: 'arthur', runtime: 'telegram' }, index).address).toBe(
|
|
180
|
-
'telegram-arthur',
|
|
181
|
-
)
|
|
182
|
-
expect(resolveCallerIdentity({ personality: 'arthur', runtime: 'claude' }, index).address).toBe(
|
|
183
|
-
'claude-arthur',
|
|
184
|
-
)
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test('does NOT depend on process.cwd() — chdir does not change the result', () => {
|
|
188
|
-
const before = resolveCallerIdentity({ personality: 'arthur', runtime: 'claude' }, index)
|
|
189
|
-
const original = process.cwd()
|
|
190
|
-
const elsewhere = mkdtempSync(join(tmpdir(), 'iapeer-chdir-'))
|
|
191
|
-
try {
|
|
192
|
-
process.chdir(elsewhere)
|
|
193
|
-
const after = resolveCallerIdentity({ personality: 'arthur', runtime: 'claude' }, index)
|
|
194
|
-
expect(after).toEqual(before)
|
|
195
|
-
expect(after.cwd).toBe('/Users/macmini/Peers/arthur') // from registry, not process.cwd()
|
|
196
|
-
} finally {
|
|
197
|
-
process.chdir(original)
|
|
198
|
-
rmSync(elsewhere, { recursive: true, force: true })
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
test('unknown caller → throws (spoofing guard)', () => {
|
|
203
|
-
expect(() => resolveCallerIdentity({ personality: 'ghost', runtime: 'claude' }, index)).toThrow(
|
|
204
|
-
/unknown caller/,
|
|
205
|
-
)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('undeclared runtime for a known caller → throws', () => {
|
|
209
|
-
// boris declares only claude; codex must be rejected
|
|
210
|
-
expect(() => resolveCallerIdentity({ personality: 'boris', runtime: 'codex' }, index)).toThrow(
|
|
211
|
-
/not declared/,
|
|
212
|
-
)
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
test('invalid personality format → throws', () => {
|
|
216
|
-
expect(() => resolveCallerIdentity({ personality: 'Bad Name', runtime: 'claude' }, index)).toThrow()
|
|
217
|
-
})
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
-
// ensurePeerProfile — create-peer path installs the always-on plist for INFRA
|
|
222
|
-
// runtimes (and ONLY those). Always under IAPEER_LAUNCHAGENTS_DIR so the suite
|
|
223
|
-
// never writes into the real ~/Library/LaunchAgents.
|
|
224
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
|
-
|
|
226
|
-
describe('ensurePeerProfile create-peer → always-on plist (infra only)', () => {
|
|
227
|
-
let root: string
|
|
228
|
-
beforeEach(() => {
|
|
229
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-provision-'))
|
|
230
|
-
})
|
|
231
|
-
afterEach(() => {
|
|
232
|
-
rmSync(root, { recursive: true, force: true })
|
|
233
|
-
})
|
|
234
|
-
const laEnv = () =>
|
|
235
|
-
// IAPEER_ROOT isolates the now-GLOBAL infra log dir (~/.iapeer/logs/<p>, Фаза §8)
|
|
236
|
-
// under the sandbox — without it installAlwaysOnPlist's log mkdir resolves to the
|
|
237
|
-
// real home.
|
|
238
|
-
({ IAPEER_LAUNCHAGENTS_DIR: join(root, 'LaunchAgents'), IAPEER_ROOT: join(root, 'iapeer') }) as NodeJS.ProcessEnv
|
|
239
|
-
|
|
240
|
-
test('provisioning a NEW infra (notifier) peer installs a foundation-owned plist', () => {
|
|
241
|
-
const env = laEnv()
|
|
242
|
-
const peerCwd = join(root, 'timer') // basename → personality "timer"
|
|
243
|
-
const profile = ensurePeerProfile({ cwd: peerCwd, env, runtime: 'notifier' })
|
|
244
|
-
expect(profile.personality).toBe('timer')
|
|
245
|
-
expect(profile.intelligence).toBe('absent') // notifier zone default
|
|
246
|
-
|
|
247
|
-
const plist = launchdPlistPath('timer', env)
|
|
248
|
-
expect(existsSync(plist)).toBe(true)
|
|
249
|
-
expect(isFoundationOwnedPlist(plist)).toBe(true)
|
|
250
|
-
// and the profile was written (full provision)
|
|
251
|
-
expect(existsSync(peerProfilePath(peerCwd))).toBe(true)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
test('provisioning a NEW warm-on-demand (claude) peer installs NO plist (unchanged)', () => {
|
|
255
|
-
const env = laEnv()
|
|
256
|
-
const peerCwd = join(root, 'worker') // personality "worker"
|
|
257
|
-
ensurePeerProfile({ cwd: peerCwd, env, runtime: 'claude' })
|
|
258
|
-
expect(existsSync(launchdPlistPath('worker', env))).toBe(false)
|
|
259
|
-
expect(existsSync(peerProfilePath(peerCwd))).toBe(true) // profile still created
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
test('infra provision whose Label collides with a FOREIGN plist is refused — no profile written', () => {
|
|
263
|
-
const env = laEnv()
|
|
264
|
-
const laDir = join(root, 'LaunchAgents')
|
|
265
|
-
mkdirSync(laDir, { recursive: true })
|
|
266
|
-
// a live persistent-peer "boris" already owns com.iapeer.boris.plist (no sentinel)
|
|
267
|
-
const foreign = '<?xml version="1.0"?>\n<plist><dict><key>Label</key><string>com.iapeer.boris</string></dict></plist>\n'
|
|
268
|
-
const plist = launchdPlistPath('boris', env)
|
|
269
|
-
writeFileSync(plist, foreign)
|
|
270
|
-
|
|
271
|
-
const peerCwd = join(root, 'boris') // would derive personality "boris" → collision
|
|
272
|
-
expect(() => ensurePeerProfile({ cwd: peerCwd, env, runtime: 'notifier' })).toThrow(
|
|
273
|
-
/foundation-managed|refus/i,
|
|
274
|
-
)
|
|
275
|
-
// the live PP plist is untouched, and the half-provision left NO peer-profile.json
|
|
276
|
-
expect(readFileSync(plist, 'utf8')).toBe(foreign)
|
|
277
|
-
expect(existsSync(peerProfilePath(peerCwd))).toBe(false)
|
|
278
|
-
})
|
|
279
|
-
})
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// Profile-standard validator + index↔local self-heal (reconcile / reindex).
|
|
2
|
-
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
4
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
5
|
-
import { tmpdir } from 'os'
|
|
6
|
-
import { join } from 'path'
|
|
7
|
-
import { isConformant, migrateProfileRuntimeField, reconcileIndex, reindexFromLocals, validateProfileStandard } from './profileStandard.ts'
|
|
8
|
-
import { readFileSync } from 'fs'
|
|
9
|
-
import { readPeersIndex } from '../registry/index.ts'
|
|
10
|
-
|
|
11
|
-
describe('validateProfileStandard', () => {
|
|
12
|
-
const cwd = '/Users/x/Peers/boris' // basename normalizes to "boris"
|
|
13
|
-
|
|
14
|
-
test('a conformant profile (contract field default_runtime) has no issues', () => {
|
|
15
|
-
const issues = validateProfileStandard(
|
|
16
|
-
{ personality: 'boris', default_runtime: 'claude', runtimes: ['claude'], description: 'PM', intelligence: 'artificial' },
|
|
17
|
-
cwd,
|
|
18
|
-
)
|
|
19
|
-
expect(issues).toEqual([])
|
|
20
|
-
expect(isConformant(issues)).toBe(true)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('legacy `runtime` field → WARN (not error), still conformant', () => {
|
|
24
|
-
const issues = validateProfileStandard(
|
|
25
|
-
{ personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial' },
|
|
26
|
-
cwd,
|
|
27
|
-
)
|
|
28
|
-
expect(isConformant(issues)).toBe(true)
|
|
29
|
-
expect(issues.find(i => i.field === 'default_runtime')?.severity).toBe('warn')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('default_runtime + in-sync legacy mirror (Phase-2 write shape) → NO issues', () => {
|
|
33
|
-
const issues = validateProfileStandard(
|
|
34
|
-
{ personality: 'boris', default_runtime: 'claude', runtime: 'claude', runtimes: ['claude'], description: 'PM', intelligence: 'artificial' },
|
|
35
|
-
cwd,
|
|
36
|
-
)
|
|
37
|
-
expect(issues).toEqual([])
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('DIVERGED legacy mirror → WARN on default_runtime, still conformant', () => {
|
|
41
|
-
const issues = validateProfileStandard(
|
|
42
|
-
{ personality: 'boris', default_runtime: 'codex', runtime: 'claude', runtimes: ['codex', 'claude'], description: '', intelligence: 'artificial' },
|
|
43
|
-
cwd,
|
|
44
|
-
)
|
|
45
|
-
expect(isConformant(issues)).toBe(true)
|
|
46
|
-
const warn = issues.find(i => i.field === 'default_runtime')
|
|
47
|
-
expect(warn?.severity).toBe('warn')
|
|
48
|
-
expect(warn?.message).toContain('diverged')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('aliases misfiled in interfaces.telegram → WARN (owner migration to expansion.aliases pending)', () => {
|
|
52
|
-
const issues = validateProfileStandard(
|
|
53
|
-
{
|
|
54
|
-
personality: 'boris', default_runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial',
|
|
55
|
-
interfaces: { telegram: { bot: 'k', bot_username: 'x_bot', aliases: { '/alias-new': 'txt' } } },
|
|
56
|
-
},
|
|
57
|
-
cwd,
|
|
58
|
-
)
|
|
59
|
-
expect(isConformant(issues)).toBe(true)
|
|
60
|
-
expect(issues.some(i => i.field === 'interfaces.telegram.aliases' && i.severity === 'warn')).toBe(true)
|
|
61
|
-
// passport-only telegram section → silent
|
|
62
|
-
const clean = validateProfileStandard(
|
|
63
|
-
{
|
|
64
|
-
personality: 'boris', default_runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial',
|
|
65
|
-
interfaces: { telegram: { bot: 'k', bot_username: 'x_bot' } },
|
|
66
|
-
expansion: { aliases: { '/alias-new': 'txt' } },
|
|
67
|
-
},
|
|
68
|
-
cwd,
|
|
69
|
-
)
|
|
70
|
-
expect(clean).toEqual([])
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('top-level aliases → WARN (owner migration pending), still conformant', () => {
|
|
74
|
-
const issues = validateProfileStandard(
|
|
75
|
-
{ personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial', aliases: { '/new': 'x' } },
|
|
76
|
-
cwd,
|
|
77
|
-
)
|
|
78
|
-
expect(isConformant(issues)).toBe(true)
|
|
79
|
-
expect(issues.some(i => i.field === 'aliases' && i.severity === 'warn')).toBe(true)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('personality mirror disagreeing with cwd → WARN (self-heal), not error', () => {
|
|
83
|
-
const issues = validateProfileStandard(
|
|
84
|
-
{ personality: 'WRONG', runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial' },
|
|
85
|
-
cwd,
|
|
86
|
-
)
|
|
87
|
-
expect(isConformant(issues)).toBe(true)
|
|
88
|
-
expect(issues.some(i => i.field === 'personality' && i.severity === 'warn')).toBe(true)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('ERRORS: bad runtime, runtimes missing the default, bad intelligence', () => {
|
|
92
|
-
// runtime ids carry NO hyphen (/^[a-z][a-z0-9]{0,31}$/) → a hyphenated value is an error
|
|
93
|
-
expect(isConformant(validateProfileStandard({ default_runtime: 'no-dash', runtimes: ['claude'], intelligence: 'artificial' }, cwd))).toBe(false)
|
|
94
|
-
const e1 = validateProfileStandard({ default_runtime: 'claude', runtimes: ['codex'], intelligence: 'artificial' }, cwd)
|
|
95
|
-
expect(isConformant(e1)).toBe(false) // runtimes does not include the default
|
|
96
|
-
const e2 = validateProfileStandard({ default_runtime: 'claude', runtimes: ['claude'], intelligence: 'bogus' }, cwd)
|
|
97
|
-
expect(isConformant(e2)).toBe(false)
|
|
98
|
-
const e3 = validateProfileStandard({ runtimes: ['claude'], intelligence: 'artificial' }, cwd)
|
|
99
|
-
expect(isConformant(e3)).toBe(false) // no runtime/default_runtime at all
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('intelligence accepts the live vocab + legacy on read', () => {
|
|
103
|
-
for (const v of ['artificial', 'natural', 'absent', 'human', 'scripted']) {
|
|
104
|
-
const issues = validateProfileStandard({ default_runtime: 'claude', runtimes: ['claude'], intelligence: v }, cwd)
|
|
105
|
-
expect(issues.some(i => i.field === 'intelligence')).toBe(false)
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('reconcileIndex / reindexFromLocals (self-heal)', () => {
|
|
111
|
-
let root: string
|
|
112
|
-
const prevRoot = process.env.IAPEER_ROOT
|
|
113
|
-
let borisCwd: string
|
|
114
|
-
|
|
115
|
-
beforeEach(() => {
|
|
116
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-ps-'))
|
|
117
|
-
process.env.IAPEER_ROOT = root
|
|
118
|
-
// a local profile (source of truth) with a FRESH description + narrowed runtimes
|
|
119
|
-
borisCwd = join(root, 'peers', 'boris')
|
|
120
|
-
mkdirSync(join(borisCwd, '.iapeer'), { recursive: true })
|
|
121
|
-
writeFileSync(
|
|
122
|
-
join(borisCwd, '.iapeer', 'peer-profile.json'),
|
|
123
|
-
JSON.stringify({ personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: 'FRESH local desc', intelligence: 'artificial' }),
|
|
124
|
-
)
|
|
125
|
-
// a STALE index: old description + an extra runtime the local no longer declares
|
|
126
|
-
writeFileSync(
|
|
127
|
-
join(root, 'peers-profiles.json'),
|
|
128
|
-
JSON.stringify({
|
|
129
|
-
version: 2,
|
|
130
|
-
peers: [{ personality: 'boris', runtime: 'claude', runtimes: ['claude', 'codex'], description: 'STALE index desc', intelligence: 'artificial', cwd: borisCwd }],
|
|
131
|
-
}),
|
|
132
|
-
)
|
|
133
|
-
})
|
|
134
|
-
afterEach(() => {
|
|
135
|
-
if (prevRoot === undefined) delete process.env.IAPEER_ROOT
|
|
136
|
-
else process.env.IAPEER_ROOT = prevRoot
|
|
137
|
-
rmSync(root, { recursive: true, force: true })
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
test('reconcile DETECTS the drift (description + runtimes) read-only', () => {
|
|
141
|
-
const r = reconcileIndex({ env: process.env })
|
|
142
|
-
const boris = r.find(e => e.personality === 'boris')
|
|
143
|
-
expect(boris?.drift).toContain('description')
|
|
144
|
-
expect(boris?.drift).toContain('runtimes')
|
|
145
|
-
// read-only: the index file is untouched
|
|
146
|
-
expect(readPeersIndex({ env: process.env }).peers[0].description).toBe('STALE index desc')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
test('reindex SELF-HEALS the index from the local profile (REPLACE, not union)', async () => {
|
|
150
|
-
const { healed } = await reindexFromLocals({ env: process.env })
|
|
151
|
-
expect(healed.some(h => h.startsWith('boris:'))).toBe(true)
|
|
152
|
-
const rec = readPeersIndex({ env: process.env }).peers[0]
|
|
153
|
-
expect(rec.description).toBe('FRESH local desc')
|
|
154
|
-
expect(rec.runtimes).toEqual(['claude']) // codex DROPPED (reindex replaces, unlike upsert union)
|
|
155
|
-
// after the heal, reconcile is clean
|
|
156
|
-
expect(reconcileIndex({ env: process.env }).find(e => e.personality === 'boris')?.drift).toEqual([])
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
test('reindex writes the Phase-2 disk shape: default_runtime + in-sync legacy mirror', async () => {
|
|
160
|
-
await reindexFromLocals({ env: process.env })
|
|
161
|
-
const disk = JSON.parse(readFileSync(join(root, 'peers-profiles.json'), 'utf8'))
|
|
162
|
-
expect(disk.peers[0].default_runtime).toBe('claude')
|
|
163
|
-
expect(disk.peers[0].runtime).toBe('claude') // legacy mirror until Phase 3
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('migrateProfileRuntimeField: legacy-only profile → rewritten with default_runtime + mirror, owner fields preserved, idempotent', () => {
|
|
167
|
-
const profilePath = join(borisCwd, '.iapeer', 'peer-profile.json')
|
|
168
|
-
// enrich the legacy profile with owner-section + lifecycle fields that MUST survive
|
|
169
|
-
writeFileSync(
|
|
170
|
-
profilePath,
|
|
171
|
-
JSON.stringify({
|
|
172
|
-
personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: 'd', intelligence: 'human',
|
|
173
|
-
initial_prompt: 'boot', wake_policy: 'ephemeral',
|
|
174
|
-
interfaces: { telegram: { bot: 'k' } },
|
|
175
|
-
expansion: { aliases: { '/alias-new': 'txt' } },
|
|
176
|
-
notifier: { triggers: [] },
|
|
177
|
-
}),
|
|
178
|
-
)
|
|
179
|
-
expect(migrateProfileRuntimeField(borisCwd)).toBe(true)
|
|
180
|
-
const after = JSON.parse(readFileSync(profilePath, 'utf8'))
|
|
181
|
-
expect(after.default_runtime).toBe('claude')
|
|
182
|
-
expect(after.runtime).toBe('claude') // in-sync legacy mirror
|
|
183
|
-
expect(after.intelligence).toBe('human') // legacy vocab preserved VERBATIM (no silent migration)
|
|
184
|
-
expect(after.initial_prompt).toBe('boot')
|
|
185
|
-
expect(after.wake_policy).toBe('ephemeral')
|
|
186
|
-
expect(after.interfaces).toEqual({ telegram: { bot: 'k' } })
|
|
187
|
-
expect(after.expansion).toEqual({ aliases: { '/alias-new': 'txt' } })
|
|
188
|
-
expect(after.notifier).toEqual({ triggers: [] })
|
|
189
|
-
// idempotent: already in shape → untouched
|
|
190
|
-
expect(migrateProfileRuntimeField(borisCwd)).toBe(false)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
test('migrateProfileRuntimeField: DIVERGED mirror heals to default_runtime (the contract side wins)', () => {
|
|
194
|
-
const profilePath = join(borisCwd, '.iapeer', 'peer-profile.json')
|
|
195
|
-
writeFileSync(
|
|
196
|
-
profilePath,
|
|
197
|
-
JSON.stringify({ personality: 'boris', default_runtime: 'codex', runtime: 'claude', runtimes: ['codex', 'claude'], description: '', intelligence: 'artificial' }),
|
|
198
|
-
)
|
|
199
|
-
expect(migrateProfileRuntimeField(borisCwd)).toBe(true)
|
|
200
|
-
const after = JSON.parse(readFileSync(profilePath, 'utf8'))
|
|
201
|
-
expect(after.default_runtime).toBe('codex')
|
|
202
|
-
expect(after.runtime).toBe('codex') // mirror re-synced to the governing field
|
|
203
|
-
})
|
|
204
|
-
})
|