@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,192 +0,0 @@
|
|
|
1
|
-
// update-runtime (§(г)) — version-gate / forced re-install / idempotent re-provision /
|
|
2
|
-
// peer restart. Sandboxed (IAPEER_ROOT temp dirs, IAPEER_TEST_SANDBOX), injected
|
|
3
|
-
// npx + npm-version + restart.
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
6
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
7
|
-
import { tmpdir } from 'os'
|
|
8
|
-
import { join } from 'path'
|
|
9
|
-
import { updateAllRuntimes, updateRuntime, type RestartedPeer } from './update.ts'
|
|
10
|
-
import { readRuntimeManifest, writeRuntimeManifest, type RuntimeManifest } from './index.ts'
|
|
11
|
-
import { upsertPeer } from '../registry/index.ts'
|
|
12
|
-
|
|
13
|
-
const roots: string[] = []
|
|
14
|
-
function mkTmp(): string {
|
|
15
|
-
const d = mkdtempSync(join(tmpdir(), 'iapeer-rtup-'))
|
|
16
|
-
roots.push(d)
|
|
17
|
-
return d
|
|
18
|
-
}
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
function envFor(root: string, path?: string): NodeJS.ProcessEnv {
|
|
24
|
-
return {
|
|
25
|
-
IAPEER_ROOT: join(root, 'iapeer'),
|
|
26
|
-
IAPEER_LAUNCHAGENTS_DIR: join(root, 'LA'),
|
|
27
|
-
IAPEER_TEST_SANDBOX: '1',
|
|
28
|
-
HOME: root,
|
|
29
|
-
...(path ? { PATH: path } : {}),
|
|
30
|
-
} as NodeJS.ProcessEnv
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function stubBins(): { dir: string; hook: string } {
|
|
34
|
-
const dir = mkTmp()
|
|
35
|
-
writeFileSync(join(dir, 'notifier-runtime'), '#!/bin/sh\nexec sleep 1\n', { mode: 0o755 })
|
|
36
|
-
const hook = join(dir, 'sc.sh')
|
|
37
|
-
writeFileSync(hook, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
|
|
38
|
-
return { dir, hook }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const restartOk =
|
|
42
|
-
(log: string[]) =>
|
|
43
|
-
(personality: string): RestartedPeer => {
|
|
44
|
-
log.push(personality)
|
|
45
|
-
return { personality, state: 'restarted' }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('updateRuntime (§(г): gate → re-install → re-provision → restart)', () => {
|
|
49
|
-
test('full update: re-npx forced, declared set re-provisioned, peers restarted', async () => {
|
|
50
|
-
const root = mkTmp()
|
|
51
|
-
const { dir, hook } = stubBins()
|
|
52
|
-
const env = envFor(root, dir)
|
|
53
|
-
writeRuntimeManifest(
|
|
54
|
-
{ runtime: 'notifier', version: '0.1.0', selfConfig: hook, peers: [{ personality: 'timer', intelligence: 'absent' }] },
|
|
55
|
-
{ env },
|
|
56
|
-
)
|
|
57
|
-
// the live-host layout: the declared peer already sits in its DEFAULT location
|
|
58
|
-
// (re-provision must be a no-clobber pass over it, not an identity conflict)
|
|
59
|
-
await upsertPeer(
|
|
60
|
-
{ personality: 'timer', runtime: 'notifier', cwd: join(root, 'iapeer', 'peers', 'timer'), intelligence: 'absent' },
|
|
61
|
-
{ env },
|
|
62
|
-
)
|
|
63
|
-
let npxRan = false
|
|
64
|
-
const restarts: string[] = []
|
|
65
|
-
const r = await updateRuntime({
|
|
66
|
-
runtime: 'notifier',
|
|
67
|
-
env,
|
|
68
|
-
runNpx: (_pkg, e) => {
|
|
69
|
-
npxRan = true
|
|
70
|
-
// the package self-deploys the NEW manifest with the new version stamp
|
|
71
|
-
const m: RuntimeManifest = { runtime: 'notifier', version: '0.2.0', selfConfig: hook, peers: [{ personality: 'timer', intelligence: 'absent' }] }
|
|
72
|
-
writeRuntimeManifest(m, { env: e })
|
|
73
|
-
return { ok: true }
|
|
74
|
-
},
|
|
75
|
-
npmVersion: () => '0.2.0',
|
|
76
|
-
restartPeer: restartOk(restarts),
|
|
77
|
-
})
|
|
78
|
-
expect(r.state).toBe('updated')
|
|
79
|
-
expect(npxRan).toBe(true) // FORCED re-npx despite the present manifest
|
|
80
|
-
expect(r.from).toBe('0.1.0')
|
|
81
|
-
expect(r.to).toBe('0.2.0')
|
|
82
|
-
expect(r.peers.map(p => p.personality)).toEqual(['timer']) // re-provisioned
|
|
83
|
-
expect(restarts).toEqual(['timer']) // restarted via the injected stop/start
|
|
84
|
-
expect(readRuntimeManifest('notifier', { env })?.version).toBe('0.2.0')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('version-gate: manifest stamp equals npm latest → already-latest, NOTHING runs', async () => {
|
|
88
|
-
const env = envFor(mkTmp())
|
|
89
|
-
writeRuntimeManifest({ runtime: 'notifier', version: '0.2.0' }, { env })
|
|
90
|
-
let npxRan = false
|
|
91
|
-
const r = await updateRuntime({ runtime: 'notifier', env, runNpx: () => ((npxRan = true), { ok: true }), npmVersion: () => '0.2.0' })
|
|
92
|
-
expect(r.state).toBe('already-latest')
|
|
93
|
-
expect(npxRan).toBe(false)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('--force overrides the gate', async () => {
|
|
97
|
-
const root = mkTmp()
|
|
98
|
-
const { dir, hook } = stubBins()
|
|
99
|
-
const env = envFor(root, dir)
|
|
100
|
-
writeRuntimeManifest({ runtime: 'notifier', version: '0.2.0', selfConfig: hook }, { env })
|
|
101
|
-
let npxRan = false
|
|
102
|
-
const r = await updateRuntime({
|
|
103
|
-
runtime: 'notifier',
|
|
104
|
-
env,
|
|
105
|
-
force: true,
|
|
106
|
-
runNpx: () => ((npxRan = true), { ok: true }),
|
|
107
|
-
npmVersion: () => '0.2.0',
|
|
108
|
-
restartPeer: restartOk([]),
|
|
109
|
-
})
|
|
110
|
-
expect(r.state).toBe('updated')
|
|
111
|
-
expect(npxRan).toBe(true)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test('no version stamp → gate skipped, idempotent re-install with an honest detail', async () => {
|
|
115
|
-
const root = mkTmp()
|
|
116
|
-
const { dir, hook } = stubBins()
|
|
117
|
-
const env = envFor(root, dir)
|
|
118
|
-
writeRuntimeManifest({ runtime: 'notifier', selfConfig: hook }, { env }) // owners have not stamped yet
|
|
119
|
-
const r = await updateRuntime({
|
|
120
|
-
runtime: 'notifier',
|
|
121
|
-
env,
|
|
122
|
-
runNpx: () => ({ ok: true }),
|
|
123
|
-
npmVersion: () => '0.2.0',
|
|
124
|
-
restartPeer: restartOk([]),
|
|
125
|
-
})
|
|
126
|
-
expect(r.state).toBe('updated')
|
|
127
|
-
expect(r.detail).toContain('no version stamp')
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('not installed → not-installed with the install-runtime recipe; npm unreachable → loud', async () => {
|
|
131
|
-
const env = envFor(mkTmp())
|
|
132
|
-
const r = await updateRuntime({ runtime: 'notifier', env, npmVersion: () => '0.2.0' })
|
|
133
|
-
expect(r.state).toBe('not-installed')
|
|
134
|
-
expect(r.detail).toContain('install-runtime notifier')
|
|
135
|
-
writeRuntimeManifest({ runtime: 'notifier', version: '0.1.0' }, { env })
|
|
136
|
-
const r2 = await updateRuntime({ runtime: 'notifier', env, npmVersion: () => null })
|
|
137
|
-
expect(r2.state).toBe('npm-unreachable')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
test('failed re-npx → install-failed, no deploy, no restarts', async () => {
|
|
141
|
-
const env = envFor(mkTmp())
|
|
142
|
-
writeRuntimeManifest({ runtime: 'notifier', version: '0.1.0' }, { env })
|
|
143
|
-
const restarts: string[] = []
|
|
144
|
-
const r = await updateRuntime({
|
|
145
|
-
runtime: 'notifier',
|
|
146
|
-
env,
|
|
147
|
-
runNpx: () => ({ ok: false, detail: 'network down' }),
|
|
148
|
-
npmVersion: () => '0.2.0',
|
|
149
|
-
restartPeer: restartOk(restarts),
|
|
150
|
-
})
|
|
151
|
-
expect(r.state).toBe('install-failed')
|
|
152
|
-
expect(restarts).toEqual([])
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test('mode-b runtime (telegram, no declared peers): empty re-provision, registered router restarted', async () => {
|
|
156
|
-
const root = mkTmp()
|
|
157
|
-
const env = envFor(root)
|
|
158
|
-
writeRuntimeManifest({ runtime: 'telegram', version: '0.10.0' }, { env })
|
|
159
|
-
await upsertPeer({ personality: 'arthur', runtime: 'telegram', cwd: '/tmp/arthur', intelligence: 'natural' }, { env })
|
|
160
|
-
const restarts: string[] = []
|
|
161
|
-
const r = await updateRuntime({
|
|
162
|
-
runtime: 'telegram',
|
|
163
|
-
env,
|
|
164
|
-
runNpx: (_pkg, e) => {
|
|
165
|
-
writeRuntimeManifest({ runtime: 'telegram', version: '0.10.3' }, { env: e })
|
|
166
|
-
return { ok: true }
|
|
167
|
-
},
|
|
168
|
-
npmVersion: () => '0.10.3',
|
|
169
|
-
restartPeer: restartOk(restarts),
|
|
170
|
-
})
|
|
171
|
-
expect(r.state).toBe('updated')
|
|
172
|
-
expect(r.peers).toEqual([]) // operator-add runtime: nothing declared to re-provision
|
|
173
|
-
expect(restarts).toEqual(['arthur']) // the router still restarts onto the new code
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
describe('updateAllRuntimes (--all)', () => {
|
|
178
|
-
test('updates installed runtimes, reports the rest not-installed (never an error)', async () => {
|
|
179
|
-
const root = mkTmp()
|
|
180
|
-
const env = envFor(root)
|
|
181
|
-
writeRuntimeManifest({ runtime: 'telegram', version: '1.0.0' }, { env })
|
|
182
|
-
const results = await updateAllRuntimes({
|
|
183
|
-
env,
|
|
184
|
-
runNpx: () => ({ ok: true }),
|
|
185
|
-
npmVersion: () => '1.0.0',
|
|
186
|
-
restartPeer: restartOk([]),
|
|
187
|
-
})
|
|
188
|
-
const byRt = Object.fromEntries(results.map(r => [r.runtime, r.state]))
|
|
189
|
-
expect(byRt.telegram).toBe('already-latest')
|
|
190
|
-
expect(byRt.notifier).toBe('not-installed')
|
|
191
|
-
})
|
|
192
|
-
})
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// Spawn-flip cutover Block 2, Ф0b-3 slice 3b — the fidelity observer SKIPS supervisor-hosted peers.
|
|
2
|
-
//
|
|
3
|
-
// The observer compares the pty-model vs LIVE TMUX. pty-hosting is now the DEFAULT, so a normal
|
|
4
|
-
// (unmarked) peer has no tmux to compare against — and during a flip a lingering tmux socket for it
|
|
5
|
-
// would surface FALSE divergences. This proves the skip fires IN enumerate even in that race (a live
|
|
6
|
-
// tmux session + a pane-log present): the default peer is NOT enrolled, while a `.no-pty-host` opt-out
|
|
7
|
-
// (tmux) sibling IS. Real-tmux, guarded (a clean CI runner has no tmux; the release machine has it —
|
|
8
|
-
// same pattern as routesend-wake).
|
|
9
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
10
|
-
import { spawnSync } from 'node:child_process'
|
|
11
|
-
import { closeSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
|
-
import { tmpdir } from 'node:os'
|
|
13
|
-
import { join } from 'node:path'
|
|
14
|
-
import { buildSocketPath } from '../core/socket.ts'
|
|
15
|
-
import { enumerate } from './index.ts'
|
|
16
|
-
|
|
17
|
-
const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
|
|
18
|
-
|
|
19
|
-
let sockDir: string
|
|
20
|
-
let logDir: string
|
|
21
|
-
const KEEP = 'keepobs' // .no-pty-host opt-out (tmux) → observed
|
|
22
|
-
const SKIP = 'skipobs' // default (no marker) → pty-hosted → skipped
|
|
23
|
-
const addrOf = (p: string) => `codex-${p}`
|
|
24
|
-
const sockOf = (p: string) => buildSocketPath('codex', p, sockDir)
|
|
25
|
-
|
|
26
|
-
function startTmux(personality: string): void {
|
|
27
|
-
const r = spawnSync(
|
|
28
|
-
'tmux',
|
|
29
|
-
['-S', sockOf(personality), 'new-session', '-d', '-x', '80', '-y', '24', '-s', addrOf(personality), 'cat'],
|
|
30
|
-
{ encoding: 'utf8' },
|
|
31
|
-
)
|
|
32
|
-
if (r.status !== 0) throw new Error(`tmux new-session failed: ${r.stderr}`)
|
|
33
|
-
}
|
|
34
|
-
function killTmux(personality: string): void {
|
|
35
|
-
spawnSync('tmux', ['-S', sockOf(personality), 'kill-server'], { stdio: 'ignore' })
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
beforeAll(() => {
|
|
39
|
-
sockDir = mkdtempSync(join(tmpdir(), 'iapeer-obs-sock-'))
|
|
40
|
-
logDir = mkdtempSync(join(tmpdir(), 'iapeer-obs-log-'))
|
|
41
|
-
if (!tmuxAvailable) return
|
|
42
|
-
// Both peers: a live tmux session + a pane-log (so enrolment is otherwise satisfied).
|
|
43
|
-
for (const p of [KEEP, SKIP]) {
|
|
44
|
-
startTmux(p)
|
|
45
|
-
writeFileSync(join(logDir, `${addrOf(p)}.log`), '')
|
|
46
|
-
}
|
|
47
|
-
// KEEP carries the `.no-pty-host` opt-OUT → it is on tmux, so the observer MUST compare it. SKIP is a
|
|
48
|
-
// DEFAULT (unmarked) peer → pty-hosted → skipped even WITH a lingering live tmux session (the
|
|
49
|
-
// flip-transition race the skip guards against).
|
|
50
|
-
writeFileSync(join(logDir, `${addrOf(KEEP)}.no-pty-host`), '')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
afterAll(() => {
|
|
54
|
-
if (tmuxAvailable) for (const p of [KEEP, SKIP]) killTmux(p)
|
|
55
|
-
rmSync(sockDir, { recursive: true, force: true })
|
|
56
|
-
rmSync(logDir, { recursive: true, force: true })
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe.if(tmuxAvailable)('observer enumerate skips pty-default peers, compares .no-pty-host (tmux) peers', () => {
|
|
60
|
-
test('a default (unmarked, pty-hosted) peer is NOT enrolled despite a live tmux session; a .no-pty-host opt-out one IS', () => {
|
|
61
|
-
const models = new Map<string, any>()
|
|
62
|
-
enumerate(models, { sockDir, logDir, eventLogDir: logDir })
|
|
63
|
-
expect(models.has(addrOf(KEEP))).toBe(true) // .no-pty-host opt-out (tmux) → observed (live tmux + log)
|
|
64
|
-
expect(models.has(addrOf(SKIP))).toBe(false) // default pty-hosted → skipped despite lingering live tmux
|
|
65
|
-
for (const o of models.values()) { try { if (o.fd != null) closeSync(o.fd) } catch { /* */ } }
|
|
66
|
-
})
|
|
67
|
-
})
|
package/src/shadow/gate.test.ts
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
GateTracker,
|
|
4
|
-
classifyDirection,
|
|
5
|
-
classifySampleState,
|
|
6
|
-
evaluateReadyGateFlipEligibility,
|
|
7
|
-
type GateSummary,
|
|
8
|
-
type PeerSample,
|
|
9
|
-
type VerdictGateStat,
|
|
10
|
-
} from './gate.ts'
|
|
11
|
-
|
|
12
|
-
// SYNTHETIC ACCEPTANCE (boris's Block-1 gate): prove the instrument classifies known-behind vs
|
|
13
|
-
// known-ahead and records coverage CORRECTLY — the live proof that it measures right before we
|
|
14
|
-
// trust any pty verdict for a flip. Pure: no tmux, no @xterm, booleans in, gate stats out.
|
|
15
|
-
|
|
16
|
-
const mkSample = (peer: string, runtime: string, over: Partial<PeerSample> = {}): PeerSample => ({
|
|
17
|
-
peer,
|
|
18
|
-
runtime,
|
|
19
|
-
occupancy: { pty: false, tmux: false },
|
|
20
|
-
readyGate: { pty: false, tmux: false },
|
|
21
|
-
liveness: { pty: true, tmux: true },
|
|
22
|
-
bytesFed: 0,
|
|
23
|
-
hasHistory: false,
|
|
24
|
-
...over,
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('classifyDirection (tmux authoritative; true = active/ready/alive pole)', () => {
|
|
28
|
-
test('equal → agree', () => {
|
|
29
|
-
expect(classifyDirection(true, true)).toBe('agree')
|
|
30
|
-
expect(classifyDirection(false, false)).toBe('agree')
|
|
31
|
-
})
|
|
32
|
-
test('pty under-reports (false vs true) → pty-behind (the stale class)', () => {
|
|
33
|
-
expect(classifyDirection(false, true)).toBe('pty-behind')
|
|
34
|
-
})
|
|
35
|
-
test('pty over-reports (true vs false) → pty-ahead', () => {
|
|
36
|
-
expect(classifyDirection(true, false)).toBe('pty-ahead')
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
describe('classifySampleState (coverage regimes — idle must be distinguishable, false-green lesson)', () => {
|
|
41
|
-
test('tmux occupancy → composer-busy (regardless of bytes)', () => {
|
|
42
|
-
expect(classifySampleState(true, 0)).toBe('composer-busy')
|
|
43
|
-
expect(classifySampleState(true, 999)).toBe('composer-busy')
|
|
44
|
-
})
|
|
45
|
-
test('no occupancy + output churn → model-advancing (non-trivial regime)', () => {
|
|
46
|
-
expect(classifySampleState(false, 4096)).toBe('model-advancing')
|
|
47
|
-
})
|
|
48
|
-
test('no occupancy + no churn → idle', () => {
|
|
49
|
-
expect(classifySampleState(false, 0)).toBe('idle')
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
describe('GateTracker — direction is scored hard vs benign per the adjudicated criterion', () => {
|
|
54
|
-
test('pty-BEHIND is ALWAYS hard (zero tolerance) and closes the verdict gate', () => {
|
|
55
|
-
const g = new GateTracker()
|
|
56
|
-
g.sample(0, mkSample('p', 'claude', { occupancy: { pty: false, tmux: true } }))
|
|
57
|
-
const s = g.summary()
|
|
58
|
-
expect(s.verdicts.occupancy.behind).toBe(1)
|
|
59
|
-
expect(s.verdicts.occupancy.gateClean).toBe(false)
|
|
60
|
-
expect(s.hardViolations.some(v => v.kind === 'behind' && v.verdict === 'occupancy')).toBe(true)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('boot-window pty-AHEAD that self-heals within budget → BENIGN (latency recorded)', () => {
|
|
64
|
-
const g = new GateTracker({ settleTicks: 3, convergeBudgetTicks: 3 })
|
|
65
|
-
// tick0: readyGate ahead (pty ready, tmux not) — peer just born → boot window
|
|
66
|
-
g.sample(0, mkSample('p', 'claude', { readyGate: { pty: true, tmux: false } }))
|
|
67
|
-
// tick1: readyGate agrees (both ready) → transient closes, latency 1, boot+within budget → benign
|
|
68
|
-
g.sample(1, mkSample('p', 'claude', { readyGate: { pty: true, tmux: true } }))
|
|
69
|
-
const rg = g.summary().verdicts.readyGate
|
|
70
|
-
expect(rg.aheadBenign).toBe(1)
|
|
71
|
-
expect(rg.aheadHard).toBe(0)
|
|
72
|
-
expect(rg.gateClean).toBe(true)
|
|
73
|
-
expect(rg.selfHeal.count).toBe(1)
|
|
74
|
-
expect(rg.selfHeal.max).toBe(1)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('READY-GATE non-converging pty-AHEAD is HARD, never benign (refinement #3: false-ready = msg-loss)', () => {
|
|
78
|
-
const g = new GateTracker({ settleTicks: 3, convergeBudgetTicks: 3 })
|
|
79
|
-
// hold readyGate ahead (pty ready, tmux NEVER ready → stays in boot window, never converges)
|
|
80
|
-
for (let t = 0; t <= 5; t++) g.sample(t, mkSample('p', 'codex', { readyGate: { pty: true, tmux: false } }))
|
|
81
|
-
const rg = g.summary().verdicts.readyGate
|
|
82
|
-
expect(rg.aheadHard).toBeGreaterThanOrEqual(1)
|
|
83
|
-
expect(rg.aheadBenign).toBe(0)
|
|
84
|
-
expect(rg.gateClean).toBe(false)
|
|
85
|
-
expect(g.summary().hardViolations.some(v => v.kind === 'ahead-nonconverged' && v.verdict === 'readyGate')).toBe(true)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('STEADY-STATE pty-AHEAD is HARD immediately (cannot be a boot self-heal)', () => {
|
|
89
|
-
const g = new GateTracker({ settleTicks: 2, convergeBudgetTicks: 3 })
|
|
90
|
-
// boot the peer: readyGate true for settleTicks
|
|
91
|
-
g.sample(0, mkSample('p', 'claude', { readyGate: { pty: true, tmux: true } }))
|
|
92
|
-
g.sample(1, mkSample('p', 'claude', { readyGate: { pty: true, tmux: true } })) // bootDone now
|
|
93
|
-
// steady-state occupancy ahead → hard 'ahead-steady'
|
|
94
|
-
g.sample(2, mkSample('p', 'claude', { readyGate: { pty: true, tmux: true }, occupancy: { pty: true, tmux: false } }))
|
|
95
|
-
const occ = g.summary().verdicts.occupancy
|
|
96
|
-
expect(occ.aheadHard).toBeGreaterThanOrEqual(1)
|
|
97
|
-
expect(g.summary().hardViolations.some(v => v.kind === 'ahead-steady' && v.verdict === 'occupancy')).toBe(true)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test('full agreement → clean gate, zero hard violations', () => {
|
|
101
|
-
const g = new GateTracker()
|
|
102
|
-
for (let t = 0; t < 5; t++) g.sample(t, mkSample('p', 'claude', { readyGate: { pty: true, tmux: true } }))
|
|
103
|
-
const s = g.summary()
|
|
104
|
-
expect(s.hardViolations).toHaveLength(0)
|
|
105
|
-
expect(s.verdicts.readyGate.gateClean).toBe(true)
|
|
106
|
-
expect(s.verdicts.occupancy.gateClean).toBe(true)
|
|
107
|
-
expect(s.verdicts.liveness.gateClean).toBe(true)
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe('GateTracker — coverage matrix (clean counts only over traversed cells)', () => {
|
|
112
|
-
test('births split fresh vs wake-resume by history-at-first-sight', () => {
|
|
113
|
-
const g = new GateTracker()
|
|
114
|
-
g.tickStart(1)
|
|
115
|
-
g.sample(0, mkSample('fresh-peer', 'claude', { hasHistory: false }))
|
|
116
|
-
g.tickStart(2)
|
|
117
|
-
g.sample(1, mkSample('woke-peer', 'codex', { hasHistory: true })) // history at first sight = wake-resume
|
|
118
|
-
const cov = g.summary().coverage
|
|
119
|
-
expect(cov.births['claude/fresh']).toBe(1)
|
|
120
|
-
expect(cov.births['codex/wake-resume']).toBe(1)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
test('a peer that died and reappeared is a restart', () => {
|
|
124
|
-
const g = new GateTracker()
|
|
125
|
-
g.sample(0, mkSample('p', 'claude'))
|
|
126
|
-
g.peerGone('p')
|
|
127
|
-
g.sample(5, mkSample('p', 'claude'))
|
|
128
|
-
expect(g.summary().coverage.restarts['claude']).toBe(1)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('state cells, fleet width, and history samples are recorded', () => {
|
|
132
|
-
const g = new GateTracker()
|
|
133
|
-
g.tickStart(3) // fleet width 3 this tick
|
|
134
|
-
g.sample(0, mkSample('p', 'claude', { occupancy: { pty: true, tmux: true } })) // composer-busy
|
|
135
|
-
g.sample(1, mkSample('p', 'claude', { bytesFed: 4096 })) // model-advancing
|
|
136
|
-
g.sample(2, mkSample('p', 'claude', { bytesFed: 0 })) // idle
|
|
137
|
-
g.sample(3, mkSample('h', 'codex', { hasHistory: true }))
|
|
138
|
-
const cov = g.summary().coverage
|
|
139
|
-
expect(cov.fleetWidthMax).toBe(3)
|
|
140
|
-
expect(cov.byRuntimeState['claude/composer-busy']).toBe(1)
|
|
141
|
-
expect(cov.byRuntimeState['claude/model-advancing']).toBe(1)
|
|
142
|
-
expect(cov.byRuntimeState['claude/idle']).toBe(1)
|
|
143
|
-
expect(cov.historySamples['codex']).toBe(1)
|
|
144
|
-
expect(cov.runtimes).toEqual(['claude', 'codex'])
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe('GateTracker — robust center cell (gone→back + history + first-ready parity; cadence-proof)', () => {
|
|
149
|
-
// Drive a peer through a gone→back cycle: birth (not-ready), then die. The caller's next sample
|
|
150
|
-
// is the re-enroll (a resume/restart), where the center is evaluated at the first ready sample.
|
|
151
|
-
const goneBack = (g: GateTracker, peer: string, rt: string): void => {
|
|
152
|
-
g.sample(0, mkSample(peer, rt, { hasHistory: true, readyGate: { pty: false, tmux: false } }))
|
|
153
|
-
g.peerGone(peer)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
test('warm-continuous peer (never gone→back) → NOT counted, even ready-with-history (warm-exclusion)', () => {
|
|
157
|
-
const g = new GateTracker()
|
|
158
|
-
g.sample(0, mkSample('w', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: false } }))
|
|
159
|
-
g.sample(1, mkSample('w', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } }))
|
|
160
|
-
expect(g.summary().coverage.readyResumeCaught['codex']).toBeUndefined()
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
test('gone→back + history + first-ready parity → counted (robust, even first-sight-already-ready)', () => {
|
|
164
|
-
const g = new GateTracker()
|
|
165
|
-
goneBack(g, 'c', 'codex')
|
|
166
|
-
g.sample(1, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } })) // re-enroll already-ready (fast resume)
|
|
167
|
-
const cov = g.summary().coverage
|
|
168
|
-
expect(cov.readyResumeCaught['codex']).toBe(1)
|
|
169
|
-
expect(cov.readyResumeCaughtViaTransition['codex']).toBeUndefined() // window not observed (fast) — still counted
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
test('gone→back WITHOUT history → NOT counted (a fresh restart, not a resume-with-history)', () => {
|
|
173
|
-
const g = new GateTracker()
|
|
174
|
-
g.sample(0, mkSample('c', 'codex', { hasHistory: false, readyGate: { pty: false, tmux: false } }))
|
|
175
|
-
g.peerGone('c')
|
|
176
|
-
g.sample(1, mkSample('c', 'codex', { hasHistory: false, readyGate: { pty: true, tmux: true } }))
|
|
177
|
-
expect(g.summary().coverage.readyResumeCaught['codex']).toBeUndefined()
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
test('gone→back + history + DIVERGENCE at first ready (model missed) → NOT counted + dirty (behind)', () => {
|
|
181
|
-
const g = new GateTracker()
|
|
182
|
-
goneBack(g, 'c', 'codex')
|
|
183
|
-
g.sample(1, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: true } })) // tmux ready, model NOT
|
|
184
|
-
const s = g.summary()
|
|
185
|
-
expect(s.coverage.readyResumeCaught['codex']).toBeUndefined()
|
|
186
|
-
expect(s.verdicts.readyGate.behind).toBeGreaterThanOrEqual(1) // the splash-blind bug → dirty → ineligible
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test('counted exactly ONCE per life (dedup: one resume = one center++)', () => {
|
|
190
|
-
const g = new GateTracker()
|
|
191
|
-
goneBack(g, 'c', 'codex')
|
|
192
|
-
for (let t = 1; t <= 4; t++) g.sample(t, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } }))
|
|
193
|
-
expect(g.summary().coverage.readyResumeCaught['codex']).toBe(1)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test('observed not-ready→ready window → counted AND flagged caughtViaTransition (diagnostic only)', () => {
|
|
197
|
-
const g = new GateTracker()
|
|
198
|
-
goneBack(g, 'c', 'codex')
|
|
199
|
-
g.sample(1, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: false } })) // re-enroll not-ready
|
|
200
|
-
g.sample(2, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } })) // becomes ready
|
|
201
|
-
const cov = g.summary().coverage
|
|
202
|
-
expect(cov.readyResumeCaught['codex']).toBe(1)
|
|
203
|
-
expect(cov.readyResumeCaughtViaTransition['codex']).toBe(1) // window was observed
|
|
204
|
-
})
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
describe('evaluateReadyGateFlipEligibility (boris guardrail: the eligibility logic itself under test)', () => {
|
|
208
|
-
const stat = (over: Partial<VerdictGateStat> = {}): VerdictGateStat => ({
|
|
209
|
-
samples: 10,
|
|
210
|
-
agree: 10,
|
|
211
|
-
behind: 0,
|
|
212
|
-
aheadBenign: 0,
|
|
213
|
-
aheadHard: 0,
|
|
214
|
-
selfHeal: { count: 0, max: 0, mean: 0 },
|
|
215
|
-
gateClean: true,
|
|
216
|
-
...over,
|
|
217
|
-
})
|
|
218
|
-
const mkSummary = (over: {
|
|
219
|
-
rgBehind?: number
|
|
220
|
-
rgAheadHard?: number
|
|
221
|
-
center?: Record<string, number>
|
|
222
|
-
births?: Record<string, number>
|
|
223
|
-
restarts?: Record<string, number>
|
|
224
|
-
} = {}): GateSummary => ({
|
|
225
|
-
verdicts: {
|
|
226
|
-
occupancy: stat(),
|
|
227
|
-
readyGate: stat({ behind: over.rgBehind ?? 0, aheadHard: over.rgAheadHard ?? 0 }),
|
|
228
|
-
liveness: stat(),
|
|
229
|
-
},
|
|
230
|
-
hardViolations: [],
|
|
231
|
-
coverage: {
|
|
232
|
-
fleetWidthMax: 2,
|
|
233
|
-
runtimes: ['claude', 'codex'],
|
|
234
|
-
byRuntimeState: {},
|
|
235
|
-
births: over.births ?? {},
|
|
236
|
-
restarts: over.restarts ?? {},
|
|
237
|
-
historySamples: {},
|
|
238
|
-
readyResumeCaught: over.center ?? {},
|
|
239
|
-
readyResumeCaughtViaTransition: {},
|
|
240
|
-
},
|
|
241
|
-
})
|
|
242
|
-
const COMPLETE = {
|
|
243
|
-
center: { codex: 1 },
|
|
244
|
-
births: { 'claude/fresh': 1, 'codex/wake-resume': 1 },
|
|
245
|
-
restarts: { codex: 1 },
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
test('incomplete (empty) → NOT eligible, reasons name every missing cell (actionable)', () => {
|
|
249
|
-
const e = evaluateReadyGateFlipEligibility(mkSummary())
|
|
250
|
-
expect(e.eligible).toBe(false)
|
|
251
|
-
expect(e.reasons.some(r => /center codex×wake-resume×history/.test(r))).toBe(true)
|
|
252
|
-
expect(e.reasons.some(r => /no codex birth/.test(r))).toBe(true)
|
|
253
|
-
expect(e.reasons.some(r => /no claude birth/.test(r))).toBe(true)
|
|
254
|
-
expect(e.reasons.some(r => /no restart/.test(r))).toBe(true)
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
test('complete + clean → eligible', () => {
|
|
258
|
-
const e = evaluateReadyGateFlipEligibility(mkSummary(COMPLETE))
|
|
259
|
-
expect(e.eligible).toBe(true)
|
|
260
|
-
expect(e.reasons).toHaveLength(0)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
test('complete BUT dirty (readyGate behind) → NOT eligible, reason names the dirt', () => {
|
|
264
|
-
const e = evaluateReadyGateFlipEligibility(mkSummary({ ...COMPLETE, rgBehind: 2 }))
|
|
265
|
-
expect(e.eligible).toBe(false)
|
|
266
|
-
expect(e.reasons.some(r => /readyGate dirty: behind=2/.test(r))).toBe(true)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
test('complete except the CENTER cell empty → NOT eligible, center reason only', () => {
|
|
270
|
-
const e = evaluateReadyGateFlipEligibility(mkSummary({ ...COMPLETE, center: {} }))
|
|
271
|
-
expect(e.eligible).toBe(false)
|
|
272
|
-
expect(e.reasons).toHaveLength(1)
|
|
273
|
-
expect(e.reasons[0]).toMatch(/center codex×wake-resume×history: 0/)
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
test('complete except no restart → NOT eligible, restart reason only', () => {
|
|
277
|
-
const e = evaluateReadyGateFlipEligibility(mkSummary({ ...COMPLETE, restarts: {} }))
|
|
278
|
-
expect(e.eligible).toBe(false)
|
|
279
|
-
expect(e.reasons).toEqual(['no restart observed'])
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
test('composer-busy is ORTHOGONAL — its absence never blocks ready-gate eligibility', () => {
|
|
283
|
-
// COMPLETE carries no composer-busy state anywhere; eligibility must still be true.
|
|
284
|
-
expect(evaluateReadyGateFlipEligibility(mkSummary(COMPLETE)).eligible).toBe(true)
|
|
285
|
-
})
|
|
286
|
-
})
|