@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.
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 +95 -78
  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 +10 -10
  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 +20 -13
  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 -215
  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,185 +0,0 @@
1
- // Ephemeral serial queue (M3) — FIFO primitives + the drain consumer.
2
- // Retry semantics (boris acceptance (b)) are pinned here: a FAILED wake leaves
3
- // the item at the head and the NEXT drain call retries the SAME task; only a
4
- // READY wake consumes it. Strict FIFO order is asserted across drains.
5
-
6
- import { afterEach, describe, expect, test } from 'bun:test'
7
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import { spawnSync } from 'child_process'
11
- import {
12
- drainAllEphemeralQueues,
13
- drainEphemeralQueue,
14
- enqueueEphemeralTask,
15
- ephemeralQueueDepth,
16
- ephemeralQueueDir,
17
- listQueuedIdentities,
18
- peekEphemeralTask,
19
- removeEphemeralTask,
20
- type LifecycleConfig,
21
- type WakeArgs,
22
- type WakeResult,
23
- } from './index.ts'
24
-
25
- const dirs: string[] = []
26
- function mkTmp(): string {
27
- const d = mkdtempSync(join(tmpdir(), 'iapeer-equeue-'))
28
- dirs.push(d)
29
- return d
30
- }
31
- afterEach(() => {
32
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
33
- })
34
-
35
- function mkCfg(): LifecycleConfig {
36
- const root = mkTmp()
37
- return {
38
- stateDir: join(root, 'state'),
39
- sockDir: join(root, 'socks'),
40
- eventLogDir: join(root, 'logs'),
41
- } as LifecycleConfig
42
- }
43
-
44
- describe('ephemeral queue primitives (FIFO)', () => {
45
- test('enqueue returns depth; peek is non-destructive; remove consumes; strict FIFO', () => {
46
- const cfg = mkCfg()
47
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(0)
48
- expect(peekEphemeralTask(cfg, 'claude-w')).toBeNull()
49
-
50
- expect(enqueueEphemeralTask(cfg, 'claude-w', { task: 'first', topic: 't1' })).toBe(1)
51
- expect(enqueueEphemeralTask(cfg, 'claude-w', { task: 'second' })).toBe(2)
52
- expect(enqueueEphemeralTask(cfg, 'claude-w', { task: 'third', topic: 't3' })).toBe(3)
53
-
54
- const head = peekEphemeralTask(cfg, 'claude-w')
55
- expect(head?.task).toBe('first')
56
- expect(head?.topic).toBe('t1')
57
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(3) // peek did not consume
58
-
59
- removeEphemeralTask(cfg, 'claude-w', head!.seq)
60
- removeEphemeralTask(cfg, 'claude-w', head!.seq) // idempotent
61
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(2)
62
- expect(peekEphemeralTask(cfg, 'claude-w')?.task).toBe('second')
63
- expect(peekEphemeralTask(cfg, 'claude-w')?.topic).toBeUndefined()
64
- })
65
-
66
- test('enqueue skips taken seq names (exclusive-create) — no overwrite of a pending task', () => {
67
- const cfg = mkCfg()
68
- enqueueEphemeralTask(cfg, 'claude-w', { task: 'one' })
69
- // simulate a competitor that already claimed the next seq
70
- writeFileSync(join(ephemeralQueueDir(cfg, 'claude-w'), '000002'), JSON.stringify({ task: 'competitor' }))
71
- expect(enqueueEphemeralTask(cfg, 'claude-w', { task: 'three' })).toBe(3)
72
- // all three distinct tasks live side-by-side
73
- const dir = ephemeralQueueDir(cfg, 'claude-w')
74
- expect(JSON.parse(readFileSync(join(dir, '000002'), 'utf8')).task).toBe('competitor')
75
- expect(JSON.parse(readFileSync(join(dir, '000003'), 'utf8')).task).toBe('three')
76
- })
77
-
78
- test('a poison head (corrupt JSON) is dropped, not wedging the queue', () => {
79
- const cfg = mkCfg()
80
- mkdirSync(ephemeralQueueDir(cfg, 'claude-w'), { recursive: true })
81
- writeFileSync(join(ephemeralQueueDir(cfg, 'claude-w'), '000001'), 'NOT JSON {{{')
82
- enqueueEphemeralTask(cfg, 'claude-w', { task: 'good' })
83
- const head = peekEphemeralTask(cfg, 'claude-w')
84
- expect(head?.task).toBe('good')
85
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(1) // poison slot dropped
86
- })
87
-
88
- test('listQueuedIdentities: only non-empty queues, sorted', () => {
89
- const cfg = mkCfg()
90
- enqueueEphemeralTask(cfg, 'claude-b', { task: 'x' })
91
- enqueueEphemeralTask(cfg, 'claude-a', { task: 'y' })
92
- mkdirSync(ephemeralQueueDir(cfg, 'claude-empty'), { recursive: true }) // empty dir → excluded
93
- expect(listQueuedIdentities(cfg)).toEqual(['claude-a', 'claude-b'])
94
- })
95
- })
96
-
97
- describe('drainEphemeralQueue (peek → wake → rm-on-READY)', () => {
98
- function fakeWake(
99
- script: Array<'READY' | 'FAILED'>,
100
- calls: WakeArgs[],
101
- ): (args: WakeArgs) => Promise<WakeResult> {
102
- return async args => {
103
- calls.push(args)
104
- const status = script[Math.min(calls.length - 1, script.length - 1)]!
105
- return { status, woke: status === 'READY', runtime: 'claude' }
106
- }
107
- }
108
-
109
- test('empty queue → null, wake NOT called', async () => {
110
- const cfg = mkCfg()
111
- const calls: WakeArgs[] = []
112
- expect(await drainEphemeralQueue(cfg, 'w', 'claude', { wakeFn: fakeWake(['READY'], calls) })).toBeNull()
113
- expect(calls).toEqual([])
114
- })
115
-
116
- test('READY consumes the head; successive drains feed tasks in STRICT FIFO order', async () => {
117
- const cfg = mkCfg()
118
- enqueueEphemeralTask(cfg, 'claude-w', { task: 'task-A', topic: 'ta' })
119
- enqueueEphemeralTask(cfg, 'claude-w', { task: 'task-B' })
120
- const calls: WakeArgs[] = []
121
- const deps = { wakeFn: fakeWake(['READY'], calls) }
122
-
123
- expect((await drainEphemeralQueue(cfg, 'w', 'claude', deps))?.status).toBe('READY')
124
- expect(calls[0]).toMatchObject({ personality: 'w', runtime: 'claude', task: 'task-A', topic: 'ta' })
125
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(1) // A consumed
126
-
127
- expect((await drainEphemeralQueue(cfg, 'w', 'claude', deps))?.status).toBe('READY')
128
- expect(calls[1]).toMatchObject({ task: 'task-B' }) // FIFO: B strictly after A
129
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(0)
130
- expect(await drainEphemeralQueue(cfg, 'w', 'claude', deps)).toBeNull() // drained dry
131
-
132
- // durable drain trace (acceptance (a)): ev=ephemeral-drain with depth
133
- const logged = readFileSync(join(cfg.eventLogDir, 'lifecycle.log'), 'utf8')
134
- expect(logged).toContain('ev=ephemeral-drain')
135
- expect(logged).toContain('identity=claude-w')
136
- expect(logged).toContain('depth=2')
137
- })
138
-
139
- test('FAILED wake LEAVES the item at the head — the next drain RETRIES the same task (acceptance (b))', async () => {
140
- const cfg = mkCfg()
141
- enqueueEphemeralTask(cfg, 'claude-w', { task: 'flaky-task' })
142
- const calls: WakeArgs[] = []
143
- const deps = { wakeFn: fakeWake(['FAILED', 'READY'], calls) }
144
-
145
- expect((await drainEphemeralQueue(cfg, 'w', 'claude', deps))?.status).toBe('FAILED')
146
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(1) // NOT consumed on failure
147
-
148
- expect((await drainEphemeralQueue(cfg, 'w', 'claude', deps))?.status).toBe('READY')
149
- expect(calls.length).toBe(2)
150
- expect(calls[1]?.task).toBe('flaky-task') // the SAME task, retried
151
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(0)
152
- })
153
-
154
- const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
155
- test.if(tmuxAvailable)('a LIVE session blocks the drain (one task per session invariant)', async () => {
156
- const cfg = mkCfg()
157
- mkdirSync(cfg.sockDir, { recursive: true })
158
- const sock = join(cfg.sockDir, 'tmux-iap-claude-w.sock')
159
- enqueueEphemeralTask(cfg, 'claude-w', { task: 'queued-while-busy' })
160
- const calls: WakeArgs[] = []
161
- try {
162
- spawnSync('tmux', ['-S', sock, 'new-session', '-d', '-s', 'claude-w', 'sleep', '60'])
163
- expect(await drainEphemeralQueue(cfg, 'w', 'claude', { wakeFn: fakeWake(['READY'], calls) })).toBeNull()
164
- expect(calls).toEqual([]) // no wake while the session lives
165
- expect(ephemeralQueueDepth(cfg, 'claude-w')).toBe(1) // task waits for the reap
166
- } finally {
167
- spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
168
- }
169
- })
170
-
171
- test('drainAllEphemeralQueues: scans every queued identity, H4-skips launchd-managed', async () => {
172
- const cfg = mkCfg()
173
- const laDir = mkTmp()
174
- const env = { ...process.env, IAPEER_LAUNCHAGENTS_DIR: laDir } as NodeJS.ProcessEnv
175
- enqueueEphemeralTask(cfg, 'claude-free', { task: 'x' })
176
- enqueueEphemeralTask(cfg, 'claude-held', { task: 'y' })
177
- writeFileSync(join(laDir, 'com.iapeer.held.plist'), '') // 'held' is launchd-managed
178
- const calls: WakeArgs[] = []
179
- const results = await drainAllEphemeralQueues(cfg, { env, wakeFn: fakeWake(['READY'], calls) })
180
- expect(results.length).toBe(1)
181
- expect(calls.map(c => c.personality)).toEqual(['free']) // held NEVER woken (H4)
182
- expect(ephemeralQueueDepth(cfg, 'claude-free')).toBe(0)
183
- expect(ephemeralQueueDepth(cfg, 'claude-held')).toBe(1) // untouched
184
- })
185
- })
@@ -1,157 +0,0 @@
1
- // onboardMemoryProvider — the host-phase memory-slot step (контракт «Слот
2
- // памяти»). Default-YES, report-only for the exit code; the PROVIDER writes the
3
- // slot declaration (the step verifies it did). All writes under a temp
4
- // IAPEER_ROOT — never the live host.
5
-
6
- import { afterEach, describe, expect, test } from 'bun:test'
7
- import { mkdtempSync, rmSync, writeFileSync } from 'fs'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import { coreKnownInitArgs, onboardMemoryProvider, DEFAULT_MEMORY_PACKAGE } from './memory.ts'
11
- import { memoryProviderPath } from '../status/index.ts'
12
-
13
- const dirs: string[] = []
14
- function mkTmp(): string {
15
- const d = mkdtempSync(join(tmpdir(), 'iapeer-memstep-'))
16
- dirs.push(d)
17
- return d
18
- }
19
- afterEach(() => {
20
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
21
- })
22
-
23
- function envFor(root: string): NodeJS.ProcessEnv {
24
- return { IAPEER_ROOT: root } as NodeJS.ProcessEnv
25
- }
26
-
27
- const SLOT = {
28
- provider: 'iapeer-memory',
29
- package: DEFAULT_MEMORY_PACKAGE,
30
- version: '0.1.0',
31
- registeredAt: '2026-06-10T00:00:00Z',
32
- }
33
-
34
- function registry(root: string, naturals: string[]): void {
35
- writeFileSync(
36
- join(root, 'peers-profiles.json'),
37
- JSON.stringify({
38
- version: 2,
39
- peers: [
40
- ...naturals.map(p => ({ personality: p, runtime: 'telegram', runtimes: ['telegram'], description: '', intelligence: 'natural', cwd: `/tmp/${p}` })),
41
- { personality: 'bot', runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial', cwd: '/tmp/bot' },
42
- ],
43
- }),
44
- )
45
- }
46
-
47
- describe('coreKnownInitArgs (the EXHAUSTIVE v1 passthrough list)', () => {
48
- test('exactly one natural peer → --human <personality>', () => {
49
- const root = mkTmp()
50
- registry(root, ['arthur'])
51
- expect(coreKnownInitArgs(envFor(root))).toEqual(['--human', 'arthur'])
52
- })
53
- test('zero or many naturals → pass NOTHING (the provider asks itself)', () => {
54
- const none = mkTmp()
55
- registry(none, [])
56
- expect(coreKnownInitArgs(envFor(none))).toEqual([])
57
- const many = mkTmp()
58
- registry(many, ['arthur', 'maria'])
59
- expect(coreKnownInitArgs(envFor(many))).toEqual([])
60
- })
61
- test('legacy intelligence "human" normalizes to natural (read-compat)', () => {
62
- const root = mkTmp()
63
- writeFileSync(
64
- join(root, 'peers-profiles.json'),
65
- JSON.stringify({ version: 2, peers: [{ personality: 'arthur', runtime: 'telegram', runtimes: ['telegram'], description: '', intelligence: 'human', cwd: '/tmp/a' }] }),
66
- )
67
- expect(coreKnownInitArgs(envFor(root))).toEqual(['--human', 'arthur'])
68
- })
69
- test('no registry at all → [] (never throws)', () => {
70
- expect(coreKnownInitArgs(envFor(mkTmp()))).toEqual([])
71
- })
72
- })
73
-
74
- describe('onboardMemoryProvider', () => {
75
- test('--no-memory → skipped-flag, init never invoked', async () => {
76
- const root = mkTmp()
77
- let invoked = 0
78
- const r = await onboardMemoryProvider({
79
- skip: true,
80
- env: envFor(root),
81
- runInit: () => ((invoked++), { status: 0, unavailable: false }),
82
- })
83
- expect(r.state).toBe('skipped-flag')
84
- expect(invoked).toBe(0)
85
- })
86
-
87
- test('same package already in the slot → already (idempotent no-op, no init call)', async () => {
88
- const root = mkTmp()
89
- writeFileSync(memoryProviderPath(envFor(root)), JSON.stringify(SLOT))
90
- let invoked = 0
91
- const r = await onboardMemoryProvider({
92
- env: envFor(root),
93
- runInit: () => ((invoked++), { status: 0, unavailable: false }),
94
- })
95
- expect(r.state).toBe('already')
96
- expect(r.provider?.provider).toBe('iapeer-memory')
97
- expect(invoked).toBe(0)
98
- })
99
-
100
- test('slot occupied by ANOTHER provider → refused-foreign (never silent overwrite)', async () => {
101
- const root = mkTmp()
102
- writeFileSync(memoryProviderPath(envFor(root)), JSON.stringify({ ...SLOT, provider: 'other-mem', package: '@x/other' }))
103
- const r = await onboardMemoryProvider({ env: envFor(root), runInit: () => ({ status: 0, unavailable: false }) })
104
- expect(r.state).toBe('refused-foreign')
105
- expect(r.detail).toMatch(/occupied .*other-mem.*uninstall/)
106
- })
107
-
108
- test('dry-run → reports the exact would-be command incl. --human passthrough', async () => {
109
- const root = mkTmp()
110
- registry(root, ['arthur'])
111
- const r = await onboardMemoryProvider({ dryRun: true, env: envFor(root) })
112
- expect(r.state).toBe('dry-run')
113
- expect(r.detail).toBe(`would run: npx -y ${DEFAULT_MEMORY_PACKAGE} init --human arthur`)
114
- })
115
-
116
- test('package unavailable → skipped-unavailable (SOFT skip — release order never blocks onboard)', async () => {
117
- const root = mkTmp()
118
- registry(root, [])
119
- const r = await onboardMemoryProvider({ env: envFor(root), runInit: () => ({ status: 1, unavailable: true }) })
120
- expect(r.state).toBe('skipped-unavailable')
121
- expect(r.detail).toMatch(/not available/)
122
- })
123
-
124
- test('init ok + provider declared the slot → installed (slot read back)', async () => {
125
- const root = mkTmp()
126
- registry(root, ['arthur'])
127
- const env = envFor(root)
128
- const calls: string[][] = []
129
- const r = await onboardMemoryProvider({
130
- env,
131
- runInit: (pkg, args) => {
132
- calls.push([pkg, ...args])
133
- writeFileSync(memoryProviderPath(env), JSON.stringify(SLOT)) // the PROVIDER writes the slot
134
- return { status: 0, unavailable: false }
135
- },
136
- })
137
- expect(r.state).toBe('installed')
138
- expect(r.provider?.version).toBe('0.1.0')
139
- expect(calls).toEqual([[DEFAULT_MEMORY_PACKAGE, 'init', '--human', 'arthur']])
140
- })
141
-
142
- test('init ok but slot NOT declared → provider-init-failed (its contract duty, surfaced)', async () => {
143
- const root = mkTmp()
144
- registry(root, [])
145
- const r = await onboardMemoryProvider({ env: envFor(root), runInit: () => ({ status: 0, unavailable: false }) })
146
- expect(r.state).toBe('provider-init-failed')
147
- expect(r.detail).toMatch(/did not declare/)
148
- })
149
-
150
- test('init non-zero (e.g. non-tty refusal) → provider-init-failed with the exit code', async () => {
151
- const root = mkTmp()
152
- registry(root, [])
153
- const r = await onboardMemoryProvider({ env: envFor(root), runInit: () => ({ status: 2, unavailable: false }) })
154
- expect(r.state).toBe('provider-init-failed')
155
- expect(r.detail).toMatch(/exited 2/)
156
- })
157
- })
@@ -1,39 +0,0 @@
1
- // onboard — the fleet-critical detector (isAgfpdInList): is OUR marketplace already
2
- // registered? A false negative would RE-register on an already-configured host (the
3
- // exact fleet-mutation onboard must avoid). Tested against REAL claude/codex
4
- // `plugin marketplace list` output shapes.
5
-
6
- import { describe, expect, test } from 'bun:test'
7
- import { isAgfpdInList } from './index.ts'
8
-
9
- describe('isAgfpdInList (marketplace-registered detector)', () => {
10
- test('claude list shape (❯ name + GitHub source ref) → true', () => {
11
- const claudeOut = `Configured marketplaces:
12
-
13
- ❯ claude-plugins-official
14
- Source: GitHub (anthropics/claude-plugins-official)
15
-
16
- ❯ agfpd
17
- Source: GitHub (agfpd/agfpd-marketplace)
18
- `
19
- expect(isAgfpdInList(claudeOut)).toBe(true)
20
- })
21
-
22
- test('codex list shape (name + root path columns) → true', () => {
23
- const codexOut = `MARKETPLACE ROOT
24
- agfpd /Users/x/.codex/.tmp/marketplaces/agfpd
25
- claude-plugins-official /Users/x/.codex/.tmp/marketplaces/claude-plugins-official
26
- `
27
- expect(isAgfpdInList(codexOut)).toBe(true)
28
- })
29
-
30
- test('NOT registered (only other marketplaces) → false', () => {
31
- expect(isAgfpdInList('Configured marketplaces:\n\n ❯ claude-plugins-official\n Source: GitHub (anthropics/claude-plugins-official)\n')).toBe(false)
32
- expect(isAgfpdInList('')).toBe(false)
33
- })
34
-
35
- test('a different agfpd-* token (e.g. a plugin name) does NOT false-positive', () => {
36
- // a line that mentions agfpd-prompt-architect but the agfpd marketplace is NOT listed
37
- expect(isAgfpdInList('plugins:\n agfpd-prompt-architect@somewhere (installed)\n')).toBe(false)
38
- })
39
- })
@@ -1,201 +0,0 @@
1
- // Backbone default-yes onboard steps (design «Onboard костяка» FINAL 10.06) —
2
- // notifier (zero questions, soft-skip on unavailability) and telegram (human peer
3
- // via TELEGRAM_USER_ID env → the package's self-config hook; idempotent vs an
4
- // existing natural peer). All sandboxed: IAPEER_ROOT / IAPEER_LAUNCHAGENTS_DIR
5
- // temp dirs, IAPEER_TEST_SANDBOX skips real launchctl, injected npx/ask.
6
-
7
- import { afterEach, describe, expect, test } from 'bun:test'
8
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import { findNaturalPeer, onboardNotifierStep, onboardTelegramStep } from './steps.ts'
12
- import { writeRuntimeManifest, type RuntimeManifest } from '../runtime/index.ts'
13
- import { findPeer, readPeersIndex, upsertPeer } from '../registry/index.ts'
14
-
15
- const roots: string[] = []
16
- function mkTmp(): string {
17
- const d = mkdtempSync(join(tmpdir(), 'iapeer-steps-'))
18
- roots.push(d)
19
- return d
20
- }
21
- afterEach(() => {
22
- while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
23
- })
24
-
25
- function envFor(root: string, path?: string): NodeJS.ProcessEnv {
26
- return {
27
- IAPEER_ROOT: join(root, 'iapeer'),
28
- IAPEER_LAUNCHAGENTS_DIR: join(root, 'LA'),
29
- IAPEER_TEST_SANDBOX: '1',
30
- HOME: root,
31
- ...(path ? { PATH: path } : {}),
32
- } as NodeJS.ProcessEnv
33
- }
34
-
35
- /** Stub launcher + recording self-config hook on a PATH dir. */
36
- function stubBins(bin: string): { dir: string; hook: string } {
37
- const dir = mkTmp()
38
- writeFileSync(join(dir, bin), '#!/bin/sh\nexec sleep 1\n', { mode: 0o755 })
39
- const hook = join(dir, 'sc.sh')
40
- // records personality + the TELEGRAM_USER_ID it received → proves env passthrough
41
- writeFileSync(
42
- hook,
43
- '#!/bin/sh\nprintf "%s|%s" "$IAPEER_PEER_PERSONALITY" "$TELEGRAM_USER_ID" > "$IAPEER_ROOT/sc-$IAPEER_PEER_PERSONALITY"\nexit 0\n',
44
- { mode: 0o755 },
45
- )
46
- return { dir, hook }
47
- }
48
-
49
- describe('onboardNotifierStep (а — default-yes, zero questions)', () => {
50
- test('npx self-deploy → declared set provisioned', async () => {
51
- const root = mkTmp()
52
- const { dir, hook } = stubBins('notifier-runtime')
53
- const env = envFor(root, dir)
54
- const runNpx = (_pkg: string, e: NodeJS.ProcessEnv) => {
55
- const m: RuntimeManifest = {
56
- runtime: 'notifier',
57
- selfConfig: hook,
58
- peers: [
59
- { personality: 'timer', intelligence: 'absent' },
60
- { personality: 'watcher', intelligence: 'absent' },
61
- ],
62
- }
63
- writeRuntimeManifest(m, { env: e })
64
- return { ok: true }
65
- }
66
- const r = await onboardNotifierStep({ env, runNpx })
67
- expect(r.state).toBe('deployed')
68
- expect(r.peers.map(p => p.personality).sort()).toEqual(['timer', 'watcher'])
69
- expect(findPeer(readPeersIndex({ env }), 'timer')).not.toBeNull()
70
- })
71
-
72
- test('npx failure (no bun / unpublished / no network) → SOFT skip, not a failure', async () => {
73
- const env = envFor(mkTmp())
74
- const r = await onboardNotifierStep({ env, runNpx: () => ({ ok: false, detail: 'bun: command not found' }) })
75
- expect(r.state).toBe('skipped-unavailable')
76
- expect(r.detail).toContain('bun: command not found')
77
- expect(r.detail).toContain('install later')
78
- })
79
-
80
- test('package installed but manifest missing → deploy-failed (REAL break, not unavailability)', async () => {
81
- const env = envFor(mkTmp())
82
- // npx "succeeds" but self-deploys nothing — the package broke its contract
83
- const r = await onboardNotifierStep({ env, runNpx: () => ({ ok: true }) })
84
- expect(r.state).toBe('deploy-failed')
85
- expect(r.detail).toContain('manifest')
86
- })
87
-
88
- test('--no-notifier → skipped-flag; dry-run reports intent without touching anything', async () => {
89
- const env = envFor(mkTmp())
90
- expect((await onboardNotifierStep({ skip: true, env })).state).toBe('skipped-flag')
91
- const dry = await onboardNotifierStep({ dryRun: true, env })
92
- expect(dry.state).toBe('dry-run')
93
- expect(dry.detail).toContain('@agfpd/notifier-runtime')
94
- })
95
-
96
- test('idempotent re-run: manifest present → install skipped, deploy no-clobber', async () => {
97
- const root = mkTmp()
98
- const { dir, hook } = stubBins('notifier-runtime')
99
- const env = envFor(root, dir)
100
- writeRuntimeManifest(
101
- { runtime: 'notifier', selfConfig: hook, peers: [{ personality: 'timer', intelligence: 'absent' }] },
102
- { env },
103
- )
104
- let npxCalled = false
105
- const first = await onboardNotifierStep({ env, runNpx: () => ((npxCalled = true), { ok: true }) })
106
- expect(first.state).toBe('deployed')
107
- expect(npxCalled).toBe(false) // manifest-gated — npx never re-ran
108
- const second = await onboardNotifierStep({ env, runNpx: () => ({ ok: true }) })
109
- expect(second.state).toBe('deployed') // no-clobber re-verify
110
- expect(readPeersIndex({ env }).peers.filter(p => p.personality === 'timer').length).toBe(1)
111
- })
112
- })
113
-
114
- describe('onboardTelegramStep (б — human peer, идемпотентность, non-tty)', () => {
115
- test('flags path: creates the human peer; TELEGRAM_USER_ID reaches the self-config hook', async () => {
116
- const root = mkTmp()
117
- const { dir, hook } = stubBins('telegram-runtime')
118
- const env = envFor(root, dir)
119
- writeRuntimeManifest({ runtime: 'telegram', selfConfig: hook }, { env }) // installed (mode b — no declared peers)
120
- const r = await onboardTelegramStep({ human: 'Arthur', userId: '123456789', env })
121
- expect(r.state).toBe('created')
122
- expect(r.personality).toBe('arthur') // normalized
123
- const peer = findPeer(readPeersIndex({ env }), 'arthur')!
124
- expect(peer.intelligence).toBe('natural')
125
- expect(peer.runtime).toBe('telegram')
126
- // the hook saw the user id via env (owner's contract: hook writes interfaces itself)
127
- expect(readFileSync(join(env.IAPEER_ROOT as string, 'sc-arthur'), 'utf8')).toBe('arthur|123456789')
128
- })
129
-
130
- test('IDEMPOTENT: an existing natural peer → already, NEVER a second human (boris check)', async () => {
131
- const env = envFor(mkTmp())
132
- await upsertPeer(
133
- { personality: 'arthur', runtime: 'telegram', cwd: '/tmp/arthur', intelligence: 'natural' },
134
- { env },
135
- )
136
- let asked = false
137
- const r = await onboardTelegramStep({
138
- env,
139
- ask: async () => ((asked = true), 'never'),
140
- isTty: true,
141
- })
142
- expect(r.state).toBe('already')
143
- expect(r.personality).toBe('arthur')
144
- expect(r.detail).toContain('уже есть')
145
- expect(asked).toBe(false) // no questions, no install — the WHOLE step no-ops
146
- })
147
-
148
- test('non-tty without flags → refusal of the STEP (with the flag recipe), not of onboard', async () => {
149
- const root = mkTmp()
150
- const env = envFor(root)
151
- writeRuntimeManifest({ runtime: 'telegram' }, { env })
152
- const r = await onboardTelegramStep({ env, isTty: false })
153
- expect(r.state).toBe('refused-non-tty')
154
- expect(r.detail).toContain('--telegram-human')
155
- })
156
-
157
- test('tty prompt path: answers flow in; empty answer → soft refusal', async () => {
158
- const root = mkTmp()
159
- const { dir, hook } = stubBins('telegram-runtime')
160
- const env = envFor(root, dir)
161
- writeRuntimeManifest({ runtime: 'telegram', selfConfig: hook }, { env })
162
- const answers = ['leo', '42']
163
- const r = await onboardTelegramStep({ env, isTty: true, ask: async () => answers.shift() ?? '' })
164
- expect(r.state).toBe('created')
165
- expect(r.personality).toBe('leo')
166
- // empty answer → not now
167
- const env2 = envFor(mkTmp())
168
- writeRuntimeManifest({ runtime: 'telegram' }, { env: env2 })
169
- const r2 = await onboardTelegramStep({ env: env2, isTty: true, ask: async () => '' })
170
- expect(r2.state).toBe('refused-non-tty')
171
- })
172
-
173
- test('invalid explicit flags → invalid-input (hard): bad name / non-digit user id', async () => {
174
- const root = mkTmp()
175
- const env = envFor(root)
176
- writeRuntimeManifest({ runtime: 'telegram' }, { env })
177
- expect((await onboardTelegramStep({ human: '!!!', userId: '42', env })).state).toBe('invalid-input')
178
- const r = await onboardTelegramStep({ human: 'leo', userId: 'abc', env })
179
- expect(r.state).toBe('invalid-input')
180
- expect(r.detail).toContain('@userinfobot')
181
- })
182
-
183
- test('package unavailable → soft skip (step), with the install-later recipe', async () => {
184
- const env = envFor(mkTmp())
185
- const r = await onboardTelegramStep({
186
- human: 'leo',
187
- userId: '42',
188
- env,
189
- runNpx: () => ({ ok: false, detail: 'npm ERR 404' }),
190
- })
191
- expect(r.state).toBe('skipped-unavailable')
192
- expect(r.detail).toContain('install-runtime telegram')
193
- })
194
-
195
- test('findNaturalPeer: null on empty/no registry, found when present', async () => {
196
- const env = envFor(mkTmp())
197
- expect(findNaturalPeer(env)).toBeNull()
198
- await upsertPeer({ personality: 'h', runtime: 'telegram', cwd: '/tmp/h', intelligence: 'natural' }, { env })
199
- expect(findNaturalPeer(env)).toBe('h')
200
- })
201
- })