@agfpd/iapeer 0.2.77 → 0.2.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/package.json +21 -2
  4. package/src/cli/index.ts +95 -78
  5. package/src/connect/index.ts +4 -4
  6. package/src/core/constants.ts +3 -3
  7. package/src/core/normalize.ts +2 -2
  8. package/src/create/index.ts +1 -1
  9. package/src/daemon/index.ts +5 -5
  10. package/src/daemon/main.ts +9 -9
  11. package/src/enable/index.ts +19 -18
  12. package/src/enable/provisionCommand.ts +10 -10
  13. package/src/identity/index.ts +2 -2
  14. package/src/identity/profileStandard.ts +8 -8
  15. package/src/init/index.ts +10 -10
  16. package/src/install/index.ts +2 -2
  17. package/src/install/signing.ts +3 -4
  18. package/src/launch/adapters/claude.ts +7 -7
  19. package/src/launch/adapters/codex.ts +5 -6
  20. package/src/launch/canary.ts +8 -8
  21. package/src/launch/cmdlog.ts +3 -3
  22. package/src/launch/composeSystemPrompt.ts +2 -1
  23. package/src/launch/index.ts +5 -6
  24. package/src/launch/invocation.ts +1 -1
  25. package/src/launch/launchd.ts +14 -15
  26. package/src/launch/launchdRun.ts +6 -6
  27. package/src/launch/nativeMemory.ts +9 -9
  28. package/src/launch/ptyHost.ts +1 -1
  29. package/src/launch/readyGateModel.ts +3 -3
  30. package/src/launch/types.ts +7 -7
  31. package/src/lifecycle/eventlog.ts +2 -2
  32. package/src/lifecycle/index.ts +21 -21
  33. package/src/lifecycle/queue.ts +1 -1
  34. package/src/onboard/index.ts +19 -13
  35. package/src/onboard/memory.ts +5 -5
  36. package/src/onboard/steps.ts +8 -9
  37. package/src/provision/index.ts +2 -3
  38. package/src/runtime/deploy.ts +2 -2
  39. package/src/runtime/index.ts +4 -4
  40. package/src/shadow/gate.ts +6 -6
  41. package/src/shadow/index.ts +2 -2
  42. package/src/status/index.ts +11 -11
  43. package/src/storage/index.ts +1 -1
  44. package/src/supervisor/daemon.ts +20 -13
  45. package/src/transport/index.ts +11 -11
  46. package/src/update/index.ts +4 -4
  47. package/src/cli/cli.test.ts +0 -603
  48. package/src/cli/listTui.test.ts +0 -71
  49. package/src/codec/codec.test.ts +0 -271
  50. package/src/connect/connect.test.ts +0 -163
  51. package/src/core/constants.test.ts +0 -32
  52. package/src/core/normalize.test.ts +0 -98
  53. package/src/create/create.test.ts +0 -143
  54. package/src/daemon/daemon-http.test.ts +0 -114
  55. package/src/daemon/daemon.test.ts +0 -267
  56. package/src/daemon/main.test.ts +0 -413
  57. package/src/enable/enable.test.ts +0 -95
  58. package/src/enable/provisionCommand.test.ts +0 -107
  59. package/src/identity/identity.test.ts +0 -279
  60. package/src/identity/profileStandard.test.ts +0 -204
  61. package/src/init/init.test.ts +0 -215
  62. package/src/init/runtime-resolve.test.ts +0 -49
  63. package/src/install/install.test.ts +0 -66
  64. package/src/install/signing.test.ts +0 -84
  65. package/src/launch/bootdeliver.test.ts +0 -108
  66. package/src/launch/bootstrap.test.ts +0 -56
  67. package/src/launch/canary.test.ts +0 -346
  68. package/src/launch/cmdlog.test.ts +0 -94
  69. package/src/launch/codexHooksTrust.test.ts +0 -248
  70. package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
  71. package/src/launch/composeSystemPrompt.test.ts +0 -145
  72. package/src/launch/hostComposerOccupied.test.ts +0 -63
  73. package/src/launch/invocation.test.ts +0 -174
  74. package/src/launch/launch.test.ts +0 -359
  75. package/src/launch/launchd.test.ts +0 -499
  76. package/src/launch/nativeMemory.test.ts +0 -193
  77. package/src/launch/ptyHost.test.ts +0 -38
  78. package/src/launch/readyGateModel.test.ts +0 -72
  79. package/src/launch/sockdir.test.ts +0 -74
  80. package/src/lifecycle/eventlog.test.ts +0 -114
  81. package/src/lifecycle/lifecycle.test.ts +0 -1056
  82. package/src/lifecycle/queue.test.ts +0 -185
  83. package/src/onboard/memory.test.ts +0 -157
  84. package/src/onboard/onboard.test.ts +0 -39
  85. package/src/onboard/steps.test.ts +0 -201
  86. package/src/provision/provision.test.ts +0 -288
  87. package/src/registry/registry.test.ts +0 -432
  88. package/src/runtime/runtime.test.ts +0 -226
  89. package/src/runtime/update.test.ts +0 -192
  90. package/src/shadow/enumerate-skip.test.ts +0 -67
  91. package/src/shadow/gate.test.ts +0 -286
  92. package/src/shadow/install.test.ts +0 -153
  93. package/src/shadow/shadow.test.ts +0 -42
  94. package/src/status/status.test.ts +0 -208
  95. package/src/storage/peers-home.test.ts +0 -34
  96. package/src/storage/rotatelog.test.ts +0 -44
  97. package/src/storage/storage.test.ts +0 -65
  98. package/src/supervisor/attached-marker.test.ts +0 -84
  99. package/src/supervisor/boot.test.ts +0 -66
  100. package/src/supervisor/client.attach.test.ts +0 -107
  101. package/src/supervisor/daemon.test.ts +0 -213
  102. package/src/supervisor/deliver.test.ts +0 -58
  103. package/src/supervisor/panelog.test.ts +0 -95
  104. package/src/supervisor/protocol.test.ts +0 -222
  105. package/src/supervisor/selfargv.test.ts +0 -22
  106. package/src/supervisor/spinner-strip.test.ts +0 -52
  107. package/src/transport/compactDone.test.ts +0 -144
  108. package/src/transport/composerQueue.test.ts +0 -204
  109. package/src/transport/hostControl.test.ts +0 -87
  110. package/src/transport/hostDeliver.test.ts +0 -137
  111. package/src/transport/hostLive.test.ts +0 -50
  112. package/src/transport/routesend-wake.test.ts +0 -357
  113. package/src/update/update.test.ts +0 -208
@@ -1,174 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { homedir } from 'os'
3
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { join, dirname } from 'node:path'
6
- import { CODEX_BEARER_ENV_VAR, CODEX_DUMMY_BEARER } from '../core/constants.ts'
7
- import { readLaunchEnv, peerLaunchEnvPath } from '../storage/index.ts'
8
- import { claudeAdapter } from './adapters/claude.ts'
9
- import { codexAdapter } from './adapters/codex.ts'
10
- import { buildLaunchInvocation } from './invocation.ts'
11
- import type { LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from './types.ts'
12
-
13
- // NON-CIRCULAR GOLDEN (boris's bar): the ORACLE is the PRE-extraction inline build copied VERBATIM
14
- // from launch/index.ts @595b0f4 — the source of truth is the OLD code, NOT buildLaunchInvocation.
15
- // The differential test asserts the extracted function reproduces the oracle byte-for-byte; the
16
- // independent contract assertions (hardcoded from the contract, not from the function's output) catch
17
- // the case where oracle and function are wrong together. Two anchors: differential catches a
18
- // reorganization drift, contract catches a shared error.
19
- function oracleInline(
20
- spec: LaunchSpec,
21
- adapter: RuntimeAdapter,
22
- cfg: LaunchAdapterConfig & { env?: NodeJS.ProcessEnv },
23
- ): { argv: string[]; env: NodeJS.ProcessEnv } {
24
- const env = cfg.env ?? process.env
25
- const cwd = spec.cwd
26
- const identity = spec.identity
27
- const launchEnv = readLaunchEnv(cwd, spec.runtime)
28
- const specWithArgs: LaunchSpec =
29
- launchEnv.startArgs.length > 0
30
- ? { ...spec, extraArgs: [...(spec.extraArgs ?? []), ...launchEnv.startArgs] }
31
- : spec
32
- const runtimeArgv = adapter.buildArgv(specWithArgs, cfg)
33
- const childEnv: NodeJS.ProcessEnv = {
34
- ...env,
35
- ...launchEnv.env,
36
- ...(spec.runtime === 'codex' ? { [CODEX_BEARER_ENV_VAR]: CODEX_DUMMY_BEARER } : {}),
37
- PEER_PERSONALITY: spec.personality,
38
- PEER_RUNTIME: spec.runtime,
39
- PEER_IDENTITY: identity,
40
- PATH: env.PATH ?? `${homedir()}/.bun/bin:${homedir()}/.local/bin:/opt/homebrew/bin:/usr/bin:/bin`,
41
- }
42
- return { argv: runtimeArgv, env: childEnv }
43
- }
44
-
45
- const CFG: LaunchAdapterConfig & { env: NodeJS.ProcessEnv } = {
46
- claudeBin: '/bin/claude',
47
- codexBin: '/bin/codex',
48
- env: { PATH: '/test/path', BASE_VAR: 'base' },
49
- }
50
-
51
- function spec(over: Partial<LaunchSpec>): LaunchSpec {
52
- return {
53
- personality: 'p',
54
- runtime: 'claude',
55
- cwd: '/nonexistent-cwd-no-launchenv',
56
- identity: 'claude-p',
57
- socketPath: '/tmp/s.sock',
58
- extraArgs: [],
59
- ...over,
60
- }
61
- }
62
-
63
- const adapterFor = (s: LaunchSpec): RuntimeAdapter => (s.runtime === 'codex' ? codexAdapter : claudeAdapter)
64
-
65
- // Representative specs: both runtimes × {bare, with system-prompt, with resume + extraArgs}.
66
- const SPECS: Array<{ name: string; spec: LaunchSpec }> = [
67
- { name: 'claude bare', spec: spec({ runtime: 'claude', identity: 'claude-p' }) },
68
- {
69
- name: 'claude + system-prompt + resume + extras',
70
- spec: spec({ runtime: 'claude', identity: 'claude-p', systemPromptFile: '/sp.md', resume: true, resumeRef: 'uuid-9', extraArgs: ['--foo'] }),
71
- },
72
- { name: 'codex bare', spec: spec({ runtime: 'codex', identity: 'codex-p' }) },
73
- {
74
- name: 'codex + system-prompt + resume',
75
- spec: spec({ runtime: 'codex', identity: 'codex-p', systemPromptFile: '/sp.md', resume: true }),
76
- },
77
- ]
78
-
79
- describe('buildLaunchInvocation — differential vs the verbatim pre-extraction oracle', () => {
80
- for (const { name, spec: s } of SPECS) {
81
- test(`byte-identical to the inline build: ${name}`, () => {
82
- const got = buildLaunchInvocation(s, adapterFor(s), CFG)
83
- const want = oracleInline(s, adapterFor(s), CFG)
84
- expect(got.argv).toEqual(want.argv)
85
- expect(got.env).toEqual(want.env)
86
- })
87
- }
88
-
89
- test('the launch.env merge branch (non-empty startArgs + extra env) is byte-identical too', () => {
90
- const cwd = mkdtempSync(join(tmpdir(), 'inv-launchenv-'))
91
- try {
92
- const p = peerLaunchEnvPath(cwd, 'codex')
93
- mkdirSync(dirname(p), { recursive: true })
94
- writeFileSync(p, 'PEER_START_ARGS=--from-env-a --from-env-b\nEXTRA_VAR=xyz\n')
95
- const s = spec({ runtime: 'codex', identity: 'codex-p', cwd, extraArgs: ['--explicit'] })
96
- const got = buildLaunchInvocation(s, codexAdapter, CFG)
97
- const want = oracleInline(s, codexAdapter, CFG)
98
- expect(got.argv).toEqual(want.argv)
99
- expect(got.env).toEqual(want.env)
100
- // sanity on the branch itself: explicit extras precede launch.env startArgs, both after base flags
101
- expect(got.argv).toContain('--explicit')
102
- expect(got.argv).toContain('--from-env-a')
103
- expect(got.env.EXTRA_VAR).toBe('xyz')
104
- } finally {
105
- rmSync(cwd, { recursive: true, force: true })
106
- }
107
- })
108
- })
109
-
110
- // Silent-loss guard — the claude-hosting canary caught a warm-deliver false-fail: a launch INITIATED
111
- // from inside a claude session leaked CLAUDE_CODE_SESSION_ID + CLAUDE_CODE_CHILD_SESSION into the
112
- // child, which then wrote no own transcript → newestActivityMtime null → landed-confirm blind while
113
- // the message landed. buildLaunchInvocation must strip the claude-code-internal namespace so a
114
- // launched claude is always a FRESH top-level session, regardless of initiator env.
115
- describe('buildLaunchInvocation — strips the claude-code-internal session namespace', () => {
116
- const DIRTY: LaunchAdapterConfig & { env: NodeJS.ProcessEnv } = {
117
- claudeBin: '/bin/claude',
118
- codexBin: '/bin/codex',
119
- env: {
120
- PATH: '/test/path',
121
- BASE_VAR: 'base',
122
- CLAUDE_CODE_SESSION_ID: 'spawner-session-uuid', // the corruptor (pins the child to the spawner's session)
123
- CLAUDE_CODE_CHILD_SESSION: '1', // the corruptor (marks a child sub-session → suppresses own transcript)
124
- CLAUDECODE: '1',
125
- CLAUDE_CODE_ENTRYPOINT: 'cli',
126
- CLAUDE_CODE_EXECPATH: '/v/2.1.177',
127
- CLAUDE_EFFORT: 'xhigh', // NOT in the claude-code namespace → must survive (scoped strip, not a wipe)
128
- },
129
- }
130
- test('claude launch: the whole CLAUDE_CODE_* namespace + CLAUDECODE is removed', () => {
131
- const { env } = buildLaunchInvocation(spec({ runtime: 'claude', identity: 'claude-p' }), claudeAdapter, DIRTY)
132
- expect(env.CLAUDE_CODE_SESSION_ID).toBeUndefined()
133
- expect(env.CLAUDE_CODE_CHILD_SESSION).toBeUndefined()
134
- expect(env.CLAUDECODE).toBeUndefined()
135
- expect(env.CLAUDE_CODE_ENTRYPOINT).toBeUndefined()
136
- expect(env.CLAUDE_CODE_EXECPATH).toBeUndefined()
137
- // scoped: non-namespace vars (incl. a CLAUDE_-prefixed-but-not-CLAUDE_CODE_ one) survive
138
- expect(env.BASE_VAR).toBe('base')
139
- expect(env.CLAUDE_EFFORT).toBe('xhigh')
140
- expect(env.PEER_IDENTITY).toBe('claude-p')
141
- })
142
- test('codex launch: same strip applies (harmless — codex ignores CLAUDE_*) and the bearer stub stays', () => {
143
- const { env } = buildLaunchInvocation(spec({ runtime: 'codex', identity: 'codex-p' }), codexAdapter, DIRTY)
144
- expect(env.CLAUDE_CODE_SESSION_ID).toBeUndefined()
145
- expect(env.CLAUDECODE).toBeUndefined()
146
- expect(env[CODEX_BEARER_ENV_VAR]).toBe(CODEX_DUMMY_BEARER)
147
- })
148
- })
149
-
150
- // Independent contract anchors — expectations come from the CONTRACT, not from the function output.
151
- describe('buildLaunchInvocation — identity ABI + system-prompt swap (contract values)', () => {
152
- test('codex carries the non-secret IAPEER_BEARER stub; claude does NOT (identity in .mcp.json)', () => {
153
- expect(buildLaunchInvocation(spec({ runtime: 'codex', identity: 'codex-p' }), codexAdapter, CFG).env[CODEX_BEARER_ENV_VAR]).toBe(
154
- CODEX_DUMMY_BEARER,
155
- )
156
- expect(
157
- buildLaunchInvocation(spec({ runtime: 'claude', identity: 'claude-p' }), claudeAdapter, CFG).env[CODEX_BEARER_ENV_VAR],
158
- ).toBeUndefined()
159
- })
160
-
161
- test('the identity ABI is present and matches the spec', () => {
162
- const { env } = buildLaunchInvocation(spec({ runtime: 'codex', personality: 'boris', identity: 'codex-boris' }), codexAdapter, CFG)
163
- expect(env.PEER_PERSONALITY).toBe('boris')
164
- expect(env.PEER_RUNTIME).toBe('codex')
165
- expect(env.PEER_IDENTITY).toBe('codex-boris')
166
- })
167
-
168
- test('the per-session system-prompt swap rides in argv (codex -c, claude --system-prompt-file)', () => {
169
- const codexArgv = buildLaunchInvocation(spec({ runtime: 'codex', identity: 'codex-p', systemPromptFile: '/sp.md' }), codexAdapter, CFG).argv
170
- expect(codexArgv).toContain('model_instructions_file=/sp.md')
171
- const claudeArgv = buildLaunchInvocation(spec({ runtime: 'claude', identity: 'claude-p', systemPromptFile: '/sp.md' }), claudeAdapter, CFG).argv
172
- expect(claudeArgv.join(' ')).toContain('--system-prompt-file /sp.md')
173
- })
174
- })
@@ -1,359 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { exitCauseHook, exitLogPath, getAdapter, launch, paneTail } from './index.ts'
3
- import { claudeAdapter } from './adapters/claude.ts'
4
- import { codexAdapter } from './adapters/codex.ts'
5
- import { telegramAdapter } from './adapters/telegram.ts'
6
- import { notifierAdapter } from './adapters/notifier.ts'
7
- import type { LaunchAdapterConfig, LaunchConfig, LaunchSpec } from './types.ts'
8
-
9
- const cfg: LaunchAdapterConfig = { claudeBin: '/bin/claude', codexBin: 'codex' }
10
- // Full LaunchConfig for the gate tests — the intelligence gate returns FAILED at
11
- // step 0, BEFORE any tmux/FS work, so these values are never exercised.
12
- const launchCfg: LaunchConfig = {
13
- claudeBin: '/bin/claude',
14
- codexBin: 'codex',
15
- sockDir: '/tmp',
16
- bootDeadlineSecs: 1,
17
- readyGateSecs: 1,
18
- logDir: '/tmp/iapeer-test-logs',
19
- }
20
- function spec(over: Partial<LaunchSpec> = {}): LaunchSpec {
21
- return {
22
- personality: 'p',
23
- runtime: 'claude',
24
- cwd: '/tmp/p',
25
- identity: 'claude-p',
26
- socketPath: '/tmp/tmux-iap-claude-p.sock',
27
- ...over,
28
- }
29
- }
30
-
31
- describe('getAdapter', () => {
32
- test('dispatches each runtime to its adapter', () => {
33
- expect(getAdapter('claude')).toBe(claudeAdapter)
34
- expect(getAdapter('codex')).toBe(codexAdapter)
35
- expect(getAdapter('telegram')).toBe(telegramAdapter)
36
- expect(getAdapter('notifier')).toBe(notifierAdapter)
37
- })
38
- test('codex is tui+doctrine, telegram is router+no-doctrine', () => {
39
- expect(codexAdapter.kind).toBe('tui')
40
- expect(codexAdapter.usesDoctrine).toBe(true)
41
- expect(telegramAdapter.kind).toBe('router')
42
- expect(telegramAdapter.usesDoctrine).toBe(false)
43
- })
44
- test('unknown runtime throws', () => {
45
- expect(() => getAdapter('webhook')).toThrow()
46
- })
47
- })
48
-
49
- describe('codexAdapter.buildArgv', () => {
50
- test('bare (no resume, no doctrine): --no-alt-screen -C cwd --dangerously-bypass', () => {
51
- expect(codexAdapter.buildArgv(spec({ runtime: 'codex', cwd: '/w' }), cfg)).toEqual([
52
- 'codex',
53
- '--no-alt-screen',
54
- '-C',
55
- '/w',
56
- '--dangerously-bypass-approvals-and-sandbox',
57
- ])
58
- })
59
- test('with resume + model_instructions_file (exact order)', () => {
60
- expect(
61
- codexAdapter.buildArgv(spec({ runtime: 'codex', cwd: '/w', resume: true, systemPromptFile: '/sp.md' }), cfg),
62
- ).toEqual([
63
- 'codex',
64
- 'resume',
65
- '--last',
66
- '--no-alt-screen',
67
- '-C',
68
- '/w',
69
- '-c',
70
- 'model_instructions_file=/sp.md',
71
- '--dangerously-bypass-approvals-and-sandbox',
72
- ])
73
- })
74
- })
75
-
76
- describe('codexAdapter.isInputReady', () => {
77
- test('fresh boot: splash + composer → true', () => {
78
- const pane = [
79
- '╭───────────────────────────────╮',
80
- '│ >_ OpenAI Codex (v0.138.0) │',
81
- '╰───────────────────────────────╯',
82
- '› Explain this codebase',
83
- 'gpt-5.5 xhigh · ~/Peers/p',
84
- ].join('\n')
85
- expect(codexAdapter.isInputReady(pane)).toBe(true)
86
- })
87
-
88
- test('resume replay scrolled the splash off — composer alone is READY (12.06 incident)', () => {
89
- // The live zombie pane shape: history tail + MCP noise + composer, NO
90
- // 'OpenAI Codex' anywhere in the visible capture. The old predicate required
91
- // the splash and spun false for the whole 240 s boot deadline.
92
- const pane = [
93
- ' <message><![CDATA[Компакт прошел?]]></message>',
94
- ' </iap>',
95
- '• Принял. Ответ пиру не слал: он явно написал, что больше ничего не нужно.',
96
- '⚠ MCP startup incomplete (failed: iap)',
97
- '› Explain this codebase',
98
- 'gpt-5.5 xhigh · ~/Peers/p',
99
- ].join('\n')
100
- expect(pane.includes('OpenAI Codex')).toBe(false) // fixture honesty
101
- expect(codexAdapter.isInputReady(pane)).toBe(true)
102
- })
103
-
104
- test('update screen up → false even with a composer-looking line', () => {
105
- const pane = ['Update available!', 'Press enter to continue', '› something'].join('\n')
106
- expect(codexAdapter.isInputReady(pane)).toBe(false)
107
- })
108
-
109
- test('no composer row yet (box only, model loading) → false', () => {
110
- const pane = ['│ >_ OpenAI Codex (v0.138.0) │', '│ model: loading │'].join('\n')
111
- expect(codexAdapter.isInputReady(pane)).toBe(false)
112
- })
113
-
114
- test('bare › without trailing space (a stray glyph) → false', () => {
115
- expect(codexAdapter.isInputReady('›\nnothing else')).toBe(false)
116
- })
117
- })
118
-
119
- describe('paneTail (never-became-ready observability)', () => {
120
- test('last two non-empty lines, joined, quotes flattened', () => {
121
- const pane = 'first\n\n middle line \n⚠ MCP "failed"\n› composer\n\n'
122
- expect(paneTail(pane)).toBe("⚠ MCP 'failed' ⏎ › composer")
123
- })
124
- test('single line and empty captures', () => {
125
- expect(paneTail('only')).toBe('only')
126
- expect(paneTail('')).toBe('')
127
- expect(paneTail('\n \n')).toBe('')
128
- })
129
- test('clips to 160 chars', () => {
130
- expect(paneTail('x'.repeat(300)).length).toBe(160)
131
- })
132
- })
133
-
134
- describe('telegramAdapter (router — no TUI surface)', () => {
135
- test('router predicates are trivial', () => {
136
- expect(telegramAdapter.bootDialogKeys('anything')).toBeNull()
137
- expect(telegramAdapter.isInputReady('anything')).toBe(true)
138
- expect(telegramAdapter.newestActivityMtime('/w')).toBeNull()
139
- })
140
- })
141
-
142
- describe('notifierAdapter (router — infra/always-on)', () => {
143
- test('kind:router, no doctrine', () => {
144
- expect(notifierAdapter.runtime).toBe('notifier')
145
- expect(notifierAdapter.kind).toBe('router')
146
- expect(notifierAdapter.usesDoctrine).toBe(false)
147
- })
148
- test('buildArgv = notifier-runtime run [+extra], default bin', () => {
149
- expect(notifierAdapter.buildArgv(spec({ runtime: 'notifier' }), cfg)).toEqual(['notifier-runtime', 'run'])
150
- expect(
151
- notifierAdapter.buildArgv(spec({ runtime: 'notifier', extraArgs: ['--foo'] }), { ...cfg, notifierBin: '/n/bin' }),
152
- ).toEqual(['/n/bin', 'run', '--foo'])
153
- })
154
- test('router predicates are trivial (no TUI surface)', () => {
155
- expect(notifierAdapter.bootDialogKeys('anything')).toBeNull()
156
- expect(notifierAdapter.isInputReady('anything')).toBe(true)
157
- expect(notifierAdapter.newestActivityMtime('/w')).toBeNull()
158
- expect(notifierAdapter.resolveResume('/w')).toEqual({ ok: true })
159
- })
160
- })
161
-
162
- describe('claudeAdapter', () => {
163
- test('kind/usesDoctrine', () => {
164
- expect(claudeAdapter.kind).toBe('tui')
165
- expect(claudeAdapter.usesDoctrine).toBe(true)
166
- })
167
-
168
- test('buildArgv bare (no system-prompt, no resume)', () => {
169
- expect(claudeAdapter.buildArgv(spec(), cfg)).toEqual([
170
- '/bin/claude',
171
- '--dangerously-skip-permissions',
172
- '--disallowedTools',
173
- 'AskUserQuestion',
174
- ])
175
- })
176
-
177
- test('buildArgv with system-prompt-file + resume + extras → --continue (NOT --resume <uuid>)', () => {
178
- // resume uses `--continue` (continue the cwd's most-recent session), NOT
179
- // `--resume <uuid>` — in claude 2.1.169 `--resume <arg>` is a search query, not a
180
- // session-id. resumeRef is set by the daemon but no longer consumed by the launch.
181
- const argv = claudeAdapter.buildArgv(
182
- spec({ systemPromptFile: '/tmp/sp.md', resume: true, resumeRef: 'uuid-1', extraArgs: ['--foo'] }),
183
- cfg,
184
- )
185
- expect(argv).toEqual([
186
- '/bin/claude',
187
- '--dangerously-skip-permissions',
188
- '--disallowedTools',
189
- 'AskUserQuestion',
190
- '--system-prompt-file',
191
- '/tmp/sp.md',
192
- '--continue',
193
- '--foo',
194
- ])
195
- expect(argv).not.toContain('--resume')
196
- expect(argv).not.toContain('uuid-1')
197
- })
198
-
199
- test('buildArgv: resume FALSE (fresh) → no --continue / --resume', () => {
200
- const argv = claudeAdapter.buildArgv(spec({ resume: false, resumeRef: 'uuid-1' }), cfg)
201
- expect(argv).not.toContain('--continue')
202
- expect(argv).not.toContain('--resume')
203
- })
204
-
205
- test('buildArgv carries NO currency (no marketplace/plugin tokens)', () => {
206
- const flat = claudeAdapter.buildArgv(spec({ systemPromptFile: '/x' }), cfg).join(' ')
207
- expect(flat).not.toMatch(/marketplace|plugin (install|update|marketplace)/)
208
- })
209
-
210
- test('isInputReady: ❯ + bypass banner, dialogs gone → true', () => {
211
- expect(claudeAdapter.isInputReady('… ❯ …\nbypass permissions on')).toBe(true)
212
- })
213
- test('isInputReady: a boot dialog present → false even with ❯', () => {
214
- expect(claudeAdapter.isInputReady('trust this folder\n❯ bypass permissions on')).toBe(false)
215
- })
216
- test('isInputReady: no bypass banner → false', () => {
217
- expect(claudeAdapter.isInputReady('❯ just a prompt')).toBe(false)
218
- })
219
-
220
- test('bootDialogKeys: proceed-modals → [Enter]; clean/load pane → null', () => {
221
- expect(claudeAdapter.bootDialogKeys('I am using this for local development')).toEqual(['Enter'])
222
- expect(claudeAdapter.bootDialogKeys('trust this folder')).toEqual(['Enter'])
223
- expect(claudeAdapter.bootDialogKeys('Allow external CLAUDE.md file imports?')).toEqual(['Enter'])
224
- // the post-select "Resuming…" load state is NOT a modal to Enter — just wait.
225
- expect(claudeAdapter.bootDialogKeys('Resuming the full session')).toBeNull()
226
- expect(claudeAdapter.bootDialogKeys('❯ ready')).toBeNull()
227
- })
228
-
229
- test('bootDialogKeys: resume compact-picker is CURSOR-VERIFIED — Enter ONLY after ❯ is seen on "2. full"', () => {
230
- // Regression (boris 10.06, 313k resume): the blind ['Down','Enter'] burst lost the
231
- // Down in one pty chunk and Enter confirmed the DEFAULT "1. Resume from summary"
232
- // → silent /compact. Owner's invariant: NO compact on resume. One key per boot
233
- // iteration; confirm only on a PROVEN cursor position.
234
- const cursorOn1 = '❯ 1. Resume from summary (recommended)\n 2. Resume full session as-is'
235
- const cursorOn2 = ' 1. Resume from summary (recommended)\n❯ 2. Resume full session as-is'
236
- // cursor on the compacting default → step down, do NOT confirm
237
- expect(claudeAdapter.bootDialogKeys(cursorOn1)).toEqual(['Down'])
238
- // a swallowed Down self-heals: the unchanged pane just gets another Down
239
- expect(claudeAdapter.bootDialogKeys(cursorOn1)).toEqual(['Down'])
240
- // cursor PROVEN on "2. Resume full session" → now (and only now) confirm
241
- expect(claudeAdapter.bootDialogKeys(cursorOn2)).toEqual(['Enter'])
242
- // Enter can never reach the compacting default: option-1 cursor never maps to Enter
243
- expect(claudeAdapter.bootDialogKeys(cursorOn1)).not.toContain('Enter')
244
- // the 2.1.170 live layout (3rd item present) behaves the same
245
- const live170 =
246
- "This session is 7h 3m old and 314.2k tokens.\n❯ 1. Resume from summary (recommended)\n 2. Resume full session as-is\n 3. Don't ask me again\nEnter to confirm · Esc to cancel"
247
- expect(claudeAdapter.bootDialogKeys(live170)).toEqual(['Down'])
248
- })
249
- })
250
-
251
- // ─── exit-cause observability: the pane-died hook builder (pure string) ──────
252
- describe('exitCauseHook (exit-cause observability)', () => {
253
- const hook = exitCauseHook('claude-iapeer', '/r/logs/iapeer/exits.log')
254
-
255
- test('exitLogPath → exits.log sibling to lifecycle.log', () => {
256
- expect(exitLogPath('/r/logs/iapeer')).toBe('/r/logs/iapeer/exits.log')
257
- })
258
- test('reads pane_dead_status AND pane_dead_signal (both death classes)', () => {
259
- // graceful exit populates #{pane_dead_status}; a signal populates #{pane_dead_signal}.
260
- expect(hook).toContain('dead_status=#{pane_dead_status}')
261
- expect(hook).toContain('dead_signal=#{pane_dead_signal}')
262
- })
263
- test('one logfmt line: ts + ev=session-exit + identity, appended to the exit log', () => {
264
- expect(hook).toContain('ev=session-exit')
265
- expect(hook).toContain('identity=claude-iapeer')
266
- expect(hook).toContain('ts=%s')
267
- expect(hook).toContain('>> "/r/logs/iapeer/exits.log"')
268
- expect(hook).toContain('\\n') // literal backslash-n for sh printf, not a real newline
269
- })
270
- test('logs BEFORE it reaps: run-shell (sync, no -b) then tmux-native kill-session', () => {
271
- // run-shell must NOT be backgrounded (-b) — the printf has to finish before the
272
- // kill tears the server down, else the line is lost to the race (verified live).
273
- expect(hook).not.toContain('run-shell -b')
274
- expect(hook.indexOf('run-shell')).toBeLessThan(hook.indexOf('kill-session'))
275
- // tmux-NATIVE kill-session (no shell `tmux`) → needs no PATH (launchd minimal env).
276
- expect(hook).toContain('kill-session -t "claude-iapeer"')
277
- })
278
- test('silences the server-death canary (native wait-for -S) BEFORE kill-session — no double record', () => {
279
- // kill-session on a single-session server → exit-empty takes the server down;
280
- // without the signal the canary would add a second `ev=server-exit` record for
281
- // a death this hook just captured (session-exit carries the code/signal).
282
- expect(hook).toContain('wait-for -S "iap-canary-claude-iapeer"')
283
- expect(hook.indexOf('wait-for -S')).toBeLessThan(hook.indexOf('kill-session'))
284
- expect(hook.indexOf('run-shell')).toBeLessThan(hook.indexOf('wait-for -S')) // log first
285
- })
286
- test('quoting: single-quoted run-shell arg (tmux layer) wrapping double-quoted sh', () => {
287
- expect(hook).toMatch(/run-shell '.*'/)
288
- expect(hook).not.toContain("''") // no empty/again-collapsed single-quote pair
289
- })
290
- })
291
-
292
- // ─── Ф-A #2: deliveryMarkers OWNED by the adapter (07.06 refactor) ───────────
293
- describe('deliveryMarkers (adapter-owned, was transport PROMPT_GLYPHS)', () => {
294
- test('claude: ❯ glyph + paste patterns', () => {
295
- expect(claudeAdapter.deliveryMarkers.promptGlyphs).toEqual(['❯'])
296
- expect(claudeAdapter.deliveryMarkers.pastePatterns?.some(re => re.test('[Pasted text +5 lines]'))).toBe(true)
297
- expect(claudeAdapter.deliveryMarkers.ghostTextSgr).toEqual(['2', '38;5;246'])
298
- })
299
- test('codex: › glyph (not ❯) — per-runtime, no false cross-match', () => {
300
- expect(codexAdapter.deliveryMarkers.promptGlyphs).toEqual(['›'])
301
- expect(codexAdapter.deliveryMarkers.promptGlyphs).not.toContain('❯')
302
- expect(codexAdapter.deliveryMarkers.ghostTextSgr).toEqual(['2', '38;5;246'])
303
- })
304
- test('routers have no submit surface → empty glyphs', () => {
305
- expect(telegramAdapter.deliveryMarkers.promptGlyphs).toEqual([])
306
- expect(notifierAdapter.deliveryMarkers.promptGlyphs).toEqual([])
307
- })
308
- })
309
-
310
- // ─── Ф-A #3: intelligence gate (telegram launch requires natural) ────────────
311
- describe('launch intelligence gate (adapter.requiresIntelligence)', () => {
312
- test('telegram declares requiresIntelligence=natural; tui runtimes declare none', () => {
313
- expect(telegramAdapter.requiresIntelligence).toBe('natural')
314
- expect(claudeAdapter.requiresIntelligence).toBeUndefined()
315
- expect(codexAdapter.requiresIntelligence).toBeUndefined()
316
- expect(notifierAdapter.requiresIntelligence).toBeUndefined()
317
- })
318
-
319
- test('launch REFUSES a non-natural peer on telegram (fail-loud, before any tmux)', async () => {
320
- const r = await launch(
321
- spec({ runtime: 'telegram', identity: 'telegram-bot', socketPath: '/tmp/tmux-iap-telegram-bot.sock', intelligence: 'artificial' }),
322
- telegramAdapter,
323
- 'first',
324
- launchCfg,
325
- )
326
- expect(r.status).toBe('FAILED')
327
- expect(r.reason).toMatch(/requires intelligence=natural/)
328
- })
329
-
330
- test('launch REFUSES when intelligence is unknown (cannot confirm natural)', async () => {
331
- const r = await launch(
332
- spec({ runtime: 'telegram', identity: 'telegram-bot', socketPath: '/tmp/tmux-iap-telegram-bot.sock' }),
333
- telegramAdapter,
334
- 'first',
335
- launchCfg,
336
- )
337
- expect(r.status).toBe('FAILED')
338
- expect(r.reason).toMatch(/unknown/)
339
- })
340
- })
341
-
342
- // ─── Ф-E #control: executeControl (adapter-owned in-session control mapping) ──
343
- describe('executeControl (Ф-E control commands)', () => {
344
- test('claude: interrupt → [Escape]; compact → type /compact then Enter', () => {
345
- expect(claudeAdapter.executeControl({ name: 'interrupt' })).toEqual({ sequence: [['Escape']] })
346
- expect(claudeAdapter.executeControl({ name: 'compact' })).toEqual({ sequence: [['-l', '/compact'], ['Enter']], stepDelayMs: 300 })
347
- expect(claudeAdapter.executeControl({ name: 'bogus' })).toBeNull()
348
- })
349
- test('codex: interrupt → [Escape] (×1, snapped live); compact → /compact + Enter (snapped live 12.06, codex-cli 0.138)', () => {
350
- expect(codexAdapter.executeControl({ name: 'interrupt' })).toEqual({ sequence: [['Escape']] })
351
- expect(codexAdapter.executeControl({ name: 'compact' })).toEqual({ sequence: [['-l', '/compact'], ['Enter']], stepDelayMs: 300 })
352
- expect(codexAdapter.executeControl({ name: 'bogus' })).toBeNull()
353
- })
354
- test('routers (telegram/notifier) refuse all control (no TUI turn)', () => {
355
- expect(telegramAdapter.executeControl({ name: 'interrupt' })).toBeNull()
356
- expect(notifierAdapter.executeControl({ name: 'interrupt' })).toBeNull()
357
- expect(telegramAdapter.executeControl({ name: 'compact' })).toBeNull()
358
- })
359
- })