@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.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/package.json +21 -2
- package/src/cli/index.ts +47 -48
- package/src/connect/index.ts +4 -4
- package/src/core/constants.ts +3 -3
- package/src/core/normalize.ts +2 -2
- package/src/create/index.ts +1 -1
- package/src/daemon/index.ts +5 -5
- package/src/daemon/main.ts +9 -9
- package/src/enable/index.ts +19 -18
- package/src/enable/provisionCommand.ts +10 -10
- package/src/identity/index.ts +2 -2
- package/src/identity/profileStandard.ts +8 -8
- package/src/init/index.ts +10 -10
- package/src/install/index.ts +2 -2
- package/src/install/signing.ts +3 -4
- package/src/launch/adapters/claude.ts +7 -7
- package/src/launch/adapters/codex.ts +5 -6
- package/src/launch/canary.ts +8 -8
- package/src/launch/cmdlog.ts +3 -3
- package/src/launch/composeSystemPrompt.ts +2 -1
- package/src/launch/index.ts +5 -6
- package/src/launch/invocation.ts +1 -1
- package/src/launch/launchd.ts +14 -15
- package/src/launch/launchdRun.ts +6 -6
- package/src/launch/nativeMemory.ts +9 -9
- package/src/launch/ptyHost.ts +1 -1
- package/src/launch/readyGateModel.ts +3 -3
- package/src/launch/types.ts +7 -7
- package/src/lifecycle/eventlog.ts +2 -2
- package/src/lifecycle/index.ts +21 -21
- package/src/lifecycle/queue.ts +1 -1
- package/src/onboard/index.ts +19 -13
- package/src/onboard/memory.ts +5 -5
- package/src/onboard/steps.ts +8 -9
- package/src/provision/index.ts +2 -3
- package/src/runtime/deploy.ts +2 -2
- package/src/runtime/index.ts +4 -4
- package/src/shadow/gate.ts +6 -6
- package/src/shadow/index.ts +2 -2
- package/src/status/index.ts +11 -11
- package/src/storage/index.ts +1 -1
- package/src/supervisor/daemon.ts +14 -8
- package/src/transport/index.ts +11 -11
- package/src/update/index.ts +4 -4
- package/src/cli/cli.test.ts +0 -603
- package/src/cli/listTui.test.ts +0 -71
- package/src/codec/codec.test.ts +0 -271
- package/src/connect/connect.test.ts +0 -163
- package/src/core/constants.test.ts +0 -32
- package/src/core/normalize.test.ts +0 -98
- package/src/create/create.test.ts +0 -143
- package/src/daemon/daemon-http.test.ts +0 -114
- package/src/daemon/daemon.test.ts +0 -267
- package/src/daemon/main.test.ts +0 -413
- package/src/enable/enable.test.ts +0 -95
- package/src/enable/provisionCommand.test.ts +0 -107
- package/src/identity/identity.test.ts +0 -279
- package/src/identity/profileStandard.test.ts +0 -204
- package/src/init/init.test.ts +0 -215
- package/src/init/runtime-resolve.test.ts +0 -49
- package/src/install/install.test.ts +0 -66
- package/src/install/signing.test.ts +0 -84
- package/src/launch/bootdeliver.test.ts +0 -108
- package/src/launch/bootstrap.test.ts +0 -56
- package/src/launch/canary.test.ts +0 -346
- package/src/launch/cmdlog.test.ts +0 -94
- package/src/launch/codexHooksTrust.test.ts +0 -248
- package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
- package/src/launch/composeSystemPrompt.test.ts +0 -145
- package/src/launch/hostComposerOccupied.test.ts +0 -63
- package/src/launch/invocation.test.ts +0 -174
- package/src/launch/launch.test.ts +0 -359
- package/src/launch/launchd.test.ts +0 -499
- package/src/launch/nativeMemory.test.ts +0 -193
- package/src/launch/ptyHost.test.ts +0 -38
- package/src/launch/readyGateModel.test.ts +0 -72
- package/src/launch/sockdir.test.ts +0 -74
- package/src/lifecycle/eventlog.test.ts +0 -114
- package/src/lifecycle/lifecycle.test.ts +0 -1056
- package/src/lifecycle/queue.test.ts +0 -185
- package/src/onboard/memory.test.ts +0 -157
- package/src/onboard/onboard.test.ts +0 -39
- package/src/onboard/steps.test.ts +0 -201
- package/src/provision/provision.test.ts +0 -288
- package/src/registry/registry.test.ts +0 -432
- package/src/runtime/runtime.test.ts +0 -226
- package/src/runtime/update.test.ts +0 -192
- package/src/shadow/enumerate-skip.test.ts +0 -67
- package/src/shadow/gate.test.ts +0 -286
- package/src/shadow/install.test.ts +0 -153
- package/src/shadow/shadow.test.ts +0 -42
- package/src/status/status.test.ts +0 -208
- package/src/storage/peers-home.test.ts +0 -34
- package/src/storage/rotatelog.test.ts +0 -44
- package/src/storage/storage.test.ts +0 -65
- package/src/supervisor/attached-marker.test.ts +0 -84
- package/src/supervisor/boot.test.ts +0 -66
- package/src/supervisor/client.attach.test.ts +0 -107
- package/src/supervisor/daemon.test.ts +0 -213
- package/src/supervisor/deliver.test.ts +0 -58
- package/src/supervisor/panelog.test.ts +0 -95
- package/src/supervisor/protocol.test.ts +0 -222
- package/src/supervisor/selfargv.test.ts +0 -22
- package/src/supervisor/spinner-strip.test.ts +0 -52
- package/src/transport/compactDone.test.ts +0 -144
- package/src/transport/composerQueue.test.ts +0 -204
- package/src/transport/hostControl.test.ts +0 -87
- package/src/transport/hostDeliver.test.ts +0 -137
- package/src/transport/hostLive.test.ts +0 -50
- package/src/transport/routesend-wake.test.ts +0 -357
- package/src/update/update.test.ts +0 -208
|
@@ -1,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
|
-
})
|