@agfpd/iapeer 0.2.76 → 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.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/package.json +21 -2
  4. package/src/cli/index.ts +47 -48
  5. package/src/connect/index.ts +4 -4
  6. package/src/core/constants.ts +3 -3
  7. package/src/core/normalize.ts +2 -2
  8. package/src/create/index.ts +1 -1
  9. package/src/daemon/index.ts +5 -5
  10. package/src/daemon/main.ts +9 -9
  11. package/src/enable/index.ts +19 -18
  12. package/src/enable/provisionCommand.ts +10 -10
  13. package/src/identity/index.ts +2 -2
  14. package/src/identity/profileStandard.ts +8 -8
  15. package/src/init/index.ts +20 -13
  16. package/src/install/index.ts +2 -2
  17. package/src/install/signing.ts +3 -4
  18. package/src/launch/adapters/claude.ts +7 -7
  19. package/src/launch/adapters/codex.ts +5 -6
  20. package/src/launch/canary.ts +8 -8
  21. package/src/launch/cmdlog.ts +3 -3
  22. package/src/launch/composeSystemPrompt.ts +2 -1
  23. package/src/launch/index.ts +5 -6
  24. package/src/launch/invocation.ts +1 -1
  25. package/src/launch/launchd.ts +14 -15
  26. package/src/launch/launchdRun.ts +6 -6
  27. package/src/launch/nativeMemory.ts +9 -9
  28. package/src/launch/ptyHost.ts +1 -1
  29. package/src/launch/readyGateModel.ts +3 -3
  30. package/src/launch/types.ts +7 -7
  31. package/src/lifecycle/eventlog.ts +2 -2
  32. package/src/lifecycle/index.ts +21 -21
  33. package/src/lifecycle/queue.ts +1 -1
  34. package/src/onboard/index.ts +19 -13
  35. package/src/onboard/memory.ts +5 -5
  36. package/src/onboard/steps.ts +8 -9
  37. package/src/provision/index.ts +2 -3
  38. package/src/runtime/deploy.ts +2 -2
  39. package/src/runtime/index.ts +4 -4
  40. package/src/shadow/gate.ts +6 -6
  41. package/src/shadow/index.ts +2 -2
  42. package/src/status/index.ts +11 -11
  43. package/src/storage/index.ts +1 -1
  44. package/src/supervisor/daemon.ts +14 -8
  45. package/src/transport/index.ts +11 -11
  46. package/src/update/index.ts +4 -4
  47. package/src/cli/cli.test.ts +0 -603
  48. package/src/cli/listTui.test.ts +0 -71
  49. package/src/codec/codec.test.ts +0 -271
  50. package/src/connect/connect.test.ts +0 -163
  51. package/src/core/constants.test.ts +0 -32
  52. package/src/core/normalize.test.ts +0 -98
  53. package/src/create/create.test.ts +0 -143
  54. package/src/daemon/daemon-http.test.ts +0 -114
  55. package/src/daemon/daemon.test.ts +0 -267
  56. package/src/daemon/main.test.ts +0 -413
  57. package/src/enable/enable.test.ts +0 -95
  58. package/src/enable/provisionCommand.test.ts +0 -107
  59. package/src/identity/identity.test.ts +0 -279
  60. package/src/identity/profileStandard.test.ts +0 -204
  61. package/src/init/init.test.ts +0 -213
  62. package/src/init/runtime-resolve.test.ts +0 -49
  63. package/src/install/install.test.ts +0 -66
  64. package/src/install/signing.test.ts +0 -84
  65. package/src/launch/bootdeliver.test.ts +0 -108
  66. package/src/launch/bootstrap.test.ts +0 -56
  67. package/src/launch/canary.test.ts +0 -346
  68. package/src/launch/cmdlog.test.ts +0 -94
  69. package/src/launch/codexHooksTrust.test.ts +0 -248
  70. package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
  71. package/src/launch/composeSystemPrompt.test.ts +0 -145
  72. package/src/launch/hostComposerOccupied.test.ts +0 -63
  73. package/src/launch/invocation.test.ts +0 -174
  74. package/src/launch/launch.test.ts +0 -359
  75. package/src/launch/launchd.test.ts +0 -499
  76. package/src/launch/nativeMemory.test.ts +0 -193
  77. package/src/launch/ptyHost.test.ts +0 -38
  78. package/src/launch/readyGateModel.test.ts +0 -72
  79. package/src/launch/sockdir.test.ts +0 -74
  80. package/src/lifecycle/eventlog.test.ts +0 -114
  81. package/src/lifecycle/lifecycle.test.ts +0 -1056
  82. package/src/lifecycle/queue.test.ts +0 -185
  83. package/src/onboard/memory.test.ts +0 -157
  84. package/src/onboard/onboard.test.ts +0 -39
  85. package/src/onboard/steps.test.ts +0 -201
  86. package/src/provision/provision.test.ts +0 -288
  87. package/src/registry/registry.test.ts +0 -432
  88. package/src/runtime/runtime.test.ts +0 -226
  89. package/src/runtime/update.test.ts +0 -192
  90. package/src/shadow/enumerate-skip.test.ts +0 -67
  91. package/src/shadow/gate.test.ts +0 -286
  92. package/src/shadow/install.test.ts +0 -153
  93. package/src/shadow/shadow.test.ts +0 -42
  94. package/src/status/status.test.ts +0 -208
  95. package/src/storage/peers-home.test.ts +0 -34
  96. package/src/storage/rotatelog.test.ts +0 -44
  97. package/src/storage/storage.test.ts +0 -65
  98. package/src/supervisor/attached-marker.test.ts +0 -84
  99. package/src/supervisor/boot.test.ts +0 -66
  100. package/src/supervisor/client.attach.test.ts +0 -107
  101. package/src/supervisor/daemon.test.ts +0 -213
  102. package/src/supervisor/deliver.test.ts +0 -58
  103. package/src/supervisor/panelog.test.ts +0 -95
  104. package/src/supervisor/protocol.test.ts +0 -222
  105. package/src/supervisor/selfargv.test.ts +0 -22
  106. package/src/supervisor/spinner-strip.test.ts +0 -52
  107. package/src/transport/compactDone.test.ts +0 -144
  108. package/src/transport/composerQueue.test.ts +0 -204
  109. package/src/transport/hostControl.test.ts +0 -87
  110. package/src/transport/hostDeliver.test.ts +0 -137
  111. package/src/transport/hostLive.test.ts +0 -50
  112. package/src/transport/routesend-wake.test.ts +0 -357
  113. 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
- })