@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,153 +0,0 @@
1
- import { afterEach, describe, expect, test } from 'bun:test'
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
3
- import { spawnSync } from 'node:child_process'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { isFoundationOwnedPlist } from '../launch/launchd.ts'
7
- import {
8
- SHADOW_PERSONALITY,
9
- SHADOW_PLIST_LABEL,
10
- buildShadowPlistSpec,
11
- cycleShadowJob,
12
- installShadowJob,
13
- installShadowPlist,
14
- shadowPlistPath,
15
- uninstallShadowJob,
16
- } from './install.ts'
17
-
18
- // Hermetic: every plist write lands under IAPEER_LAUNCHAGENTS_DIR / IAPEER_ROOT overrides, and
19
- // the suite runs with IAPEER_TEST_SANDBOX=1 (process.env) so the launchctl-touching paths
20
- // (installShadowJob / cycleShadowJob / uninstallShadowJob) fail-CLOSED to skipped-sandbox — they
21
- // NEVER load/unload a real launchd job. No tmux, no @xterm (install.ts is pure plumbing).
22
-
23
- const tmpDirs: string[] = []
24
- function mkEnv(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
25
- const root = mkdtempSync(join(tmpdir(), 'iapeer-shadow-install-'))
26
- tmpDirs.push(root)
27
- return {
28
- IAPEER_LAUNCHAGENTS_DIR: join(root, 'LaunchAgents'),
29
- IAPEER_ROOT: join(root, 'iapeer'),
30
- HOME: root,
31
- PATH: '/usr/bin:/bin',
32
- ...extra,
33
- } as NodeJS.ProcessEnv
34
- }
35
- afterEach(() => {
36
- while (tmpDirs.length) rmSync(tmpDirs.pop()!, { recursive: true, force: true })
37
- })
38
- const plutilLint = (path: string): boolean => spawnSync('plutil', ['-lint', path], { encoding: 'utf8' }).status === 0
39
-
40
- describe('buildShadowPlistSpec', () => {
41
- test('label is com.iapeer.shadow-fidelity and runs the INSTALLED `iapeer shadow`', () => {
42
- const spec = buildShadowPlistSpec({ HOME: '/Users/x' } as NodeJS.ProcessEnv)
43
- expect(spec.label).toBe(SHADOW_PLIST_LABEL)
44
- expect(SHADOW_PLIST_LABEL).toBe('com.iapeer.shadow-fidelity')
45
- expect(spec.programArguments).toEqual(['/Users/x/.local/bin/iapeer', 'shadow'])
46
- })
47
- test('launchd-minimal PATH includes /opt/homebrew/bin (tmux lives there — the observer shells read-only tmux)', () => {
48
- expect(buildShadowPlistSpec({ HOME: '/Users/x' } as NodeJS.ProcessEnv).environment.PATH).toContain('/opt/homebrew/bin')
49
- })
50
- test('stdout/stderr land under ~/.iapeer/logs/iapeer', () => {
51
- const spec = buildShadowPlistSpec(mkEnv())
52
- expect(spec.stdoutPath).toContain(join('logs', 'iapeer', 'shadow-fidelity.launchd-stdout.log'))
53
- expect(spec.stderrPath).toContain(join('logs', 'iapeer', 'shadow-fidelity.launchd-stderr.log'))
54
- })
55
- })
56
-
57
- describe('installShadowPlist', () => {
58
- test('writes a valid, foundation-owned (sentinel) always-on plist under the override dir', () => {
59
- const env = mkEnv()
60
- const r = installShadowPlist(env)
61
- expect(r.path).toBe(shadowPlistPath(env))
62
- expect(r.changed).toBe(true)
63
- expect(existsSync(r.path)).toBe(true)
64
- expect(isFoundationOwnedPlist(r.path)).toBe(true) // carries the ownership sentinel
65
- expect(plutilLint(r.path)).toBe(true)
66
- const xml = readFileSync(r.path, 'utf8')
67
- expect(xml).toContain(`<string>${SHADOW_PLIST_LABEL}</string>`)
68
- expect(xml).toContain('<string>shadow</string>')
69
- expect(xml).toContain('<key>RunAtLoad</key>')
70
- expect(xml).toContain('<key>KeepAlive</key>')
71
- })
72
-
73
- test('idempotent by content: an unchanged re-install does NOT rewrite (changed:false, mtime stable); a changed PATH DOES', () => {
74
- const env = mkEnv()
75
- const first = installShadowPlist(env)
76
- expect(first.changed).toBe(true)
77
- const mtime1 = statSync(first.path).mtimeMs
78
- const second = installShadowPlist(env)
79
- expect(second.changed).toBe(false)
80
- expect(statSync(second.path).mtimeMs).toBe(mtime1) // file untouched (no BTM spam)
81
- // a different rendered plist (PATH override) rewrites
82
- const third = installShadowPlist(mkEnv({ IAPEER_SHADOW_PATH: '/opt/homebrew/bin:/usr/bin:/bin', IAPEER_LAUNCHAGENTS_DIR: env.IAPEER_LAUNCHAGENTS_DIR!, IAPEER_ROOT: env.IAPEER_ROOT!, HOME: env.HOME! }))
83
- expect(third.changed).toBe(true)
84
- })
85
-
86
- test('collision guard: REFUSES to overwrite a foreign (no-sentinel) plist at the label', () => {
87
- const env = mkEnv()
88
- const laDir = env.IAPEER_LAUNCHAGENTS_DIR!
89
- mkdirSync(laDir, { recursive: true })
90
- const path = shadowPlistPath(env)
91
- const foreign = '<?xml version="1.0"?>\n<plist><dict><key>Label</key><string>com.iapeer.shadow-fidelity</string></dict></plist>\n'
92
- writeFileSync(path, foreign)
93
- expect(() => installShadowPlist(env)).toThrow(/foundation-managed|refus/i)
94
- expect(readFileSync(path, 'utf8')).toBe(foreign) // untouched — guard fires before any write
95
- })
96
- })
97
-
98
- describe('installShadowJob (sandbox fail-closed)', () => {
99
- test('writes the plist FILE but NEVER touches launchctl under IAPEER_TEST_SANDBOX → skipped-sandbox', () => {
100
- const env = mkEnv()
101
- const r = installShadowJob(env)
102
- expect(r.action).toBe('skipped-sandbox')
103
- expect(r.path).toBe(shadowPlistPath(env))
104
- expect(r.changed).toBe(true)
105
- expect(existsSync(r.path)).toBe(true) // file written to the override dir
106
- expect(isFoundationOwnedPlist(r.path)).toBe(true)
107
- })
108
- })
109
-
110
- describe('cycleShadowJob (deploy re-cycle hook)', () => {
111
- test('returns null when no plist exists (nothing to recycle)', () => {
112
- expect(cycleShadowJob(mkEnv())).toBeNull()
113
- })
114
- test('a written foundation plist recycles to skipped-sandbox under the test flag (never a real launchctl cycle)', () => {
115
- const env = mkEnv()
116
- installShadowPlist(env)
117
- expect(cycleShadowJob(env)?.state).toBe('skipped-sandbox')
118
- })
119
- test('a foreign plist at the label is NOT recycled (ownership guard) → null', () => {
120
- const env = mkEnv()
121
- mkdirSync(env.IAPEER_LAUNCHAGENTS_DIR!, { recursive: true })
122
- writeFileSync(shadowPlistPath(env), '<?xml version="1.0"?>\n<plist><dict/></plist>\n')
123
- expect(cycleShadowJob(env)).toBeNull()
124
- })
125
- })
126
-
127
- describe('uninstallShadowJob', () => {
128
- test('absent plist → absent (no-op)', () => {
129
- expect(uninstallShadowJob(mkEnv()).action).toBe('absent')
130
- })
131
- test('foundation plist under sandbox → skipped-sandbox, plist left in place (no real bootout)', () => {
132
- const env = mkEnv()
133
- installShadowPlist(env)
134
- const r = uninstallShadowJob(env)
135
- expect(r.action).toBe('skipped-sandbox')
136
- expect(existsSync(shadowPlistPath(env))).toBe(true)
137
- })
138
- test('foreign plist → refused-foreign, untouched', () => {
139
- const env = mkEnv()
140
- mkdirSync(env.IAPEER_LAUNCHAGENTS_DIR!, { recursive: true })
141
- const path = shadowPlistPath(env)
142
- writeFileSync(path, '<?xml version="1.0"?>\n<plist><dict/></plist>\n')
143
- expect(uninstallShadowJob(env).action).toBe('refused-foreign')
144
- expect(existsSync(path)).toBe(true)
145
- })
146
- })
147
-
148
- describe('exports wire to the running job', () => {
149
- test('SHADOW_PERSONALITY/LABEL match the live burn-in job naming', () => {
150
- expect(SHADOW_PERSONALITY).toBe('shadow-fidelity')
151
- expect(SHADOW_PLIST_LABEL).toBe('com.iapeer.shadow-fidelity')
152
- })
153
- })
@@ -1,42 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { Terminal } from '@xterm/headless'
3
- import { composerOccupancyFromModel, modelToPlainText } from './index.ts'
4
-
5
- // The observer's pty-side verdicts are pure functions of the @xterm model — hermetic, no tmux.
6
- // These mirror the verdicts validated 0-divergence vs the prod tmux verdicts in the migration
7
- // fidelity shadows (occupancy: getCell dim/ghost; ready-gate: isInputReady on plain text).
8
- const build = (frame: string, cols = 80, rows = 24): Promise<Terminal> =>
9
- new Promise(res => { const t = new Terminal({ cols, rows, allowProposedApi: true, scrollback: 200 }); t.write(frame, () => res(t)) })
10
-
11
- describe('composerOccupancyFromModel (pty occupancy verdict)', () => {
12
- test('human non-dim text after the prompt glyph → TRUE', async () => {
13
- const t = await build(`\x1b[24;1H❯ the quick brown fox`)
14
- expect(composerOccupancyFromModel(t, 80, 24, 'claude')).toBe(true)
15
- })
16
- test('dim/ghost placeholder after the glyph → FALSE (the critical discriminator)', async () => {
17
- const t = await build(`\x1b[24;1H❯ \x1b[2m\x1b[38;5;246mTry "fix typecheck errors"\x1b[0m`)
18
- expect(composerOccupancyFromModel(t, 80, 24, 'claude')).toBe(false)
19
- })
20
- test('empty composer (glyph alone) → FALSE', async () => {
21
- const t = await build(`\x1b[24;1H❯ `)
22
- expect(composerOccupancyFromModel(t, 80, 24, 'claude')).toBe(false)
23
- })
24
- test('codex glyph › with human text → TRUE', async () => {
25
- const t = await build(`\x1b[24;1H› reviewer note`)
26
- expect(composerOccupancyFromModel(t, 80, 24, 'codex')).toBe(true)
27
- })
28
- test('no prompt glyph at all → FALSE', async () => {
29
- const t = await build(`\x1b[24;1Hjust some output line`)
30
- expect(composerOccupancyFromModel(t, 80, 24, 'claude')).toBe(false)
31
- })
32
- })
33
-
34
- describe('modelToPlainText (feeds isInputReady)', () => {
35
- test('renders the viewport as plain text (no SGR), trailing-trimmed', async () => {
36
- const t = await build(`\x1b[1;1Hhello\x1b[2;1H\x1b[1mbold\x1b[0m world`)
37
- const plain = modelToPlainText(t, 80, 24)
38
- expect(plain.split('\n')[0]).toBe('hello')
39
- expect(plain).toContain('bold world')
40
- expect(plain).not.toContain('\x1b')
41
- })
42
- })
@@ -1,208 +0,0 @@
1
- // Memory slot — the DECLARATIVE provider slot (docs/Слот памяти — контракт
2
- // memory provider.md): the core only READS the root declaration; absent /
3
- // unreadable / invalid → EMPTY slot (bare core, valid state, never an error).
4
- // Plus the host-status assembly + rendering (memory: <provider> | none).
5
-
6
- import { afterEach, describe, expect, test } from 'bun:test'
7
- import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'fs'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import {
11
- formatHostStatus,
12
- heartbeatAgeSecs,
13
- hostStatus,
14
- memoryProviderPath,
15
- readMemoryProvider,
16
- type HostStatus,
17
- type MemoryProvider,
18
- } from './index.ts'
19
-
20
- const dirs: string[] = []
21
- function mkTmp(): string {
22
- const d = mkdtempSync(join(tmpdir(), 'iapeer-slot-'))
23
- dirs.push(d)
24
- return d
25
- }
26
- afterEach(() => {
27
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
28
- })
29
-
30
- function envFor(root: string): NodeJS.ProcessEnv {
31
- return { IAPEER_ROOT: root } as NodeJS.ProcessEnv
32
- }
33
-
34
- const VALID = {
35
- provider: 'iapeer-memory',
36
- package: '@agfpd/iapeer-memory',
37
- version: '0.1.0',
38
- registeredAt: '2026-06-10T00:00:00Z',
39
- }
40
-
41
- describe('readMemoryProvider (slot declaration, fail-open)', () => {
42
- test('absent file → null (EMPTY slot — valid bare state)', () => {
43
- expect(readMemoryProvider(envFor(mkTmp()))).toBeNull()
44
- })
45
-
46
- test('valid declaration → parsed provider; heartbeat optional', () => {
47
- const root = mkTmp()
48
- writeFileSync(memoryProviderPath(envFor(root)), JSON.stringify({ ...VALID, heartbeat: '/tmp/hb' }))
49
- const p = readMemoryProvider(envFor(root))
50
- expect(p).toMatchObject({ ...VALID, heartbeat: '/tmp/hb' })
51
- // without heartbeat — field absent, not empty-string
52
- writeFileSync(memoryProviderPath(envFor(root)), JSON.stringify(VALID))
53
- expect(readMemoryProvider(envFor(root))?.heartbeat).toBeUndefined()
54
- })
55
-
56
- test('garbage / wrong shape / missing required fields → null, NEVER throws', () => {
57
- const root = mkTmp()
58
- const env = envFor(root)
59
- for (const bad of ['NOT JSON {{{', '[]', '{}', JSON.stringify({ provider: 'x' }), JSON.stringify({ ...VALID, version: 42 })]) {
60
- writeFileSync(memoryProviderPath(env), bad)
61
- expect(readMemoryProvider(env)).toBeNull()
62
- }
63
- })
64
-
65
- test('LEGACY v1.1 plugin block (any shape) → ignored as unknown noise; the declaration itself stays valid (form removed 11.06, ADR-017)', () => {
66
- const root = mkTmp()
67
- const env = envFor(root)
68
- for (const legacy of [
69
- { name: 'iapeer-memory', marketplace: 'agfpd', marketplaceRef: 'agfpd/agfpd-marketplace' }, // a complete v1.1 block
70
- { name: 'x', marketplace: 'agfpd' },
71
- 'iapeer-memory',
72
- ['x'],
73
- ]) {
74
- writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, plugin: legacy }))
75
- const p = readMemoryProvider(env)
76
- expect(p).not.toBeNull()
77
- expect((p as Record<string, unknown> | null)?.plugin).toBeUndefined() // not parsed at all
78
- }
79
- })
80
-
81
- test('v1.2 provision/unprovision blocks: parsed when valid; relative command or bad args = absent (fail-open)', () => {
82
- const root = mkTmp()
83
- const env = envFor(root)
84
- const provision = { command: '/usr/local/bin/iapeer-memory', args: ['provision-peer', '--cwd', '{cwd}'] }
85
- const unprovision = { command: '/usr/local/bin/iapeer-memory', args: ['unprovision-peer'] }
86
- writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, provision, unprovision }))
87
- const p = readMemoryProvider(env)
88
- expect(p?.provision).toEqual(provision)
89
- expect(p?.unprovision).toEqual(unprovision)
90
- // empty args array is VALID (command alone)
91
- writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, provision: { command: '/x/y', args: [] } }))
92
- expect(readMemoryProvider(env)?.provision).toEqual({ command: '/x/y', args: [] })
93
- // invalid shapes → block absent, declaration itself stays valid
94
- for (const bad of [
95
- { command: 'iapeer-memory', args: [] }, // RELATIVE command (launchd minimal PATH) — contract-invalid
96
- { command: '/x/y' }, // args missing
97
- { command: '/x/y', args: ['ok', 42] }, // non-string arg
98
- { args: ['provision-peer'] }, // command missing
99
- 'provision-me', // not an object
100
- ]) {
101
- writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, provision: bad }))
102
- const r = readMemoryProvider(env)
103
- expect(r).not.toBeNull()
104
- expect(r?.provision).toBeUndefined()
105
- }
106
- })
107
- })
108
-
109
- describe('heartbeatAgeSecs', () => {
110
- test('no heartbeat declared → null; declared but absent file → null; present → age', () => {
111
- const root = mkTmp()
112
- expect(heartbeatAgeSecs({ ...VALID } as MemoryProvider)).toBeNull()
113
- const hb = join(root, 'memoryd.heartbeat')
114
- expect(heartbeatAgeSecs({ ...VALID, heartbeat: hb } as MemoryProvider)).toBeNull() // not running
115
- writeFileSync(hb, '')
116
- const old = (Date.now() - 45_000) / 1000
117
- utimesSync(hb, old, old)
118
- const age = heartbeatAgeSecs({ ...VALID, heartbeat: hb } as MemoryProvider)
119
- expect(age).toBeGreaterThanOrEqual(44)
120
- expect(age).toBeLessThanOrEqual(60)
121
- })
122
- })
123
-
124
- describe('hostStatus + formatHostStatus', () => {
125
- test('empty slot → memory: none; daemon health from the injected probe', async () => {
126
- const root = mkTmp()
127
- const s = await hostStatus({ env: envFor(root), probe: async () => true, fdaProbe: () => null })
128
- expect(s.daemon.healthy).toBe(true)
129
- expect(s.memory.provider).toBeNull()
130
- expect(formatHostStatus(s)).toContain('memory: none')
131
- })
132
-
133
- test('occupied slot + fresh heartbeat → provider line with age', async () => {
134
- const root = mkTmp()
135
- const env = envFor(root)
136
- const hb = join(root, 'memoryd.heartbeat')
137
- writeFileSync(hb, '')
138
- writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, heartbeat: hb }))
139
- const s = await hostStatus({ env, probe: async () => true, fdaProbe: () => null })
140
- expect(formatHostStatus(s)).toMatch(/memory: iapeer-memory 0\.1\.0 \(heartbeat \d+s ago\)/)
141
- })
142
-
143
- test('occupied slot, heartbeat declared but file ABSENT → "daemon not running" (graceful-shutdown semantics)', () => {
144
- const s: HostStatus = {
145
- version: '0.0.0',
146
- daemon: { healthy: false, url: null, sock: null },
147
- memory: {
148
- provider: { ...VALID, heartbeat: '/nope/hb' } as MemoryProvider,
149
- heartbeatAgeSecs: null,
150
- },
151
- fda: null,
152
- }
153
- const text = formatHostStatus(s)
154
- expect(text).toContain('memory: iapeer-memory 0.1.0 (daemon not running — no heartbeat file)')
155
- expect(text).toContain('daemon: NOT healthy')
156
- })
157
-
158
- test('discovery file addresses surface in the daemon line', async () => {
159
- const root = mkTmp()
160
- const env = envFor(root)
161
- const stateDir = join(root, 'state', 'iapeer')
162
- mkdirSync(stateDir, { recursive: true })
163
- writeFileSync(join(stateDir, 'router.json'), JSON.stringify({ sock: '/x/router.sock', tcp: 'http://127.0.0.1:8765/mcp' }))
164
- const s = await hostStatus({ env, probe: async () => true, fdaProbe: () => null })
165
- const text = formatHostStatus(s)
166
- expect(text).toContain('@ http://127.0.0.1:8765/mcp')
167
- expect(text).toContain('+ /x/router.sock')
168
- })
169
- })
170
-
171
- describe('FDA detection (iapeer status — fresh-host observability)', () => {
172
- function base(fda: boolean | null): HostStatus {
173
- return {
174
- version: '0.0.0',
175
- daemon: { healthy: true, url: null, sock: null },
176
- memory: { provider: null, heartbeatAgeSecs: null },
177
- fda,
178
- }
179
- }
180
-
181
- test('granted → terse OK line, no instruction', () => {
182
- const text = formatHostStatus(base(true))
183
- expect(text).toContain('fda: granted')
184
- expect(text).not.toContain('NOT granted')
185
- expect(text).not.toContain('Full Disk Access')
186
- })
187
-
188
- test('NOT granted → actionable hint with the binary path', () => {
189
- const text = formatHostStatus(base(false))
190
- expect(text).toContain('fda: NOT granted')
191
- expect(text).toContain('Full Disk Access')
192
- expect(text).toMatch(/iapeer$|iapeer\n/m) // the grant path ends at the binary
193
- })
194
-
195
- test('undeterminable → explicit unknown, never a false claim', () => {
196
- const text = formatHostStatus(base(null))
197
- expect(text).toContain('fda: unknown')
198
- expect(text).not.toContain('NOT granted')
199
- expect(text).not.toContain('granted\n') // not the bare "granted" either
200
- })
201
-
202
- test('hostStatus threads the injected fdaProbe verbatim', async () => {
203
- const root = mkTmp()
204
- const s = await hostStatus({ env: envFor(root), probe: async () => true, fdaProbe: () => false })
205
- expect(s.fda).toBe(false)
206
- expect(formatHostStatus(s)).toContain('fda: NOT granted')
207
- })
208
- })
@@ -1,34 +0,0 @@
1
- // peersHomeDir / defaultPeerCwd + ensureGlobalIapScaffold creating peers/ — the
2
- // foundation-owned default home for `iapeer create`.
3
-
4
- import { afterEach, describe, expect, test } from 'bun:test'
5
- import { existsSync, mkdtempSync, rmSync } from 'fs'
6
- import { tmpdir } from 'os'
7
- import { join } from 'path'
8
- import { defaultPeerCwd, ensureGlobalIapScaffold, peersHomeDir } from './index.ts'
9
-
10
- const dirs: string[] = []
11
- function mkTmp(): string {
12
- const d = mkdtempSync(join(tmpdir(), 'iapeer-ph-'))
13
- dirs.push(d)
14
- return d
15
- }
16
- afterEach(() => {
17
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
18
- })
19
-
20
- describe('peers home', () => {
21
- test('peersHomeDir / defaultPeerCwd resolve under IAPEER_ROOT', () => {
22
- const root = mkTmp()
23
- const env = { IAPEER_ROOT: join(root, 'iapeer'), HOME: root } as NodeJS.ProcessEnv
24
- expect(peersHomeDir({ env })).toBe(join(root, 'iapeer', 'peers'))
25
- expect(defaultPeerCwd('worker', { env })).toBe(join(root, 'iapeer', 'peers', 'worker'))
26
- })
27
-
28
- test('ensureGlobalIapScaffold creates ~/.iapeer/peers/', () => {
29
- const root = mkTmp()
30
- const env = { IAPEER_ROOT: join(root, 'iapeer'), HOME: root } as NodeJS.ProcessEnv
31
- ensureGlobalIapScaffold({ env })
32
- expect(existsSync(peersHomeDir({ env }))).toBe(true)
33
- })
34
- })
@@ -1,44 +0,0 @@
1
- // rotatelog — the generic rotated-logfmt-append primitive (promoted from
2
- // lifecycle/eventlog.ts when the daemon's delivery.log became the second
3
- // producer). The logfmt formatting (fmtValue/formatEventLine) is pinned in
4
- // lifecycle/eventlog.test.ts (its historical home, re-exported); these tests
5
- // cover what is NEW at this layer: the path-parameterized append + rotation.
6
-
7
- import { afterEach, describe, expect, test } from 'bun:test'
8
- import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import { appendRotatedEvent } from './rotatelog.ts'
12
-
13
- const dirs: string[] = []
14
- function mkTmp(): string {
15
- const d = mkdtempSync(join(tmpdir(), 'iapeer-rotatelog-'))
16
- dirs.push(d)
17
- return d
18
- }
19
- afterEach(() => {
20
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
21
- })
22
-
23
- describe('appendRotatedEvent', () => {
24
- test('creates the parent dir and appends one ts-stamped logfmt line', () => {
25
- const path = join(mkTmp(), 'deep', 'nested', 'some.log')
26
- appendRotatedEvent(path, { ev: 'delivery', ok: 'true', note: 'two words' }, { nowMs: 1750000000000 })
27
- const text = readFileSync(path, 'utf8')
28
- expect(text).toBe('ts=2025-06-15T15:06:40.000Z ev=delivery ok=true note="two words"\n')
29
- })
30
-
31
- test('rotates base → .1 at maxBytes and drops beyond keep', () => {
32
- const path = join(mkTmp(), 'r.log')
33
- const line = { ev: 'x', pad: 'a'.repeat(50) } // ~65 bytes/line
34
- // maxBytes 100 → every second append rotates; keep 1 → no .2 ever exists.
35
- for (let i = 0; i < 6; i++) appendRotatedEvent(path, line, { maxBytes: 100, keep: 1 })
36
- expect(existsSync(path)).toBe(true)
37
- expect(existsSync(`${path}.1`)).toBe(true)
38
- expect(existsSync(`${path}.2`)).toBe(false)
39
- })
40
-
41
- test('never throws on an unwritable path (best-effort observability)', () => {
42
- expect(() => appendRotatedEvent('/dev/null/impossible/x.log', { ev: 'x' })).not.toThrow()
43
- })
44
- })
@@ -1,65 +0,0 @@
1
- // readLaunchEnv — the per-peer per-runtime launch.env parser (Ф-A #5, zone
2
- // Хранение / Рантайм-адаптеры). Pure FS parser, no tmux: PEER_START_ARGS
3
- // word-splits (faithful to the legacy unquoted ${PEER_START_ARGS} expansion),
4
- // other KEY=VALUE lines become child env, quotes are stripped, comments/blanks
5
- // are ignored, and a missing file → empty.
6
-
7
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
8
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import { peerLaunchEnvPath, readLaunchEnv } from './index.ts'
12
-
13
- let cwd: string
14
- beforeEach(() => {
15
- cwd = mkdtempSync(join(tmpdir(), 'iapeer-launchenv-'))
16
- })
17
- afterEach(() => {
18
- rmSync(cwd, { recursive: true, force: true })
19
- })
20
-
21
- function writeLaunchEnv(runtime: string, body: string): void {
22
- const p = peerLaunchEnvPath(cwd, runtime)
23
- mkdirSync(join(p, '..'), { recursive: true })
24
- writeFileSync(p, body)
25
- }
26
-
27
- describe('readLaunchEnv', () => {
28
- test('missing file → empty (no flags, no env)', () => {
29
- expect(readLaunchEnv(cwd, 'claude')).toEqual({ startArgs: [], env: {} })
30
- })
31
-
32
- test('PEER_START_ARGS word-splits on whitespace; surrounding quotes stripped', () => {
33
- writeLaunchEnv('claude', 'PEER_START_ARGS="--dangerously-load-development-channels --foo bar"\n')
34
- expect(readLaunchEnv(cwd, 'claude').startArgs).toEqual([
35
- '--dangerously-load-development-channels',
36
- '--foo',
37
- 'bar',
38
- ])
39
- })
40
-
41
- test('single arg, no quotes', () => {
42
- writeLaunchEnv('codex', 'PEER_START_ARGS=--no-alt-screen\n')
43
- expect(readLaunchEnv(cwd, 'codex').startArgs).toEqual(['--no-alt-screen'])
44
- })
45
-
46
- test('other KEY=VALUE lines become child env; PEER_START_ARGS is consumed (not in env)', () => {
47
- writeLaunchEnv('claude', '# a comment\nexport FOO=bar\nBAZ="q u x"\nPEER_START_ARGS="--x"\n\n')
48
- const { startArgs, env } = readLaunchEnv(cwd, 'claude')
49
- expect(startArgs).toEqual(['--x'])
50
- expect(env).toEqual({ FOO: 'bar', BAZ: 'q u x' })
51
- expect(env.PEER_START_ARGS).toBeUndefined()
52
- })
53
-
54
- test('blank lines, comments, and non-assignment lines are ignored', () => {
55
- writeLaunchEnv('claude', '\n # comment\nnot an assignment line\nKEY=value\n')
56
- expect(readLaunchEnv(cwd, 'claude').env).toEqual({ KEY: 'value' })
57
- })
58
-
59
- test('empty PEER_START_ARGS → no args', () => {
60
- writeLaunchEnv('claude', 'PEER_START_ARGS=""\nFOO=bar\n')
61
- const { startArgs, env } = readLaunchEnv(cwd, 'claude')
62
- expect(startArgs).toEqual([])
63
- expect(env).toEqual({ FOO: 'bar' })
64
- })
65
- })
@@ -1,84 +0,0 @@
1
- // Spawn-flip cutover Block 2, Ф0b-3 slice 3c-1 — the supervisor `.attached` marker (the gate signal
2
- // `hasAttachedSupervisorClient` reads to hold hosted delivery ONLY for a real attached human).
3
- // boris's three faces: (1) present ⟺ clients.size>0; (2) ALL detach paths incl. ABNORMAL (abrupt
4
- // terminate → error/close), not just a clean Ctrl-]; (3) CRASH-ROBUSTNESS — a crashed predecessor
5
- // skips shutdown-cleanup, so a stale marker must be cleared on daemon START. Real detached daemon over
6
- // a real unix socket (tick runtime — no codex needed); the marker is read CROSS-PROCESS, as in prod.
7
- import { afterEach, describe, expect, test } from 'bun:test'
8
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
9
- import { tmpdir } from 'node:os'
10
- import { join } from 'node:path'
11
- import { frame, FRAME_RESIZE, sizePayload } from './protocol.ts'
12
- import { killSession, sessionAlive, sockPath, startSupervisorDaemon } from './index.ts'
13
- import { attachedPath } from './paths.ts'
14
-
15
- const [maj, min, pat] = (Bun.version || '0.0.0').split('.').map(Number) as [number, number, number]
16
- const bunPty = maj > 1 || (maj === 1 && (min > 3 || (min === 3 && pat >= 5)))
17
- const d = bunPty ? describe : describe.skip
18
-
19
- const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms))
20
- async function waitFor(pred: () => boolean, timeoutMs = 5000, step = 40): Promise<boolean> {
21
- const end = Date.now() + timeoutMs
22
- while (Date.now() < end) { if (pred()) return true; await sleep(step) }
23
- return false
24
- }
25
-
26
- interface Client { end(): void; terminate(): void }
27
- async function attach(runDir: string, session: string): Promise<Client> {
28
- const sock = await Bun.connect({
29
- unix: sockPath(runDir, session),
30
- socket: { open(s) { s.write(frame(FRAME_RESIZE, sizePayload(80, 24))) }, data() { /* */ } },
31
- })
32
- return {
33
- end: () => { try { sock.end() } catch { /* */ } },
34
- terminate: () => { try { sock.terminate() } catch { /* */ } },
35
- }
36
- }
37
-
38
- const dirs: string[] = []
39
- const mkRunDir = (): string => { const d2 = mkdtempSync(join(tmpdir(), 'iapeer-attmark-')); dirs.push(d2); return d2 }
40
- afterEach(() => { for (const d2 of dirs.splice(0)) rmSync(d2, { recursive: true, force: true }) })
41
-
42
- d('supervisor .attached marker (the hosted occupancy gate signal)', () => {
43
- test('present ⟺ clients.size>0 across multiple clients (no stuck "attached" on partial detach)', async () => {
44
- const runDir = mkRunDir(), session = 'mk'
45
- startSupervisorDaemon({ session, runtime: 'tick', runDir })
46
- expect(await waitFor(() => sessionAlive(runDir, session))).toBe(true)
47
- const marker = attachedPath(runDir, session)
48
- expect(existsSync(marker)).toBe(false) // no client yet → absent
49
- const c1 = await attach(runDir, session)
50
- expect(await waitFor(() => existsSync(marker))).toBe(true) // ≥1 attached → present
51
- const c2 = await attach(runDir, session)
52
- await sleep(150)
53
- expect(existsSync(marker)).toBe(true) // 2 attached → still present
54
- c1.end()
55
- await sleep(250)
56
- expect(existsSync(marker)).toBe(true) // 1 still attached → NOT stuck-absent, NOT removed early
57
- c2.end()
58
- expect(await waitFor(() => !existsSync(marker))).toBe(true) // 0 attached → absent
59
- killSession(runDir, session)
60
- })
61
-
62
- test('ABNORMAL detach (abrupt terminate, not a clean Ctrl-]) → marker still removed', async () => {
63
- const runDir = mkRunDir(), session = 'ab'
64
- startSupervisorDaemon({ session, runtime: 'tick', runDir })
65
- expect(await waitFor(() => sessionAlive(runDir, session))).toBe(true)
66
- const marker = attachedPath(runDir, session)
67
- const c = await attach(runDir, session)
68
- expect(await waitFor(() => existsSync(marker))).toBe(true)
69
- c.terminate() // abrupt RST — daemon sees error/close, NOT a graceful protocol detach
70
- expect(await waitFor(() => !existsSync(marker))).toBe(true) // the error/close path ALSO syncs the marker
71
- killSession(runDir, session)
72
- })
73
-
74
- test('CRASH-ROBUSTNESS: a stale marker is cleared on daemon START (crash skips shutdown-cleanup)', async () => {
75
- const runDir = mkRunDir(), session = 'cr'
76
- const marker = attachedPath(runDir, session)
77
- writeFileSync(marker, '') // a crashed predecessor left this present, with no live client
78
- expect(existsSync(marker)).toBe(true)
79
- startSupervisorDaemon({ session, runtime: 'tick', runDir }) // fresh daemon = 0 clients
80
- expect(await waitFor(() => sessionAlive(runDir, session))).toBe(true)
81
- expect(await waitFor(() => !existsSync(marker))).toBe(true) // start-cleanup removed the stale lie
82
- killSession(runDir, session)
83
- })
84
- })