@agfpd/iapeer 0.2.77 → 0.2.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +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 +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 -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,193 +0,0 @@
1
- // Native runtime-memory levers — the canonized forms (slot contract §Native-
2
- // память). The HARD requirement pinned here: NO-CLOBBER — both target files
3
- // carry foreign blocks (plugin enables, statusline wrappers) that every lever
4
- // write must preserve byte-meaningfully.
5
-
6
- import { afterEach, describe, expect, test } from 'bun:test'
7
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import {
11
- applyNativeMemory,
12
- claudeSettingsPath,
13
- codexProjectConfigPath,
14
- codexGlobalConfigPath,
15
- preTrustCodexCwd,
16
- removeCodexCwdTrust,
17
- } from './nativeMemory.ts'
18
-
19
- const dirs: string[] = []
20
- function mkTmp(): string {
21
- const d = mkdtempSync(join(tmpdir(), 'iapeer-natmem-'))
22
- dirs.push(d)
23
- return d
24
- }
25
- afterEach(() => {
26
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
27
- })
28
-
29
- describe('claude lever (settings.json merge)', () => {
30
- test('absent file → created with ONLY the lever key', () => {
31
- const cwd = mkTmp()
32
- const [o] = applyNativeMemory(cwd, ['claude'], 'off')
33
- expect(o?.state).toBe('written')
34
- expect(JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))).toEqual({ autoMemoryEnabled: false })
35
- })
36
-
37
- test('NO-CLOBBER: foreign keys (plugins, statusline) survive off AND on', () => {
38
- const cwd = mkTmp()
39
- mkdirSync(join(cwd, '.claude'), { recursive: true })
40
- const foreign = { enabledPlugins: { 'MergeMind@agfpd': true }, statusLine: { type: 'command', command: 'x' } }
41
- writeFileSync(claudeSettingsPath(cwd), JSON.stringify(foreign))
42
- applyNativeMemory(cwd, ['claude'], 'off')
43
- const afterOff = JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))
44
- expect(afterOff).toEqual({ ...foreign, autoMemoryEnabled: false })
45
- const [on] = applyNativeMemory(cwd, ['claude'], 'on')
46
- expect(on?.state).toBe('written')
47
- expect(JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))).toEqual(foreign) // key REMOVED, default restored
48
- })
49
-
50
- test('idempotent: already-false → already; on with no key → already', () => {
51
- const cwd = mkTmp()
52
- applyNativeMemory(cwd, ['claude'], 'off')
53
- expect(applyNativeMemory(cwd, ['claude'], 'off')[0]?.state).toBe('already')
54
- applyNativeMemory(cwd, ['claude'], 'on')
55
- expect(applyNativeMemory(cwd, ['claude'], 'on')[0]?.state).toBe('already')
56
- })
57
-
58
- test('non-object settings.json → failed (refuses to clobber), file untouched', () => {
59
- const cwd = mkTmp()
60
- mkdirSync(join(cwd, '.claude'), { recursive: true })
61
- writeFileSync(claudeSettingsPath(cwd), '"just a string"')
62
- const [o] = applyNativeMemory(cwd, ['claude'], 'off')
63
- expect(o?.state).toBe('failed')
64
- expect(readFileSync(claudeSettingsPath(cwd), 'utf8')).toBe('"just a string"')
65
- })
66
- })
67
-
68
- describe('codex lever (config.toml [features] merge)', () => {
69
- test('absent file → [features] section with the lever line', () => {
70
- const cwd = mkTmp()
71
- const [o] = applyNativeMemory(cwd, ['codex'], 'off')
72
- expect(o?.state).toBe('written')
73
- expect(readFileSync(codexProjectConfigPath(cwd), 'utf8')).toBe('[features]\nmemories = false\n')
74
- })
75
-
76
- test('NO-CLOBBER: plugin blocks survive; [features] appended after them (fleet-file shape)', () => {
77
- const cwd = mkTmp()
78
- mkdirSync(join(cwd, '.codex'), { recursive: true })
79
- const fleet = '[plugins."Inter-Agent-Protocol@agfpd"]\nenabled = true\n\n[plugins."MergeMind@agfpd"]\nenabled = true\n'
80
- writeFileSync(codexProjectConfigPath(cwd), fleet)
81
- applyNativeMemory(cwd, ['codex'], 'off')
82
- const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
83
- expect(text).toContain('[plugins."Inter-Agent-Protocol@agfpd"]\nenabled = true')
84
- expect(text).toContain('[plugins."MergeMind@agfpd"]\nenabled = true')
85
- expect(text).toContain('[features]\nmemories = false')
86
- })
87
-
88
- test('existing [features] WITHOUT memories (followed by another section) → inserted INSIDE the section', () => {
89
- const cwd = mkTmp()
90
- mkdirSync(join(cwd, '.codex'), { recursive: true })
91
- writeFileSync(codexProjectConfigPath(cwd), '[features]\nother = true\n\n[plugins."X@y"]\nenabled = true\n')
92
- applyNativeMemory(cwd, ['codex'], 'off')
93
- const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
94
- // the lever line lands in [features], NOT in [plugins."X@y"]
95
- expect(text.indexOf('memories = false')).toBeLessThan(text.indexOf('[plugins."X@y"]'))
96
- expect(text).toContain('other = true')
97
- })
98
-
99
- test('memories = true → replaced with false; already false → already', () => {
100
- const cwd = mkTmp()
101
- mkdirSync(join(cwd, '.codex'), { recursive: true })
102
- writeFileSync(codexProjectConfigPath(cwd), '[features]\nmemories = true\n')
103
- expect(applyNativeMemory(cwd, ['codex'], 'off')[0]?.state).toBe('written')
104
- expect(readFileSync(codexProjectConfigPath(cwd), 'utf8')).toContain('memories = false')
105
- expect(applyNativeMemory(cwd, ['codex'], 'off')[0]?.state).toBe('already')
106
- })
107
-
108
- test('on → the memories line is REMOVED (default restored), section + foreign lines kept', () => {
109
- const cwd = mkTmp()
110
- mkdirSync(join(cwd, '.codex'), { recursive: true })
111
- writeFileSync(codexProjectConfigPath(cwd), '[features]\nmemories = false\nother = true\n')
112
- const [o] = applyNativeMemory(cwd, ['codex'], 'on')
113
- expect(o?.state).toBe('written')
114
- const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
115
- expect(text).not.toContain('memories')
116
- expect(text).toContain('other = true')
117
- expect(applyNativeMemory(cwd, ['codex'], 'on')[0]?.state).toBe('already')
118
- })
119
- })
120
-
121
- describe('applyNativeMemory runtime dispatch', () => {
122
- test('both runtimes → both levers; router-only runtimes → no outcomes', () => {
123
- const cwd = mkTmp()
124
- const both = applyNativeMemory(cwd, ['claude', 'codex'], 'off')
125
- expect(both.map(o => o.runtime).sort()).toEqual(['claude', 'codex'])
126
- expect(applyNativeMemory(mkTmp(), ['telegram', 'notifier'], 'off')).toEqual([])
127
- })
128
- })
129
-
130
- describe('preTrustCodexCwd (birth-time trust, codex global config)', () => {
131
- test('codexGlobalConfigPath honors $CODEX_HOME first (same override set as init codexConfigPath — sandbox seam, live-caught by D4 11.06)', () => {
132
- expect(codexGlobalConfigPath({ CODEX_HOME: '/sandbox/codex', HOME: '/real/home' } as NodeJS.ProcessEnv)).toBe(
133
- '/sandbox/codex/config.toml',
134
- )
135
- expect(codexGlobalConfigPath({ HOME: '/real/home' } as NodeJS.ProcessEnv)).toBe('/real/home/.codex/config.toml')
136
- })
137
-
138
- test('appends a [projects] block once; NO-CLOBBER of existing content; idempotent', () => {
139
- const home = mkTmp()
140
- const env = { HOME: home } as NodeJS.ProcessEnv
141
- mkdirSync(join(home, '.codex'), { recursive: true })
142
- const existing = '[projects."/already/trusted"]\ntrust_level = "trusted"\n'
143
- writeFileSync(codexGlobalConfigPath(env), existing)
144
- const first = preTrustCodexCwd('/peers/newborn', env)
145
- expect(first.state).toBe('written')
146
- const text = readFileSync(codexGlobalConfigPath(env), 'utf8')
147
- expect(text).toContain(existing.trim())
148
- expect(text).toContain('[projects."/peers/newborn"]\ntrust_level = "trusted"')
149
- expect(preTrustCodexCwd('/peers/newborn', env).state).toBe('already')
150
- })
151
-
152
- test('removeCodexCwdTrust: drops EXACTLY the peer cwd section (header+body), keeps neighbors; idempotent; handles deleted cwd via literal match', () => {
153
- const home = mkTmp()
154
- const env = { HOME: home } as NodeJS.ProcessEnv
155
- mkdirSync(join(home, '.codex'), { recursive: true })
156
- const cfg =
157
- '[mcp_servers.iapeer]\nurl = "http://x"\n\n' +
158
- '[projects."/keep/me"]\ntrust_level = "trusted"\n\n' +
159
- '[projects."/peers/doomed"]\ntrust_level = "trusted"\n\n' +
160
- '[features]\nmemories = false\n'
161
- writeFileSync(codexGlobalConfigPath(env), cfg)
162
- // cwd "/peers/doomed" does not exist on disk → realpath fails → literal match
163
- const first = removeCodexCwdTrust('/peers/doomed', env)
164
- expect(first.state).toBe('written')
165
- const text = readFileSync(codexGlobalConfigPath(env), 'utf8')
166
- expect(text).not.toContain('/peers/doomed')
167
- expect(text).toContain('[projects."/keep/me"]\ntrust_level = "trusted"') // neighbor intact
168
- expect(text).toContain('[mcp_servers.iapeer]') // foreign sections intact
169
- expect(text).toContain('[features]\nmemories = false')
170
- expect(removeCodexCwdTrust('/peers/doomed', env).state).toBe('already') // idempotent
171
- expect(removeCodexCwdTrust('/never/was', env).state).toBe('already') // absent entry → no-op
172
- })
173
-
174
- test('removeCodexCwdTrust matches the RESOLVED path of a live symlinked cwd (writer stores realpath)', () => {
175
- const home = mkTmp()
176
- const env = { HOME: home } as NodeJS.ProcessEnv
177
- mkdirSync(join(home, '.codex'), { recursive: true })
178
- const cwd = mkTmp() // /var/folders/... on macOS — realpath differs from literal /tmp form
179
- // write the entry the way preTrustCodexCwd does (by realpath)
180
- preTrustCodexCwd(cwd, env)
181
- expect(readFileSync(codexGlobalConfigPath(env), 'utf8')).toContain('[projects."')
182
- // remove by the LITERAL cwd — must hit the resolved-form entry
183
- expect(removeCodexCwdTrust(cwd, env).state).toBe('written')
184
- expect(readFileSync(codexGlobalConfigPath(env), 'utf8')).not.toContain('trust_level')
185
- })
186
-
187
- test('absent global config → created with just the block', () => {
188
- const home = mkTmp()
189
- const env = { HOME: home } as NodeJS.ProcessEnv
190
- expect(preTrustCodexCwd('/peers/first', env).state).toBe('written')
191
- expect(readFileSync(codexGlobalConfigPath(env), 'utf8')).toBe('[projects."/peers/first"]\ntrust_level = "trusted"\n')
192
- })
193
- })
@@ -1,38 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { ptyHostEnabled } from './ptyHost.ts'
6
- import { readyGatePtyFlipEnabled } from './readyGateModel.ts'
7
-
8
- describe('ptyHostEnabled — pty-hosting is the DEFAULT, .no-pty-host opts out', () => {
9
- test('ON by default (no marker needed); OFF when the .no-pty-host opt-out marker exists; per-peer', () => {
10
- const dir = mkdtempSync(join(tmpdir(), 'ptyhost-flag-'))
11
- try {
12
- expect(ptyHostEnabled(dir, 'codex-x')).toBe(true) // DEFAULT: hosted out of the box, no marker
13
- writeFileSync(join(dir, 'codex-x.no-pty-host'), '') // explicit opt-OUT → tmux
14
- expect(ptyHostEnabled(dir, 'codex-x')).toBe(false)
15
- expect(ptyHostEnabled(dir, 'codex-y')).toBe(true) // opt-out is per-peer, not global — y stays default-on
16
- } finally {
17
- rmSync(dir, { recursive: true, force: true })
18
- }
19
- })
20
-
21
- test('legacy .pty-host opt-in marker is REDUNDANT (ignored); .pty-readygate is orthogonal; only .no-pty-host flips to tmux', () => {
22
- const dir = mkdtempSync(join(tmpdir(), 'ptyhost-distinct-'))
23
- try {
24
- // legacy opt-in marker no longer decides anything — pty is the default with or without it
25
- writeFileSync(join(dir, 'codex-perplex.pty-host'), '')
26
- expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(true)
27
- // the ready-gate viewport-source marker is a SEPARATE concern — it does not decide pty-hosting
28
- writeFileSync(join(dir, 'codex-perplex.pty-readygate'), '')
29
- expect(readyGatePtyFlipEnabled(dir, 'codex-perplex')).toBe(true)
30
- expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(true) // still hosted (default)
31
- // only the explicit opt-out marker flips it back to tmux
32
- writeFileSync(join(dir, 'codex-perplex.no-pty-host'), '')
33
- expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(false)
34
- } finally {
35
- rmSync(dir, { recursive: true, force: true })
36
- }
37
- })
38
- })
@@ -1,72 +0,0 @@
1
- import { afterEach, describe, expect, test } from 'bun:test'
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { logReadyGateCrossCheck, readyGatePtyFlipEnabled, readyGateViewport } from './readyGateModel.ts'
6
-
7
- // The Ф1 ready-gate source swap is DARK by default and falls back to capture-pane UNCONDITIONALLY,
8
- // so the flip can never make readiness worse than tmux today. These prove exactly that contract.
9
- const tmps: string[] = []
10
- const mkLogDir = (): string => {
11
- const d = mkdtempSync(join(tmpdir(), 'iapeer-rgm-'))
12
- tmps.push(d)
13
- return d
14
- }
15
- afterEach(() => {
16
- while (tmps.length) rmSync(tmps.pop()!, { recursive: true, force: true })
17
- })
18
- const FALLBACK = '<<capture-pane-fallback>>'
19
-
20
- describe('readyGatePtyFlipEnabled (per-peer flip flag, canary-first)', () => {
21
- test('absent marker → off; present marker → on', () => {
22
- const d = mkLogDir()
23
- expect(readyGatePtyFlipEnabled(d, 'codex-x')).toBe(false)
24
- writeFileSync(join(d, 'codex-x.pty-readygate'), '')
25
- expect(readyGatePtyFlipEnabled(d, 'codex-x')).toBe(true)
26
- })
27
- })
28
-
29
- describe('readyGateViewport (Ф1 viewport-source swap, tmux-fallback)', () => {
30
- test('flag OFF → the capture-pane fallback VERBATIM (byte-identical to today, no model built)', async () => {
31
- const d = mkLogDir()
32
- expect(await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 80, rows: 24 })).toBe(FALLBACK)
33
- })
34
-
35
- test('flag ON + a pane-log → the MODEL viewport (source swapped), not the fallback', async () => {
36
- const d = mkLogDir()
37
- writeFileSync(join(d, 'codex-x.pty-readygate'), '')
38
- writeFileSync(join(d, 'codex-x.log'), '\x1b[24;1H› ready prompt here') // render a codex '›' prompt row
39
- const v = await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 80, rows: 24 })
40
- expect(v).not.toBe(FALLBACK) // the source was swapped to the model
41
- expect(v).toContain('› ready prompt here') // the model rendered the prompt the predicate keys on
42
- })
43
-
44
- test('flag ON but the pane-log is MISSING → fallback (never worse than tmux)', async () => {
45
- const d = mkLogDir()
46
- writeFileSync(join(d, 'codex-x.pty-readygate'), '')
47
- expect(await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 80, rows: 24 })).toBe(FALLBACK)
48
- })
49
-
50
- test('flag ON but geometry is unknown (0x0) → fallback', async () => {
51
- const d = mkLogDir()
52
- writeFileSync(join(d, 'codex-x.pty-readygate'), '')
53
- writeFileSync(join(d, 'codex-x.log'), '\x1b[24;1H› ready')
54
- expect(await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 0, rows: 0 })).toBe(FALLBACK)
55
- })
56
- })
57
-
58
- describe('logReadyGateCrossCheck (serving-path soak observability)', () => {
59
- test('appends a record with diverged computed; agreement → false, mismatch → true', () => {
60
- const d = mkLogDir()
61
- logReadyGateCrossCheck(d, { identity: 'codex-x', modelReady: true, captureReady: false, geom: '220x50', nowISO: '2026-06-14T00:00:00Z' })
62
- logReadyGateCrossCheck(d, { identity: 'codex-x', modelReady: true, captureReady: true, geom: '220x50', nowISO: '2026-06-14T00:00:05Z' })
63
- const lines = readFileSync(join(d, 'readygate-flip.jsonl'), 'utf8').trim().split('\n').map(l => JSON.parse(l))
64
- expect(lines[0].diverged).toBe(true) // model ahead of capture
65
- expect(lines[0].modelReady).toBe(true)
66
- expect(lines[0].captureReady).toBe(false)
67
- expect(lines[1].diverged).toBe(false) // agreement
68
- })
69
- test('no event-log dir → no-op (never blocks the ready-gate)', () => {
70
- expect(() => logReadyGateCrossCheck(undefined, { identity: 'x', modelReady: true, captureReady: true, geom: '1x1', nowISO: 't' })).not.toThrow()
71
- })
72
- })
@@ -1,74 +0,0 @@
1
- // Regression: launch() must `mkdir -p` the socket's parent dir before
2
- // `tmux new-session -S <sock>` — tmux does NOT create it and fails silently
3
- // ("session died immediately") when the IAPEER_SOCK_DIR override points at a
4
- // not-yet-created dir (prod sock=/tmp always exists; this bit a sandbox pilot).
5
-
6
- import { afterEach, describe, expect, test } from 'bun:test'
7
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
8
- import { tmpdir } from 'os'
9
- import { dirname, join } from 'path'
10
- import { spawnSync } from 'child_process'
11
- import { launch } from './index.ts'
12
- import { notifierAdapter } from './adapters/notifier.ts'
13
- import type { LaunchConfig, LaunchSpec } from './types.ts'
14
-
15
- const dirs: string[] = []
16
- function mkTmp(): string {
17
- const d = mkdtempSync(join(tmpdir(), 'iapeer-sockdir-'))
18
- dirs.push(d)
19
- return d
20
- }
21
- afterEach(() => {
22
- while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
23
- })
24
-
25
- const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
26
-
27
- describe('launch creates a missing socket dir', () => {
28
- test.if(tmuxAvailable)('router launch into a NON-EXISTENT IAPEER_SOCK_DIR comes up (dir auto-created)', async () => {
29
- const root = mkTmp()
30
- const sockDir = join(root, 'does', 'not', 'exist', 'yet') // deep, absent
31
- const sock = join(sockDir, 'tmux-iap-notifier-sockt.sock')
32
- const bin = join(root, 'notifier-runtime')
33
- writeFileSync(bin, '#!/bin/sh\nexec sleep 30\n', { mode: 0o755 })
34
-
35
- const spec: LaunchSpec = {
36
- personality: 'sockt',
37
- runtime: 'notifier',
38
- cwd: root,
39
- identity: 'notifier-sockt',
40
- socketPath: sock,
41
- intelligence: 'absent',
42
- }
43
- const cfg: LaunchConfig = {
44
- claudeBin: 'claude',
45
- codexBin: 'codex',
46
- notifierBin: bin,
47
- sockDir,
48
- bootDeadlineSecs: 1,
49
- readyGateSecs: 1,
50
- logDir: join(root, 'logs'),
51
- alwaysOn: true,
52
- }
53
-
54
- // pty-supervisor hosting is the DEFAULT; this regression tests the legacy TMUX new-session sock-dir
55
- // mkdir, so opt this peer OUT of pty with a .no-pty-host marker (else it is pty-hosted and the tmux
56
- // mkdir never runs).
57
- mkdirSync(cfg.logDir, { recursive: true })
58
- writeFileSync(join(cfg.logDir, `${spec.identity}.no-pty-host`), '')
59
- try {
60
- // The regression assertion: before the fix the dir was NOT created and
61
- // `tmux new-session -S <sock>` failed silently; now launch mkdir's it. (We assert
62
- // dir creation, not the READY outcome — whether the session reaches READY depends
63
- // on tmux + the test-runner env, which is orthogonal to this fix.)
64
- await launch(spec, notifierAdapter, '', cfg)
65
- expect(existsSync(sockDir)).toBe(true)
66
- } finally {
67
- spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
68
- }
69
- })
70
-
71
- test('the fix is unconditional dirname(sock) creation (documents the parent)', () => {
72
- expect(dirname('/a/b/c/tmux-iap-notifier-x.sock')).toBe('/a/b/c')
73
- })
74
- })
@@ -1,114 +0,0 @@
1
- // eventlog — the daemon's durable, rotated lifecycle decision log. Tests the pure
2
- // logfmt formatter, the append path (into an explicit temp logDir — never the real
3
- // ~/.iapeer), and the size-rotation chain. No daemon, no tmux — pure FS.
4
-
5
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
6
- import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'fs'
7
- import { tmpdir } from 'os'
8
- import { join } from 'path'
9
- import {
10
- appendLifecycleEvent,
11
- fmtValue,
12
- formatEventLine,
13
- lifecycleLogPath,
14
- } from './eventlog.ts'
15
-
16
- const TS = 1_749_470_400_000 // fixed epoch-ms → a stable ISO for golden lines
17
- const ISO = new Date(TS).toISOString()
18
-
19
- describe('fmtValue (logfmt escaping)', () => {
20
- test('bare token stays bare', () => {
21
- expect(fmtValue('reaped-idle')).toBe('reaped-idle')
22
- expect(fmtValue('claude-boris')).toBe('claude-boris')
23
- expect(fmtValue(42)).toBe('42')
24
- })
25
- test('empty string → ""', () => {
26
- expect(fmtValue('')).toBe('""')
27
- })
28
- test('whitespace / = / " force quoting and escape', () => {
29
- expect(fmtValue('session no longer live')).toBe('"session no longer live"')
30
- expect(fmtValue('a=b')).toBe('"a=b"')
31
- expect(fmtValue('say "hi"')).toBe('"say \\"hi\\""')
32
- expect(fmtValue('back\\slash here')).toBe('"back\\\\slash here"')
33
- })
34
- })
35
-
36
- describe('formatEventLine', () => {
37
- test('ts is first; fields keep insertion order; undefined skipped', () => {
38
- const line = formatEventLine(TS, {
39
- ev: 'supervise',
40
- identity: 'claude-boris',
41
- action: 'reaped-gone',
42
- reason: 'session no longer live',
43
- ref: undefined, // dropped
44
- outcome: 'fresh-next-msg',
45
- })
46
- expect(line).toBe(
47
- `ts=${ISO} ev=supervise identity=claude-boris action=reaped-gone reason="session no longer live" outcome=fresh-next-msg`,
48
- )
49
- })
50
- test('age field renders as a bare token', () => {
51
- const line = formatEventLine(TS, { ev: 'supervise', identity: 'claude-x', action: 'reaped-idle', age: '4230s' })
52
- expect(line).toBe(`ts=${ISO} ev=supervise identity=claude-x action=reaped-idle age=4230s`)
53
- })
54
- })
55
-
56
- describe('appendLifecycleEvent', () => {
57
- let dir: string
58
-
59
- beforeEach(() => {
60
- dir = mkdtempSync(join(tmpdir(), 'iapeer-eventlog-'))
61
- })
62
- afterEach(() => {
63
- rmSync(dir, { recursive: true, force: true })
64
- })
65
-
66
- test('falsy logDir → no-op (a partial cfg never writes / never resolves a real path)', () => {
67
- expect(() => appendLifecycleEvent(undefined, { ev: 'supervise', identity: 'x' }, { nowMs: TS })).not.toThrow()
68
- expect(() => appendLifecycleEvent('', { ev: 'supervise', identity: 'x' }, { nowMs: TS })).not.toThrow()
69
- })
70
-
71
- test('writes one logfmt line per call, appended in order', () => {
72
- appendLifecycleEvent(dir, { ev: 'wake', personality: 'boris', mode: 'fresh', cause: 'crash-or-self-close' }, { nowMs: TS })
73
- appendLifecycleEvent(dir, { ev: 'supervise', identity: 'claude-doc', action: 'reaped-gone' }, { nowMs: TS + 1000 })
74
- const body = readFileSync(lifecycleLogPath(dir), 'utf8')
75
- const lines = body.trimEnd().split('\n')
76
- expect(lines).toHaveLength(2)
77
- expect(lines[0]).toBe(`ts=${ISO} ev=wake personality=boris mode=fresh cause=crash-or-self-close`)
78
- expect(lines[1]).toContain('ev=supervise identity=claude-doc action=reaped-gone')
79
- })
80
-
81
- test('creates the log dir if absent', () => {
82
- const nested = join(dir, 'logs', 'iapeer')
83
- appendLifecycleEvent(nested, { ev: 'supervise', identity: 'x' }, { nowMs: TS })
84
- expect(existsSync(lifecycleLogPath(nested))).toBe(true)
85
- })
86
-
87
- test('size rotation: base → .1, oldest dropped past keep', () => {
88
- const env = { IAPEER_LIFECYCLE_LOG_MAX_BYTES: '120', IAPEER_LIFECYCLE_LOG_KEEP: '2' }
89
- const path = lifecycleLogPath(dir)
90
- for (let i = 0; i < 6; i++) {
91
- appendLifecycleEvent(dir, { ev: 'supervise', identity: `claude-peer${i}`, action: 'reaped-gone', n: i }, { env, nowMs: TS + i })
92
- }
93
- expect(existsSync(path)).toBe(true)
94
- expect(existsSync(`${path}.1`)).toBe(true)
95
- expect(existsSync(`${path}.2`)).toBe(true)
96
- expect(existsSync(`${path}.3`)).toBe(false) // keep=2 → never a .3
97
- expect(statSync(path).size).toBeLessThanOrEqual(200)
98
- expect(readFileSync(path, 'utf8')).toContain('claude-peer5') // newest in the live base file
99
- })
100
-
101
- test('rotation preserves chronological order across files (.N oldest, base newest)', () => {
102
- const env = { IAPEER_LIFECYCLE_LOG_MAX_BYTES: '90', IAPEER_LIFECYCLE_LOG_KEEP: '3' }
103
- const path = lifecycleLogPath(dir)
104
- for (let i = 0; i < 4; i++) {
105
- appendLifecycleEvent(dir, { ev: 'supervise', identity: `claude-p${i}` }, { env, nowMs: TS + i })
106
- }
107
- const ordered = ['.3', '.2', '.1', '']
108
- .map(suf => (existsSync(path + suf) ? readFileSync(path + suf, 'utf8') : ''))
109
- .join('')
110
- const seen = [...ordered.matchAll(/identity=claude-p(\d)/g)].map(m => Number(m[1]))
111
- expect(seen).toEqual([...seen].sort((a, b) => a - b))
112
- expect(seen[seen.length - 1]).toBe(3) // newest line is p3, in the base file
113
- })
114
- })