@agfpd/iapeer 0.2.76 → 0.2.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/package.json +21 -2
- package/src/cli/index.ts +47 -48
- package/src/connect/index.ts +4 -4
- package/src/core/constants.ts +3 -3
- package/src/core/normalize.ts +2 -2
- package/src/create/index.ts +1 -1
- package/src/daemon/index.ts +5 -5
- package/src/daemon/main.ts +9 -9
- package/src/enable/index.ts +19 -18
- package/src/enable/provisionCommand.ts +10 -10
- package/src/identity/index.ts +2 -2
- package/src/identity/profileStandard.ts +8 -8
- package/src/init/index.ts +20 -13
- package/src/install/index.ts +2 -2
- package/src/install/signing.ts +3 -4
- package/src/launch/adapters/claude.ts +7 -7
- package/src/launch/adapters/codex.ts +5 -6
- package/src/launch/canary.ts +8 -8
- package/src/launch/cmdlog.ts +3 -3
- package/src/launch/composeSystemPrompt.ts +2 -1
- package/src/launch/index.ts +5 -6
- package/src/launch/invocation.ts +1 -1
- package/src/launch/launchd.ts +14 -15
- package/src/launch/launchdRun.ts +6 -6
- package/src/launch/nativeMemory.ts +9 -9
- package/src/launch/ptyHost.ts +1 -1
- package/src/launch/readyGateModel.ts +3 -3
- package/src/launch/types.ts +7 -7
- package/src/lifecycle/eventlog.ts +2 -2
- package/src/lifecycle/index.ts +21 -21
- package/src/lifecycle/queue.ts +1 -1
- package/src/onboard/index.ts +19 -13
- package/src/onboard/memory.ts +5 -5
- package/src/onboard/steps.ts +8 -9
- package/src/provision/index.ts +2 -3
- package/src/runtime/deploy.ts +2 -2
- package/src/runtime/index.ts +4 -4
- package/src/shadow/gate.ts +6 -6
- package/src/shadow/index.ts +2 -2
- package/src/status/index.ts +11 -11
- package/src/storage/index.ts +1 -1
- package/src/supervisor/daemon.ts +14 -8
- package/src/transport/index.ts +11 -11
- package/src/update/index.ts +4 -4
- package/src/cli/cli.test.ts +0 -603
- package/src/cli/listTui.test.ts +0 -71
- package/src/codec/codec.test.ts +0 -271
- package/src/connect/connect.test.ts +0 -163
- package/src/core/constants.test.ts +0 -32
- package/src/core/normalize.test.ts +0 -98
- package/src/create/create.test.ts +0 -143
- package/src/daemon/daemon-http.test.ts +0 -114
- package/src/daemon/daemon.test.ts +0 -267
- package/src/daemon/main.test.ts +0 -413
- package/src/enable/enable.test.ts +0 -95
- package/src/enable/provisionCommand.test.ts +0 -107
- package/src/identity/identity.test.ts +0 -279
- package/src/identity/profileStandard.test.ts +0 -204
- package/src/init/init.test.ts +0 -213
- package/src/init/runtime-resolve.test.ts +0 -49
- package/src/install/install.test.ts +0 -66
- package/src/install/signing.test.ts +0 -84
- package/src/launch/bootdeliver.test.ts +0 -108
- package/src/launch/bootstrap.test.ts +0 -56
- package/src/launch/canary.test.ts +0 -346
- package/src/launch/cmdlog.test.ts +0 -94
- package/src/launch/codexHooksTrust.test.ts +0 -248
- package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
- package/src/launch/composeSystemPrompt.test.ts +0 -145
- package/src/launch/hostComposerOccupied.test.ts +0 -63
- package/src/launch/invocation.test.ts +0 -174
- package/src/launch/launch.test.ts +0 -359
- package/src/launch/launchd.test.ts +0 -499
- package/src/launch/nativeMemory.test.ts +0 -193
- package/src/launch/ptyHost.test.ts +0 -38
- package/src/launch/readyGateModel.test.ts +0 -72
- package/src/launch/sockdir.test.ts +0 -74
- package/src/lifecycle/eventlog.test.ts +0 -114
- package/src/lifecycle/lifecycle.test.ts +0 -1056
- package/src/lifecycle/queue.test.ts +0 -185
- package/src/onboard/memory.test.ts +0 -157
- package/src/onboard/onboard.test.ts +0 -39
- package/src/onboard/steps.test.ts +0 -201
- package/src/provision/provision.test.ts +0 -288
- package/src/registry/registry.test.ts +0 -432
- package/src/runtime/runtime.test.ts +0 -226
- package/src/runtime/update.test.ts +0 -192
- package/src/shadow/enumerate-skip.test.ts +0 -67
- package/src/shadow/gate.test.ts +0 -286
- package/src/shadow/install.test.ts +0 -153
- package/src/shadow/shadow.test.ts +0 -42
- package/src/status/status.test.ts +0 -208
- package/src/storage/peers-home.test.ts +0 -34
- package/src/storage/rotatelog.test.ts +0 -44
- package/src/storage/storage.test.ts +0 -65
- package/src/supervisor/attached-marker.test.ts +0 -84
- package/src/supervisor/boot.test.ts +0 -66
- package/src/supervisor/client.attach.test.ts +0 -107
- package/src/supervisor/daemon.test.ts +0 -213
- package/src/supervisor/deliver.test.ts +0 -58
- package/src/supervisor/panelog.test.ts +0 -95
- package/src/supervisor/protocol.test.ts +0 -222
- package/src/supervisor/selfargv.test.ts +0 -22
- package/src/supervisor/spinner-strip.test.ts +0 -52
- package/src/transport/compactDone.test.ts +0 -144
- package/src/transport/composerQueue.test.ts +0 -204
- package/src/transport/hostControl.test.ts +0 -87
- package/src/transport/hostDeliver.test.ts +0 -137
- package/src/transport/hostLive.test.ts +0 -50
- package/src/transport/routesend-wake.test.ts +0 -357
- package/src/update/update.test.ts +0 -208
package/src/init/init.test.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// init — per-peer onboarding: HTTP-MCP .mcp.json wiring (the install-gate-proven
|
|
2
|
-
// transport), doctrine template, and the initPeer orchestration over provisionPeer.
|
|
3
|
-
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
5
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
6
|
-
import { tmpdir } from 'os'
|
|
7
|
-
import { join } from 'path'
|
|
8
|
-
import {
|
|
9
|
-
CODEX_BEARER_ENV_VAR,
|
|
10
|
-
IAPEER_MCP_SERVER_NAME,
|
|
11
|
-
codexConfigPath,
|
|
12
|
-
ensureCodexUpdateCheckDisabled,
|
|
13
|
-
ensureDoctrineTemplate,
|
|
14
|
-
initPeer,
|
|
15
|
-
resolveDaemonMcpUrl,
|
|
16
|
-
writeClaudeMcpConfig,
|
|
17
|
-
writeCodexMcpConfig,
|
|
18
|
-
} from './index.ts'
|
|
19
|
-
import { readPeersIndex } from '../registry/index.ts'
|
|
20
|
-
import { readPeerProfile } from '../identity/index.ts'
|
|
21
|
-
|
|
22
|
-
let root: string
|
|
23
|
-
let cwd: string
|
|
24
|
-
function cleanEnv(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
|
|
25
|
-
const env: NodeJS.ProcessEnv = { ...process.env, IAPEER_ROOT: root, ...extra }
|
|
26
|
-
delete env.PEER_PERSONALITY
|
|
27
|
-
delete env.PEER_IDENTITY
|
|
28
|
-
delete env.PEER_RUNTIME
|
|
29
|
-
return env
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
root = mkdtempSync(join(tmpdir(), 'iapeer-init-root-'))
|
|
34
|
-
cwd = mkdtempSync(join(tmpdir(), 'iapeer-init-cwd-'))
|
|
35
|
-
})
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
rmSync(root, { recursive: true, force: true })
|
|
38
|
-
rmSync(cwd, { recursive: true, force: true })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('resolveDaemonMcpUrl', () => {
|
|
42
|
-
test('no router.json → well-known default loopback URL (IAPEER_PORT respected)', () => {
|
|
43
|
-
expect(resolveDaemonMcpUrl({ env: cleanEnv() })).toBe('http://127.0.0.1:8765/mcp')
|
|
44
|
-
expect(resolveDaemonMcpUrl({ env: cleanEnv({ IAPEER_PORT: '9999' }) })).toBe('http://127.0.0.1:9999/mcp')
|
|
45
|
-
})
|
|
46
|
-
test('router.json present → its tcp field (daemon-published endpoint)', () => {
|
|
47
|
-
const stateDir = join(root, 'state', 'iapeer')
|
|
48
|
-
mkdirSync(stateDir, { recursive: true })
|
|
49
|
-
writeFileSync(join(stateDir, 'router.json'), JSON.stringify({ sock: '/x.sock', tcp: 'http://127.0.0.1:8765/mcp' }))
|
|
50
|
-
expect(resolveDaemonMcpUrl({ env: cleanEnv() })).toBe('http://127.0.0.1:8765/mcp')
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('writeClaudeMcpConfig', () => {
|
|
55
|
-
test('writes the iapeer http server with the X-IAPeer-Identity header', () => {
|
|
56
|
-
const path = writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
|
|
57
|
-
const doc = JSON.parse(readFileSync(path, 'utf8'))
|
|
58
|
-
expect(doc.mcpServers[IAPEER_MCP_SERVER_NAME]).toEqual({
|
|
59
|
-
type: 'http',
|
|
60
|
-
url: 'http://127.0.0.1:8765/mcp',
|
|
61
|
-
headers: { 'X-IAPeer-Identity': 'claude-boris' },
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
test('MERGES — preserves other mcpServers, only (re)writes iapeer', () => {
|
|
65
|
-
writeFileSync(join(cwd, '.mcp.json'), JSON.stringify({ mcpServers: { other: { type: 'http', url: 'https://x/mcp' } } }))
|
|
66
|
-
writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
|
|
67
|
-
const doc = JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'))
|
|
68
|
-
expect(doc.mcpServers.other).toEqual({ type: 'http', url: 'https://x/mcp' })
|
|
69
|
-
expect(doc.mcpServers.iapeer.headers['X-IAPeer-Identity']).toBe('claude-boris')
|
|
70
|
-
})
|
|
71
|
-
test('idempotent — re-running yields the same bytes', () => {
|
|
72
|
-
const p = writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
|
|
73
|
-
const first = readFileSync(p, 'utf8')
|
|
74
|
-
writeClaudeMcpConfig(cwd, 'boris', 'http://127.0.0.1:8765/mcp')
|
|
75
|
-
expect(readFileSync(p, 'utf8')).toBe(first)
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
describe('ensureDoctrineTemplate', () => {
|
|
80
|
-
test('creates the template when absent', () => {
|
|
81
|
-
const { path, created } = ensureDoctrineTemplate(cwd)
|
|
82
|
-
expect(created).toBe(true)
|
|
83
|
-
expect(existsSync(path)).toBe(true)
|
|
84
|
-
expect(readFileSync(path, 'utf8')).toContain('Peer doctrine')
|
|
85
|
-
})
|
|
86
|
-
test('never overwrites an existing doctrine', () => {
|
|
87
|
-
mkdirSync(join(cwd, '.iapeer'), { recursive: true })
|
|
88
|
-
writeFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'I am boris.')
|
|
89
|
-
const { created } = ensureDoctrineTemplate(cwd)
|
|
90
|
-
expect(created).toBe(false)
|
|
91
|
-
expect(readFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'utf8')).toBe('I am boris.')
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
describe('writeCodexMcpConfig (token-free recipe — dummy bearer + env_http_headers identity)', () => {
|
|
96
|
-
test('adds [mcp_servers.iapeer] with url + approve + bearer_token_env_var + env_http_headers', () => {
|
|
97
|
-
const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome-'))
|
|
98
|
-
try {
|
|
99
|
-
const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
|
|
100
|
-
const { path, added } = writeCodexMcpConfig('http://127.0.0.1:8765/mcp', { env })
|
|
101
|
-
expect(added).toBe(true)
|
|
102
|
-
expect(path).toBe(codexConfigPath({ env }))
|
|
103
|
-
const toml = readFileSync(path, 'utf8')
|
|
104
|
-
expect(toml).toContain('[mcp_servers.iapeer]')
|
|
105
|
-
expect(toml).toContain('url = "http://127.0.0.1:8765/mcp"')
|
|
106
|
-
expect(toml).toContain('default_tools_approval_mode = "approve"')
|
|
107
|
-
expect(toml).toContain(`bearer_token_env_var = "${CODEX_BEARER_ENV_VAR}"`)
|
|
108
|
-
expect(toml).toContain('[mcp_servers.iapeer.env_http_headers]')
|
|
109
|
-
expect(toml).toContain('"X-IAPeer-Identity" = "PEER_IDENTITY"')
|
|
110
|
-
} finally {
|
|
111
|
-
rmSync(codexHome, { recursive: true, force: true })
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
test('idempotent — re-run does NOT duplicate the block, and preserves other config', () => {
|
|
115
|
-
const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome2-'))
|
|
116
|
-
try {
|
|
117
|
-
const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
|
|
118
|
-
writeFileSync(codexConfigPath({ env }), '[other]\nkeep = true\n')
|
|
119
|
-
expect(writeCodexMcpConfig('http://x/mcp', { env }).added).toBe(true)
|
|
120
|
-
expect(writeCodexMcpConfig('http://x/mcp', { env }).added).toBe(false) // idempotent
|
|
121
|
-
const toml = readFileSync(codexConfigPath({ env }), 'utf8')
|
|
122
|
-
expect((toml.match(/\[mcp_servers\.iapeer\]/g) ?? []).length).toBe(1) // no duplicate
|
|
123
|
-
expect(toml).toContain('[other]') // other config preserved
|
|
124
|
-
expect(toml).toContain('keep = true')
|
|
125
|
-
} finally {
|
|
126
|
-
rmSync(codexHome, { recursive: true, force: true })
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('initPeer orchestration', () => {
|
|
132
|
-
test('codex peer: profile + registry + token-free codex config + doctrine', async () => {
|
|
133
|
-
const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome3-'))
|
|
134
|
-
try {
|
|
135
|
-
const env = cleanEnv({ CODEX_HOME: codexHome })
|
|
136
|
-
const r = await initPeer({ cwd, runtime: 'codex', personality: 'cpeer', env })
|
|
137
|
-
expect(readPeerProfile(cwd)?.personality).toBe('cpeer')
|
|
138
|
-
expect(r.mcpConfigPaths.length).toBe(0) // no claude .mcp.json for a codex peer
|
|
139
|
-
expect(r.codexMcpConfigPath).toBe(codexConfigPath({ env }))
|
|
140
|
-
const toml = readFileSync(r.codexMcpConfigPath!, 'utf8')
|
|
141
|
-
expect(toml).toContain('[mcp_servers.iapeer]')
|
|
142
|
-
expect(toml).toContain(`bearer_token_env_var = "${CODEX_BEARER_ENV_VAR}"`) // token-free recipe wired
|
|
143
|
-
} finally {
|
|
144
|
-
rmSync(codexHome, { recursive: true, force: true })
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test('claude peer: profile + registry + .mcp.json + doctrine, all wired', async () => {
|
|
149
|
-
const env = cleanEnv()
|
|
150
|
-
const r = await initPeer({ cwd, runtime: 'claude', personality: 'mypeer', env })
|
|
151
|
-
// identity + registry (via provision)
|
|
152
|
-
expect(readPeerProfile(cwd)?.personality).toBe('mypeer')
|
|
153
|
-
expect(readPeersIndex({ env }).peers.some(p => p.personality === 'mypeer')).toBe(true)
|
|
154
|
-
// .mcp.json wired to the daemon with the per-peer identity header
|
|
155
|
-
expect(r.mcpConfigPaths.length).toBe(1)
|
|
156
|
-
const mcp = JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'))
|
|
157
|
-
expect(mcp.mcpServers.iapeer.headers['X-IAPeer-Identity']).toBe('claude-mypeer')
|
|
158
|
-
expect(mcp.mcpServers.iapeer.url).toBe('http://127.0.0.1:8765/mcp')
|
|
159
|
-
// doctrine template created
|
|
160
|
-
expect(r.doctrineCreated).toBe(true)
|
|
161
|
-
expect(existsSync(r.doctrinePath)).toBe(true)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test('idempotent — re-init does not throw and keeps an existing doctrine', async () => {
|
|
165
|
-
const env = cleanEnv()
|
|
166
|
-
await initPeer({ cwd, runtime: 'claude', personality: 'mypeer', env })
|
|
167
|
-
writeFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'custom doctrine')
|
|
168
|
-
const r2 = await initPeer({ cwd, runtime: 'claude', personality: 'mypeer', env })
|
|
169
|
-
expect(r2.doctrineCreated).toBe(false)
|
|
170
|
-
expect(readFileSync(join(cwd, '.iapeer', 'IAPEER.md'), 'utf8')).toBe('custom doctrine')
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
describe('ensureCodexUpdateCheckDisabled (boot-gate hygiene — incident 11.06 codex-linus)', () => {
|
|
175
|
-
test('PREPENDS the top-level key BEFORE the first [section] (TOML top-level scope)', () => {
|
|
176
|
-
const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome3-'))
|
|
177
|
-
try {
|
|
178
|
-
const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
|
|
179
|
-
writeFileSync(codexConfigPath({ env }), 'model = "gpt-5.5"\n\n[projects."/x"]\ntrust_level = "trusted"\n')
|
|
180
|
-
const { changed } = ensureCodexUpdateCheckDisabled({ env })
|
|
181
|
-
expect(changed).toBe(true)
|
|
182
|
-
const toml = readFileSync(codexConfigPath({ env }), 'utf8')
|
|
183
|
-
expect(toml.startsWith('check_for_update_on_startup = false\n')).toBe(true)
|
|
184
|
-
expect(toml).toContain('[projects."/x"]') // existing config preserved
|
|
185
|
-
// idempotent: second run is a no-op
|
|
186
|
-
expect(ensureCodexUpdateCheckDisabled({ env }).changed).toBe(false)
|
|
187
|
-
expect((toml.match(/check_for_update_on_startup/g) ?? []).length).toBe(1)
|
|
188
|
-
} finally {
|
|
189
|
-
rmSync(codexHome, { recursive: true, force: true })
|
|
190
|
-
}
|
|
191
|
-
})
|
|
192
|
-
test('respects an EXPLICIT existing value (never overrides the operator)', () => {
|
|
193
|
-
const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome4-'))
|
|
194
|
-
try {
|
|
195
|
-
const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
|
|
196
|
-
writeFileSync(codexConfigPath({ env }), 'check_for_update_on_startup = true\n')
|
|
197
|
-
expect(ensureCodexUpdateCheckDisabled({ env }).changed).toBe(false)
|
|
198
|
-
expect(readFileSync(codexConfigPath({ env }), 'utf8')).toContain('check_for_update_on_startup = true')
|
|
199
|
-
} finally {
|
|
200
|
-
rmSync(codexHome, { recursive: true, force: true })
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
test('absent config → created with just the key', () => {
|
|
204
|
-
const codexHome = mkdtempSync(join(tmpdir(), 'iapeer-codexhome5-'))
|
|
205
|
-
try {
|
|
206
|
-
const env: NodeJS.ProcessEnv = { ...process.env, CODEX_HOME: codexHome }
|
|
207
|
-
expect(ensureCodexUpdateCheckDisabled({ env }).changed).toBe(true)
|
|
208
|
-
expect(readFileSync(codexConfigPath({ env }), 'utf8')).toBe('check_for_update_on_startup = false\n')
|
|
209
|
-
} finally {
|
|
210
|
-
rmSync(codexHome, { recursive: true, force: true })
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// resolvePrimaryRuntime — the runtime-aware, CONSISTENT primary-runtime resolution
|
|
2
|
-
// for init/create. Pure (filesystem markers only); no registry / launchd touched.
|
|
3
|
-
|
|
4
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
5
|
-
import { mkdirSync, mkdtempSync, rmSync } from 'fs'
|
|
6
|
-
import { tmpdir } from 'os'
|
|
7
|
-
import { join } from 'path'
|
|
8
|
-
import { resolvePrimaryRuntime } from './index.ts'
|
|
9
|
-
|
|
10
|
-
const dirs: string[] = []
|
|
11
|
-
function mkTmp(): string {
|
|
12
|
-
const d = mkdtempSync(join(tmpdir(), 'iapeer-rt-'))
|
|
13
|
-
dirs.push(d)
|
|
14
|
-
return d
|
|
15
|
-
}
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
describe('resolvePrimaryRuntime', () => {
|
|
21
|
-
test('explicit runtime wins regardless of markers', () => {
|
|
22
|
-
const cwd = mkTmp()
|
|
23
|
-
mkdirSync(join(cwd, '.codex'))
|
|
24
|
-
expect(resolvePrimaryRuntime(cwd, 'telegram')).toBe('telegram')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('no marker → claude (default)', () => {
|
|
28
|
-
expect(resolvePrimaryRuntime(mkTmp())).toBe('claude')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('.codex-only folder → codex primary (was the inconsistency: defaulted to claude)', () => {
|
|
32
|
-
const cwd = mkTmp()
|
|
33
|
-
mkdirSync(join(cwd, '.codex'))
|
|
34
|
-
expect(resolvePrimaryRuntime(cwd)).toBe('codex')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('.claude folder → claude', () => {
|
|
38
|
-
const cwd = mkTmp()
|
|
39
|
-
mkdirSync(join(cwd, '.claude'))
|
|
40
|
-
expect(resolvePrimaryRuntime(cwd)).toBe('claude')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('both markers → claude primary (agentic default order)', () => {
|
|
44
|
-
const cwd = mkTmp()
|
|
45
|
-
mkdirSync(join(cwd, '.claude'))
|
|
46
|
-
mkdirSync(join(cwd, '.codex'))
|
|
47
|
-
expect(resolvePrimaryRuntime(cwd)).toBe('claude')
|
|
48
|
-
})
|
|
49
|
-
})
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// install — the stable binary path + (lightly) the build. iapeerBinPath is the
|
|
2
|
-
// decoupling anchor: the launchd plists reference it, not process.execPath / a src
|
|
3
|
-
// file. The full `bun build --compile` is exercised LIVE (it writes a ~60M binary —
|
|
4
|
-
// too heavy for a unit test); here we pin the path resolution.
|
|
5
|
-
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
7
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
8
|
-
import { tmpdir } from 'os'
|
|
9
|
-
import { join } from 'path'
|
|
10
|
-
import { iapeerBinPath, iapeerPrevBinPath, installIapeer, rollbackIapeer } from './index.ts'
|
|
11
|
-
|
|
12
|
-
describe('iapeerBinPath', () => {
|
|
13
|
-
test('default = <home>/.local/bin/iapeer (stable host-wide path, on $PATH)', () => {
|
|
14
|
-
expect(iapeerBinPath({ HOME: '/Users/x' } as NodeJS.ProcessEnv)).toBe('/Users/x/.local/bin/iapeer')
|
|
15
|
-
})
|
|
16
|
-
test('IAPEER_BIN_DIR override (tests/sandbox — never a real ~/.local/bin)', () => {
|
|
17
|
-
expect(iapeerBinPath({ HOME: '/Users/x', IAPEER_BIN_DIR: '/tmp/sbx/bin' } as NodeJS.ProcessEnv)).toBe(
|
|
18
|
-
join('/tmp/sbx/bin', 'iapeer'),
|
|
19
|
-
)
|
|
20
|
-
})
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// Audit #25 — fail-closed sandbox guard (symmetric to the registry's). installIapeer
|
|
24
|
-
// overwrites the live prod binary ~/.local/bin/iapeer; a sandbox/test that forgets
|
|
25
|
-
// IAPEER_BIN_DIR must be REFUSED, never clobber prod. The guard fires BEFORE the build,
|
|
26
|
-
// so no `bun build --compile` runs here.
|
|
27
|
-
describe('installIapeer fail-closed sandbox guard', () => {
|
|
28
|
-
test('THROWS under IAPEER_TEST_SANDBOX=1 when binPath falls through to the REAL ~/.local/bin/iapeer', () => {
|
|
29
|
-
const env = { IAPEER_TEST_SANDBOX: '1', HOME: '/Users/fake-home' } as NodeJS.ProcessEnv
|
|
30
|
-
expect(iapeerBinPath(env)).toBe('/Users/fake-home/.local/bin/iapeer')
|
|
31
|
-
expect(() => installIapeer('/x/entry.ts', env)).toThrow(/refusing to overwrite the REAL prod binary/)
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('rollbackIapeer', () => {
|
|
36
|
-
let binDir: string
|
|
37
|
-
let env: NodeJS.ProcessEnv
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
binDir = mkdtempSync(join(tmpdir(), 'iapeer-rollback-'))
|
|
40
|
-
env = { IAPEER_TEST_SANDBOX: '1', HOME: '/Users/x', IAPEER_BIN_DIR: binDir } as NodeJS.ProcessEnv
|
|
41
|
-
})
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
rmSync(binDir, { recursive: true, force: true })
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('no .prev → failed (nothing to roll back to), binary untouched', () => {
|
|
47
|
-
writeFileSync(iapeerBinPath(env), 'CURRENT')
|
|
48
|
-
const r = rollbackIapeer(env)
|
|
49
|
-
expect(r.status).toBe('failed')
|
|
50
|
-
expect(r.reason).toMatch(/nothing to roll back/i)
|
|
51
|
-
expect(readFileSync(iapeerBinPath(env), 'utf8')).toBe('CURRENT') // unchanged
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('restores the .prev bytes over the binary', () => {
|
|
55
|
-
writeFileSync(iapeerBinPath(env), 'NEW-BROKEN')
|
|
56
|
-
writeFileSync(iapeerPrevBinPath(env), 'OLD-GOOD')
|
|
57
|
-
const r = rollbackIapeer(env)
|
|
58
|
-
expect(r.status).toBe('rolled-back')
|
|
59
|
-
expect(readFileSync(iapeerBinPath(env), 'utf8')).toBe('OLD-GOOD')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('fail-closed sandbox guard: refuses the REAL prod binary path', () => {
|
|
63
|
-
const realEnv = { IAPEER_TEST_SANDBOX: '1', HOME: '/Users/fake-home' } as NodeJS.ProcessEnv
|
|
64
|
-
expect(() => rollbackIapeer(realEnv)).toThrow(/refusing to overwrite the REAL prod binary/)
|
|
65
|
-
})
|
|
66
|
-
})
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// signInstalledBinary — stable-identity re-sign so TCC grants survive updates
|
|
2
|
-
// (Артур's DX requirement 10.06). DI-runner units; the real keychain flow was
|
|
3
|
-
// proven live (/tmp experiment: two binaries, different CDHash, IDENTICAL
|
|
4
|
-
// designated requirement `identifier "com.agfpd.iapeer" and certificate leaf`).
|
|
5
|
-
// The sandbox guard double-checks process.env, so these tests inject a runner
|
|
6
|
-
// AND call with the flag stripped via a direct env — guard tested separately.
|
|
7
|
-
|
|
8
|
-
import { describe, expect, test } from 'bun:test'
|
|
9
|
-
import { SIGNING_IDENTIFIER, SIGNING_IDENTITY_CN, signInstalledBinary, type SigningRunner } from './signing.ts'
|
|
10
|
-
|
|
11
|
-
function harness(opts: { identityExists: boolean; failAt?: 'req' | 'pkcs12' | 'import' | 'codesign' }) {
|
|
12
|
-
const calls: { cmd: string; args: string[] }[] = []
|
|
13
|
-
const run: SigningRunner = (cmd, args) => {
|
|
14
|
-
calls.push({ cmd, args })
|
|
15
|
-
if (cmd === 'security' && args[0] === 'find-identity') {
|
|
16
|
-
return { status: 0, stdout: opts.identityExists ? `1) ABC "${SIGNING_IDENTITY_CN}" (CSSMERR_TP_NOT_TRUSTED)\n` : 'no identities\n', stderr: '' }
|
|
17
|
-
}
|
|
18
|
-
if (cmd.endsWith('openssl') && args[0] === 'req') return { status: opts.failAt === 'req' ? 1 : 0, stdout: '', stderr: 'req boom' }
|
|
19
|
-
if (cmd.endsWith('openssl') && args[0] === 'pkcs12') return { status: opts.failAt === 'pkcs12' ? 1 : 0, stdout: '', stderr: 'p12 boom' }
|
|
20
|
-
if (cmd === 'security' && args[0] === 'import') return { status: opts.failAt === 'import' ? 1 : 0, stdout: '', stderr: 'import boom' }
|
|
21
|
-
if (cmd === 'codesign') return { status: opts.failAt === 'codesign' ? 1 : 0, stdout: '', stderr: 'sign boom' }
|
|
22
|
-
return { status: 0, stdout: '', stderr: '' }
|
|
23
|
-
}
|
|
24
|
-
return { calls, run }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// NOTE: process.env.IAPEER_TEST_SANDBOX === '1' under `bun run test`, so the
|
|
28
|
-
// guard SHORT-CIRCUITS every real call — these units therefore stub process.env
|
|
29
|
-
// off for the duration of each call.
|
|
30
|
-
function withSandboxOff<T>(fn: () => T): T {
|
|
31
|
-
const prev = process.env.IAPEER_TEST_SANDBOX
|
|
32
|
-
delete process.env.IAPEER_TEST_SANDBOX
|
|
33
|
-
try {
|
|
34
|
-
return fn()
|
|
35
|
-
} finally {
|
|
36
|
-
if (prev !== undefined) process.env.IAPEER_TEST_SANDBOX = prev
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('signInstalledBinary (stable identity → TCC grants survive updates)', () => {
|
|
41
|
-
test('sandbox guard: never touches the keychain under IAPEER_TEST_SANDBOX', () => {
|
|
42
|
-
const h = harness({ identityExists: true })
|
|
43
|
-
const r = signInstalledBinary('/x/iapeer', { IAPEER_TEST_SANDBOX: '1' } as NodeJS.ProcessEnv, h.run)
|
|
44
|
-
expect(r.state).toBe('skipped-sandbox')
|
|
45
|
-
expect(h.calls.length).toBe(0)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('existing identity → single codesign with the stable identifier', () => {
|
|
49
|
-
const h = harness({ identityExists: true })
|
|
50
|
-
const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
|
|
51
|
-
expect(r.state).toBe('signed')
|
|
52
|
-
const sign = h.calls.find(c => c.cmd === 'codesign')!
|
|
53
|
-
expect(sign.args).toEqual(['-f', '-s', SIGNING_IDENTITY_CN, '--identifier', SIGNING_IDENTIFIER, '/x/iapeer'])
|
|
54
|
-
// identity lookup is NOT -v (an untrusted self-signed identity must be found)
|
|
55
|
-
const find = h.calls.find(c => c.args[0] === 'find-identity')!
|
|
56
|
-
expect(find.args).not.toContain('-v')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('no identity → created once (openssl req → pkcs12 → import -T codesign), then signed', () => {
|
|
60
|
-
const h = harness({ identityExists: false })
|
|
61
|
-
const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
|
|
62
|
-
expect(r.state).toBe('signed-new-identity')
|
|
63
|
-
const seq = h.calls.map(c => `${c.cmd.split('/').pop()}:${c.args[0]}`)
|
|
64
|
-
expect(seq).toEqual(['security:find-identity', 'openssl:req', 'openssl:pkcs12', 'security:import', 'codesign:-f'])
|
|
65
|
-
const imp = h.calls.find(c => c.args[0] === 'import')!
|
|
66
|
-
expect(imp.args).toContain('-T') // codesign pre-authorized in the key ACL
|
|
67
|
-
expect(imp.args).toContain('/usr/bin/codesign')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('identity-creation failure → failed-soft with the loud TCC consequence, codesign never attempted', () => {
|
|
71
|
-
const h = harness({ identityExists: false, failAt: 'import' })
|
|
72
|
-
const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
|
|
73
|
-
expect(r.state).toBe('failed-soft')
|
|
74
|
-
expect(r.detail).toContain('TCC prompts will re-appear')
|
|
75
|
-
expect(h.calls.some(c => c.cmd === 'codesign')).toBe(false)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('codesign failure → failed-soft (install never breaks on a signing hiccup)', () => {
|
|
79
|
-
const h = harness({ identityExists: true, failAt: 'codesign' })
|
|
80
|
-
const r = withSandboxOff(() => signInstalledBinary('/x/iapeer', {} as NodeJS.ProcessEnv, h.run))
|
|
81
|
-
expect(r.state).toBe('failed-soft')
|
|
82
|
-
expect(r.detail).toContain('sign boom')
|
|
83
|
-
})
|
|
84
|
-
})
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Ф-#8b: cold-wake boot first-message delivery via load-buffer + bracketed
|
|
2
|
-
// paste-buffer — the SAME byte-path as warm delivery (transport.deliverViaTmux),
|
|
3
|
-
// replacing the old `send-keys -l` retype. Proven hermetically against a REAL
|
|
4
|
-
// tmux (gated like sockdir.test.ts) with a fake tui adapter whose "runtime" is
|
|
5
|
-
// `cat >> <file>`: whatever the boot path injects into the pane lands verbatim in
|
|
6
|
-
// the file, and the ready-gate keys on that file's mtime (the activity proxy).
|
|
7
|
-
//
|
|
8
|
-
// pty note: `cat` reads the pane pty in CANONICAL mode (line-buffered, ~1 KiB
|
|
9
|
-
// line cap on macOS) — real TUIs run raw and have no such cap. The fixture
|
|
10
|
-
// message therefore uses many sub-1-KiB LINES to total multi-KiB; what this test
|
|
11
|
-
// pins is the INJECTION path (one bracketed paste, no option-parsing traps, no
|
|
12
|
-
// key-by-key retype), not the tty discipline.
|
|
13
|
-
|
|
14
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
15
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'
|
|
16
|
-
import { tmpdir } from 'os'
|
|
17
|
-
import { join } from 'path'
|
|
18
|
-
import { spawnSync } from 'child_process'
|
|
19
|
-
import { launch } from './index.ts'
|
|
20
|
-
import type { LaunchConfig, LaunchSpec, RuntimeAdapter } from './types.ts'
|
|
21
|
-
|
|
22
|
-
const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
|
|
23
|
-
|
|
24
|
-
const dirs: string[] = []
|
|
25
|
-
function mkTmp(): string {
|
|
26
|
-
const d = mkdtempSync(join(tmpdir(), 'iapeer-bootdeliver-'))
|
|
27
|
-
dirs.push(d)
|
|
28
|
-
return d
|
|
29
|
-
}
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
/** Fake tui adapter: the "runtime" appends its pty input to `recvPath`; the
|
|
35
|
-
* activity proxy is that file's mtime (absent → null, exactly like a missing
|
|
36
|
-
* transcript), so the boot baseline is 0 and the ready-gate flips on receipt. */
|
|
37
|
-
function catAdapter(recvPath: string): RuntimeAdapter {
|
|
38
|
-
return {
|
|
39
|
-
runtime: 'claude', // any tui Runtime — the adapter object itself is what launch consumes
|
|
40
|
-
kind: 'tui',
|
|
41
|
-
usesDoctrine: false,
|
|
42
|
-
deliveryMarkers: { promptGlyphs: [] },
|
|
43
|
-
buildArgv: () => ['/bin/sh', '-c', `cat >> ${recvPath}`],
|
|
44
|
-
bootDialogKeys: () => null,
|
|
45
|
-
isInputReady: () => true,
|
|
46
|
-
newestActivityMtime: () => {
|
|
47
|
-
try {
|
|
48
|
-
return statSync(recvPath).mtimeMs
|
|
49
|
-
} catch {
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
resolveResume: () => ({ ok: true }),
|
|
54
|
-
executeControl: () => null,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
describe('boot first-message delivery (load-buffer + bracketed paste)', () => {
|
|
59
|
-
test.if(tmuxAvailable)(
|
|
60
|
-
'a multi-line, dash-leading, multi-KiB first message lands INTACT and launch goes READY',
|
|
61
|
-
async () => {
|
|
62
|
-
const root = mkTmp()
|
|
63
|
-
const recv = join(root, 'received.txt')
|
|
64
|
-
const sock = join(root, 'tmux-iap-claude-bootd.sock')
|
|
65
|
-
// The fixture stresses every historical boot-inject trap at once:
|
|
66
|
-
// • leading '-' — the send-keys option-parsing trap (audit #6);
|
|
67
|
-
// • quotes/$()/; — shell-metachar corruption if anything re-quoted the body;
|
|
68
|
-
// • multi-KiB — a size send-keys -l replayed key-by-key (8 × ~700 B lines).
|
|
69
|
-
const firstMessage = [
|
|
70
|
-
'- dash-leading first line (the send-keys option-parsing trap)',
|
|
71
|
-
`quotes "double" 'single' and $dollar \`backtick\` ; semicolon`,
|
|
72
|
-
...Array.from({ length: 8 }, (_, i) => `${i}:${'x'.repeat(700)}`),
|
|
73
|
-
].join('\n')
|
|
74
|
-
const spec: LaunchSpec = {
|
|
75
|
-
personality: 'bootd',
|
|
76
|
-
runtime: 'claude',
|
|
77
|
-
cwd: root,
|
|
78
|
-
identity: 'claude-bootd',
|
|
79
|
-
socketPath: sock,
|
|
80
|
-
intelligence: 'artificial',
|
|
81
|
-
}
|
|
82
|
-
const cfg: LaunchConfig = {
|
|
83
|
-
claudeBin: 'unused',
|
|
84
|
-
codexBin: 'unused',
|
|
85
|
-
sockDir: root,
|
|
86
|
-
bootDeadlineSecs: 10,
|
|
87
|
-
readyGateSecs: 8,
|
|
88
|
-
logDir: join(root, 'logs'),
|
|
89
|
-
}
|
|
90
|
-
// pty-supervisor hosting is the DEFAULT now; this fixture exercises the legacy TMUX boot/delivery
|
|
91
|
-
// path (load-buffer + bracketed paste) explicitly, so opt this peer OUT of pty with a .no-pty-host
|
|
92
|
-
// marker (otherwise launch hosts it under the supervisor and the tmux-path assertions never apply).
|
|
93
|
-
mkdirSync(cfg.logDir, { recursive: true })
|
|
94
|
-
writeFileSync(join(cfg.logDir, `${spec.identity}.no-pty-host`), '')
|
|
95
|
-
try {
|
|
96
|
-
const r = await launch(spec, catAdapter(recv), firstMessage, cfg)
|
|
97
|
-
expect(r.status).toBe('READY')
|
|
98
|
-
const got = readFileSync(recv, 'utf8')
|
|
99
|
-
expect(got).toContain('- dash-leading first line (the send-keys option-parsing trap)')
|
|
100
|
-
expect(got).toContain(`quotes "double" 'single' and $dollar \`backtick\` ; semicolon`)
|
|
101
|
-
for (let i = 0; i < 8; i++) expect(got).toContain(`${i}:${'x'.repeat(700)}`)
|
|
102
|
-
} finally {
|
|
103
|
-
spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
60000,
|
|
107
|
-
)
|
|
108
|
-
})
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// launchctlBootstrap — the AUTO-load primitive guards. The happy `loaded` path is
|
|
2
|
-
// proven LIVE (it calls real launchctl); the unit tests cover the three guards that
|
|
3
|
-
// must hold WITHOUT touching launchd: foreign-plist refusal, sandbox skip, and the
|
|
4
|
-
// foundation-owned gate. (The test script sets IAPEER_TEST_SANDBOX=1, so the skip
|
|
5
|
-
// branch is the default; the foreign-refusal branch is checked first regardless.)
|
|
6
|
-
|
|
7
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
8
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
9
|
-
import { tmpdir } from 'os'
|
|
10
|
-
import { join } from 'path'
|
|
11
|
-
import { launchctlBootstrap, installAlwaysOnPlist } from './index.ts'
|
|
12
|
-
|
|
13
|
-
const dirs: string[] = []
|
|
14
|
-
function mkTmp(): string {
|
|
15
|
-
const d = mkdtempSync(join(tmpdir(), 'iapeer-boot-'))
|
|
16
|
-
dirs.push(d)
|
|
17
|
-
return d
|
|
18
|
-
}
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('launchctlBootstrap guards', () => {
|
|
24
|
-
test('refuses a NON-foundation plist (no ownership sentinel) — fleet guard, checked first', () => {
|
|
25
|
-
const dir = mkTmp()
|
|
26
|
-
const foreign = join(dir, 'com.iapeer.boris.plist')
|
|
27
|
-
writeFileSync(foreign, '<?xml version="1.0"?><plist><dict></dict></plist>')
|
|
28
|
-
// refused-foreign wins even with IAPEER_TEST_SANDBOX=1 (the guard is checked before sandbox)
|
|
29
|
-
const r = launchctlBootstrap('boris', foreign, { IAPEER_TEST_SANDBOX: '1' } as NodeJS.ProcessEnv)
|
|
30
|
-
expect(r.state).toBe('refused-foreign')
|
|
31
|
-
expect(r.label).toBe('com.iapeer.boris')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('a foundation-owned plist under IAPEER_TEST_SANDBOX=1 → skipped-sandbox (no launchctl)', () => {
|
|
35
|
-
const root = mkTmp()
|
|
36
|
-
const bindir = mkTmp()
|
|
37
|
-
const bin = join(bindir, 'notifier-runtime')
|
|
38
|
-
writeFileSync(bin, '#!/bin/sh\nexec sleep 1\n', { mode: 0o755 })
|
|
39
|
-
const env = {
|
|
40
|
-
IAPEER_LAUNCHAGENTS_DIR: join(root, 'LA'),
|
|
41
|
-
HOME: root,
|
|
42
|
-
PATH: bindir,
|
|
43
|
-
IAPEER_TEST_SANDBOX: '1',
|
|
44
|
-
} as NodeJS.ProcessEnv
|
|
45
|
-
const cwd = join(root, 'timer')
|
|
46
|
-
const plist = installAlwaysOnPlist({ personality: 'timer', runtime: 'notifier', cwd, runtimeBin: bin, env })
|
|
47
|
-
const r = launchctlBootstrap('timer', plist, env)
|
|
48
|
-
expect(r.state).toBe('skipped-sandbox')
|
|
49
|
-
expect(r.label).toBe('com.iapeer.timer')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test('absent/unreadable plist → refused-foreign (not provably ours)', () => {
|
|
53
|
-
const r = launchctlBootstrap('ghost', join(mkTmp(), 'nope.plist'), { IAPEER_TEST_SANDBOX: '1' } as NodeJS.ProcessEnv)
|
|
54
|
-
expect(r.state).toBe('refused-foreign')
|
|
55
|
-
})
|
|
56
|
-
})
|