@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.
- 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 +20 -13
- 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 -213
- 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,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
|
-
})
|