@raidiant/notifai 9.0.0 → 9.2.0
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/CHANGELOG.md +14 -0
- package/dist/command-session.d.ts +14 -0
- package/dist/command-session.js +30 -0
- package/dist/command-session.js.map +1 -0
- package/dist/commands-doctor.js +17 -15
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.js +3 -29
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-harness-context.js +21 -1
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.d.ts +11 -1
- package/dist/commands-hooks.js +98 -184
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +2 -0
- package/dist/commands-init.js +41 -21
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send.d.ts +1 -0
- package/dist/commands-send.js +62 -14
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +4 -3
- package/dist/commands-setup-proof.js +29 -14
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/guidance-render.d.ts +20 -0
- package/dist/guidance-render.js +42 -0
- package/dist/guidance-render.js.map +1 -0
- package/dist/hooks.d.ts +16 -13
- package/dist/hooks.js +57 -32
- package/dist/hooks.js.map +1 -1
- package/dist/opencode-plugin.js +32 -5
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +21 -12
- package/dist/program.js.map +1 -1
- package/dist/readiness.d.ts +6 -0
- package/dist/readiness.js +37 -0
- package/dist/readiness.js.map +1 -1
- package/dist/send-attempts.d.ts +26 -0
- package/dist/send-attempts.js +139 -0
- package/dist/send-attempts.js.map +1 -0
- package/dist/session-activation.d.ts +4 -8
- package/dist/session-activation.js +25 -14
- package/dist/session-activation.js.map +1 -1
- package/package.json +1 -1
- package/src/command-session.ts +38 -0
- package/src/commands-doctor.ts +18 -28
- package/src/commands-guidance.ts +3 -38
- package/src/commands-harness-context.ts +20 -1
- package/src/commands-hooks.ts +219 -220
- package/src/commands-init.ts +46 -24
- package/src/commands-send.ts +70 -14
- package/src/commands-setup-proof.test.ts +72 -0
- package/src/commands-setup-proof.ts +33 -14
- package/src/commands.test.ts +209 -67
- package/src/commands.ts +1 -0
- package/src/guidance-render.ts +56 -0
- package/src/guidance.test.ts +28 -0
- package/src/hooks.test.ts +42 -137
- package/src/hooks.ts +67 -46
- package/src/install-hooks.test.ts +41 -16
- package/src/opencode-plugin.ts +35 -5
- package/src/program.ts +22 -12
- package/src/readiness.ts +44 -0
- package/src/send-attempts.test.ts +143 -0
- package/src/send-attempts.ts +188 -0
- package/src/session-activation.ts +35 -18
- package/src/skill.test.ts +22 -23
package/src/commands.test.ts
CHANGED
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
} from './install-hooks.js'
|
|
73
73
|
import { readSessionState, writeProjectSession, writeSessionState } from './hooks.js'
|
|
74
74
|
import { nativeSkills as realNativeSkills, type NativeSkill, type NativeSkills, type SkillScope } from './native-skills.js'
|
|
75
|
-
import { CONFIG_KEYS, loadConfig, personalProjectConfigPath, stateDir } from './config.js'
|
|
75
|
+
import { CONFIG_KEYS, loadConfig, personalProjectConfigPath, sessionConfigPath, stateDir } from './config.js'
|
|
76
76
|
import { SETUP_PROOF_STALE_MS, writeSetupProof } from './commands-setup-proof.js'
|
|
77
77
|
import { resetLatestPublishedCliVersionForTest } from './cli-release.js'
|
|
78
78
|
import { activeLogPath, createLogger, logsDiskUsage, readLogRecords } from './logging.js'
|
|
@@ -585,7 +585,7 @@ describe('command contracts', () => {
|
|
|
585
585
|
expect(io.errLines.join('\n')).toContain('project')
|
|
586
586
|
})
|
|
587
587
|
|
|
588
|
-
it('
|
|
588
|
+
it('persists one opaque retry attempt after an ambiguous submit failure', async () => {
|
|
589
589
|
const io = new CapturedIo()
|
|
590
590
|
let attemptedKey = ''
|
|
591
591
|
const client = {
|
|
@@ -611,24 +611,22 @@ describe('command contracts', () => {
|
|
|
611
611
|
).toBe(EXIT.network)
|
|
612
612
|
|
|
613
613
|
expect(attemptedKey).toMatch(/^cli-/)
|
|
614
|
-
expect(io.errLines.join('\n')).toContain(`--
|
|
614
|
+
expect(io.errLines.join('\n')).toContain('exact semantic send with `--retry`')
|
|
615
|
+
expect(io.errLines.join('\n')).toMatch(/Opaque retry attempt: sat_/)
|
|
615
616
|
expect(
|
|
616
617
|
readLogRecords(deps.env, { event: ['send.attempt'] }).records.at(-1)?.data,
|
|
617
618
|
).toMatchObject({
|
|
618
619
|
idempotency_key: attemptedKey,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
session_id: null,
|
|
620
|
+
attempt_id: expect.stringMatching(/^sat_/),
|
|
621
|
+
replay: false,
|
|
622
622
|
})
|
|
623
623
|
rmSync(root, { recursive: true, force: true })
|
|
624
624
|
})
|
|
625
625
|
|
|
626
|
-
it('prints
|
|
626
|
+
it('prints an opaque retry instruction for an ambiguous server failure', async () => {
|
|
627
627
|
const io = new CapturedIo()
|
|
628
|
-
let attemptedKey = ''
|
|
629
628
|
const client = {
|
|
630
|
-
submit: async (
|
|
631
|
-
attemptedKey = body.idempotency_key
|
|
629
|
+
submit: async () => {
|
|
632
630
|
throw new ApiCallError(503, 'temporarily_unavailable', 'Try again later.')
|
|
633
631
|
},
|
|
634
632
|
} as unknown as ApiClient
|
|
@@ -640,15 +638,40 @@ describe('command contracts', () => {
|
|
|
640
638
|
body: 'The service failed while accepting the result.',
|
|
641
639
|
}),
|
|
642
640
|
).toBe(EXIT.network)
|
|
643
|
-
expect(io.errLines.join('\n')).toContain(`--
|
|
641
|
+
expect(io.errLines.join('\n')).toContain('exact semantic send with `--retry`')
|
|
644
642
|
})
|
|
645
643
|
|
|
646
|
-
it('
|
|
644
|
+
it('reuses the opaque attempt key only after an explicit exact semantic retry', async () => {
|
|
645
|
+
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-send-retry-'))
|
|
647
646
|
const io = new CapturedIo()
|
|
648
|
-
|
|
647
|
+
const keys: string[] = []
|
|
648
|
+
let calls = 0
|
|
649
649
|
const client = {
|
|
650
650
|
submit: async (body: SubmitNotificationRequestT) => {
|
|
651
|
-
|
|
651
|
+
keys.push(body.idempotency_key)
|
|
652
|
+
calls += 1
|
|
653
|
+
if (calls === 1) throw new NetworkError('killed after an ambiguous boundary')
|
|
654
|
+
return { ...receipt, replayed: true }
|
|
655
|
+
},
|
|
656
|
+
} as unknown as ApiClient
|
|
657
|
+
const deps = {
|
|
658
|
+
...makeDeps(io, client),
|
|
659
|
+
cwd: root,
|
|
660
|
+
env: { XDG_CONFIG_HOME: path.join(root, 'config'), XDG_STATE_HOME: path.join(root, 'state') },
|
|
661
|
+
}
|
|
662
|
+
const semantic = { kind: 'done', title: 'Recovery completed', body: 'The exact work is done.' }
|
|
663
|
+
|
|
664
|
+
expect(await sendCommand(deps, semantic)).toBe(EXIT.network)
|
|
665
|
+
expect(await sendCommand(deps, { ...semantic, retry: true })).toBe(EXIT.ok)
|
|
666
|
+
expect(keys).toHaveLength(2)
|
|
667
|
+
expect(keys[1]).toBe(keys[0])
|
|
668
|
+
expect(await sendCommand(deps, { ...semantic, retry: true })).toBe(EXIT.usage)
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('routes a send with no active Companion devices into the setup coordinator', async () => {
|
|
672
|
+
const io = new CapturedIo()
|
|
673
|
+
const client = {
|
|
674
|
+
submit: async () => {
|
|
652
675
|
throw new ApiCallError(
|
|
653
676
|
409,
|
|
654
677
|
'no_active_devices',
|
|
@@ -667,7 +690,7 @@ describe('command contracts', () => {
|
|
|
667
690
|
).toBe(EXIT.failed)
|
|
668
691
|
|
|
669
692
|
expect(io.errLines.join('\n')).toMatch(/next:.*`notifai init`.*Companion/i)
|
|
670
|
-
expect(io.errLines.join('\n')).toContain(`--
|
|
693
|
+
expect(io.errLines.join('\n')).toContain('exact original Notification Request with `--retry`')
|
|
671
694
|
expect(io.errLines.join('\n')).toMatch(/verification request does not replace this Agent Event/i)
|
|
672
695
|
})
|
|
673
696
|
|
|
@@ -1146,11 +1169,13 @@ describe('command contracts', () => {
|
|
|
1146
1169
|
expect(grammar).not.toContain(".option('--detail")
|
|
1147
1170
|
expect(grammar).not.toContain(".option('--session <")
|
|
1148
1171
|
expect(grammar).toContain(".option('--body-file")
|
|
1149
|
-
expect(grammar).toContain(".option('--session-id")
|
|
1150
1172
|
expect(grammar).toContain("'--session-label <text>'")
|
|
1151
1173
|
expect(grammar).toContain("'--image <path|url|media_id>'")
|
|
1152
1174
|
expect(grammar).toContain("'--image-alt <text>'")
|
|
1153
1175
|
}
|
|
1176
|
+
expect(sendGrammar).toContain(".option('--session-id")
|
|
1177
|
+
expect(sendGrammar).toContain(".option('--retry'")
|
|
1178
|
+
expect(askGrammar).not.toContain(".option('--session-id")
|
|
1154
1179
|
// Kind now selects the sound, so the help must say so — and must not carry
|
|
1155
1180
|
// the retired separation it replaced.
|
|
1156
1181
|
expect(source).toContain('Kind is required, and it selects the sound')
|
|
@@ -1624,7 +1649,7 @@ describe('command contracts', () => {
|
|
|
1624
1649
|
expect(events[1]?.data).toMatchObject({ text_chars: 'I will deploy staging now.'.length })
|
|
1625
1650
|
})
|
|
1626
1651
|
|
|
1627
|
-
it('clears
|
|
1652
|
+
it('clears the stable-id session obligation after acknowledgement from another checkout', async () => {
|
|
1628
1653
|
const io = new CapturedIo()
|
|
1629
1654
|
const client = {
|
|
1630
1655
|
putAgentAcknowledgement: async () => ({
|
|
@@ -1647,6 +1672,7 @@ describe('command contracts', () => {
|
|
|
1647
1672
|
acknowledgement_due: [{ request_id: receipt.request_id, recorded_at: now }],
|
|
1648
1673
|
})
|
|
1649
1674
|
writeProjectSession(deps.cwd, deps.env, 'ack-session', now, 'codex')
|
|
1675
|
+
deps.cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-ack-other-checkout-'))
|
|
1650
1676
|
|
|
1651
1677
|
expect(
|
|
1652
1678
|
await acknowledgeCommand(deps, receipt.request_id, {
|
|
@@ -1868,7 +1894,7 @@ describe('command contracts', () => {
|
|
|
1868
1894
|
expect(readSessionState('close-unpushed', deps.env).pending).toBeUndefined()
|
|
1869
1895
|
})
|
|
1870
1896
|
|
|
1871
|
-
it('close withdraws one unpushed question by
|
|
1897
|
+
it('close withdraws one unpushed question by stable id from another checkout', async () => {
|
|
1872
1898
|
const io = new CapturedIo()
|
|
1873
1899
|
const client = {
|
|
1874
1900
|
closeReplies: async () => {
|
|
@@ -1889,6 +1915,7 @@ describe('command contracts', () => {
|
|
|
1889
1915
|
],
|
|
1890
1916
|
})
|
|
1891
1917
|
writeProjectSession(root, deps.env, 'close-one', Date.now(), 'codex')
|
|
1918
|
+
deps.cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-close-other-checkout-'))
|
|
1892
1919
|
|
|
1893
1920
|
expect(await closeCommand(deps, 'q_local', { json: true })).toBe(EXIT.ok)
|
|
1894
1921
|
expect(JSON.parse(io.outLines[0] ?? '{}')).toEqual({
|
|
@@ -1974,7 +2001,7 @@ describe('command contracts', () => {
|
|
|
1974
2001
|
expect(readLogRecords(deps.env, { event: ['reply.received'] }).records).toHaveLength(1)
|
|
1975
2002
|
})
|
|
1976
2003
|
|
|
1977
|
-
it('resolves replies --pending
|
|
2004
|
+
it('resolves replies --pending by exact session from another checkout', async () => {
|
|
1978
2005
|
const io = new CapturedIo()
|
|
1979
2006
|
const client = {
|
|
1980
2007
|
replies: async () => replyResponse([reply]),
|
|
@@ -1985,11 +2012,15 @@ describe('command contracts', () => {
|
|
|
1985
2012
|
deps.env = {
|
|
1986
2013
|
XDG_CONFIG_HOME: path.join(root, 'config'),
|
|
1987
2014
|
XDG_STATE_HOME: path.join(root, 'state'),
|
|
2015
|
+
CODEX_THREAD_ID: 'pending-session',
|
|
1988
2016
|
}
|
|
1989
2017
|
writeSessionState('pending-session', deps.env, {
|
|
2018
|
+
harness: 'codex',
|
|
2019
|
+
last_prompt_at: Date.now(),
|
|
1990
2020
|
pending: [{ question: 'Deploy?', request_id: receipt.request_id }],
|
|
1991
2021
|
})
|
|
1992
2022
|
writeProjectSession(root, deps.env, 'pending-session', Date.now(), 'codex')
|
|
2023
|
+
deps.cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-pending-other-checkout-'))
|
|
1993
2024
|
|
|
1994
2025
|
expect(await repliesCommand(deps, undefined, { pending: true })).toBe(EXIT.ok)
|
|
1995
2026
|
expect(io.outLines).toEqual([
|
|
@@ -3246,13 +3277,13 @@ describe('stable hook installation', () => {
|
|
|
3246
3277
|
).toBe(EXIT.ok)
|
|
3247
3278
|
const globalFile = settingsFile('opencode', true, cwd, env)
|
|
3248
3279
|
const current = readFileSync(globalFile, 'utf8')
|
|
3249
|
-
writeFileSync(globalFile, current.replace('const ADAPTER_VERSION =
|
|
3280
|
+
writeFileSync(globalFile, current.replace('const ADAPTER_VERSION = 12', 'const ADAPTER_VERSION = 11'))
|
|
3250
3281
|
|
|
3251
3282
|
io.outLines = []
|
|
3252
3283
|
expect(hooksInstallCommand(deps, { harness: 'opencode', execPath, scriptPath })).toBe(EXIT.ok)
|
|
3253
3284
|
expect(existsSync(path.join(cwd, '.opencode', 'plugins', 'notifai.js'))).toBe(false)
|
|
3254
3285
|
const refreshed = readFileSync(globalFile, 'utf8')
|
|
3255
|
-
expect(refreshed).toContain('const ADAPTER_VERSION =
|
|
3286
|
+
expect(refreshed).toContain('const ADAPTER_VERSION = 12')
|
|
3256
3287
|
expect(refreshed).toContain('experimental.chat.system.transform')
|
|
3257
3288
|
expect(io.outLines.join('\n')).toMatch(/Installed opencode hooks/)
|
|
3258
3289
|
})
|
|
@@ -5359,7 +5390,7 @@ describe('init', () => {
|
|
|
5359
5390
|
expect(out.match(/^Next:/gm)).toHaveLength(1)
|
|
5360
5391
|
})
|
|
5361
5392
|
|
|
5362
|
-
it('
|
|
5393
|
+
it('reuses proof across worktrees of one Project on the same Approved Machine', async () => {
|
|
5363
5394
|
const stateRoot = mkdtempSync(path.join(os.tmpdir(), 'init-worktrees-state-'))
|
|
5364
5395
|
let submitCalls = 0
|
|
5365
5396
|
const client = {
|
|
@@ -5391,7 +5422,62 @@ describe('init', () => {
|
|
|
5391
5422
|
).toBe(EXIT.ok)
|
|
5392
5423
|
}
|
|
5393
5424
|
|
|
5394
|
-
expect(submitCalls).toBe(
|
|
5425
|
+
expect(submitCalls).toBe(1)
|
|
5426
|
+
})
|
|
5427
|
+
|
|
5428
|
+
it('returns one authoritative structured final readiness without duplicate probes', async () => {
|
|
5429
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-json-ready-'))
|
|
5430
|
+
const io = new InteractiveIo()
|
|
5431
|
+
let healthCalls = 0
|
|
5432
|
+
let compatibilityCalls = 0
|
|
5433
|
+
let deviceCalls = 0
|
|
5434
|
+
const client = {
|
|
5435
|
+
health: async () => {
|
|
5436
|
+
healthCalls += 1
|
|
5437
|
+
return true
|
|
5438
|
+
},
|
|
5439
|
+
compatibility: async () => {
|
|
5440
|
+
compatibilityCalls += 1
|
|
5441
|
+
return currentCompatibility
|
|
5442
|
+
},
|
|
5443
|
+
listDevices: async () => {
|
|
5444
|
+
deviceCalls += 1
|
|
5445
|
+
return { devices: [readyIphone] }
|
|
5446
|
+
},
|
|
5447
|
+
evidence: async (requestId: string) =>
|
|
5448
|
+
setupEvidence(requestId, {
|
|
5449
|
+
state: 'observed',
|
|
5450
|
+
observed_at: '2026-08-25T00:00:02.000Z',
|
|
5451
|
+
latency_ms: 1_000,
|
|
5452
|
+
}),
|
|
5453
|
+
} as unknown as ApiClient
|
|
5454
|
+
mkdirSync(path.join(cwd, '.notifai'), { recursive: true })
|
|
5455
|
+
writeFileSync(path.join(cwd, '.notifai', 'config.toml'), 'project = "json-ready"\n')
|
|
5456
|
+
const deps = { ...makeDeps(io, client), cwd, env: isolatedEnv(cwd) }
|
|
5457
|
+
writeSetupProof(deps, {
|
|
5458
|
+
request_id: 'req_json_ready',
|
|
5459
|
+
device_id: readyIphone.device_id,
|
|
5460
|
+
project: 'json-ready',
|
|
5461
|
+
started_at: '2026-08-25T00:00:00.000Z',
|
|
5462
|
+
})
|
|
5463
|
+
|
|
5464
|
+
expect(await initCommand(deps, { json: true, hooks: false, skills: false })).toBe(EXIT.ok)
|
|
5465
|
+
expect(io.prompts).toEqual([])
|
|
5466
|
+
expect(io.outLines).toHaveLength(1)
|
|
5467
|
+
const result = JSON.parse(io.outLines[0] ?? '{}') as {
|
|
5468
|
+
ready: boolean
|
|
5469
|
+
can_send: boolean
|
|
5470
|
+
question_routing_ready: boolean
|
|
5471
|
+
states: Array<{ id: string; status: string; remedy: unknown }>
|
|
5472
|
+
}
|
|
5473
|
+
expect(result.ready).toBe(true)
|
|
5474
|
+
expect(result.can_send).toBe(true)
|
|
5475
|
+
expect(result.question_routing_ready).toBe(false)
|
|
5476
|
+
expect(result.states.length).toBeGreaterThan(8)
|
|
5477
|
+
expect(result.states.every((state) => 'remedy' in state)).toBe(true)
|
|
5478
|
+
expect(healthCalls).toBe(1)
|
|
5479
|
+
expect(compatibilityCalls).toBe(1)
|
|
5480
|
+
expect(deviceCalls).toBe(1)
|
|
5395
5481
|
})
|
|
5396
5482
|
})
|
|
5397
5483
|
|
|
@@ -5711,8 +5797,8 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5711
5797
|
|
|
5712
5798
|
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
5713
5799
|
const said = io.errLines.join(' ')
|
|
5714
|
-
expect(said).toMatch(/
|
|
5715
|
-
expect(said).toContain('notifai
|
|
5800
|
+
expect(said).toMatch(/Codex hooks are not installed/i)
|
|
5801
|
+
expect(said).toContain('notifai init --json')
|
|
5716
5802
|
expect(said).not.toMatch(/Claude Code: send one new prompt|OpenCode: restart/)
|
|
5717
5803
|
})
|
|
5718
5804
|
|
|
@@ -5729,12 +5815,13 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5729
5815
|
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
5730
5816
|
|
|
5731
5817
|
expect(hooksInstallCommand(deps, { harness: 'codex', execPath, scriptPath })).toBe(EXIT.ok)
|
|
5732
|
-
|
|
5818
|
+
trustInstalledCodexHooks(cwd, env)
|
|
5819
|
+
writeSessionState('codex-other-thread', env, { harness: 'codex', last_prompt_at: 42 })
|
|
5733
5820
|
writeProjectSession(cwd, env, 'codex-other-thread', 42, 'codex')
|
|
5734
5821
|
io.outLines = []
|
|
5735
5822
|
|
|
5736
5823
|
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
5737
|
-
expect(io.errLines.join(' ')).toMatch(/
|
|
5824
|
+
expect(io.errLines.join(' ')).toMatch(/exact Codex session has not fired UserPromptSubmit/i)
|
|
5738
5825
|
expect(io.outLines).not.toContain(
|
|
5739
5826
|
'Question registered. Ask it in the conversation as usual and end your turn.',
|
|
5740
5827
|
)
|
|
@@ -5754,9 +5841,9 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5754
5841
|
|
|
5755
5842
|
expect(hooksInstallCommand(deps, { harness: 'codex', execPath, scriptPath })).toBe(EXIT.ok)
|
|
5756
5843
|
trustInstalledCodexHooks(cwd, env)
|
|
5757
|
-
writeSessionState('codex-current-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
5844
|
+
writeSessionState('codex-current-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
5758
5845
|
writeProjectSession(cwd, env, 'codex-current-thread', 42, 'codex')
|
|
5759
|
-
writeSessionState('claude-concurrent', env, { last_prompt_at: 43, last_stop_at: 43 })
|
|
5846
|
+
writeSessionState('claude-concurrent', env, { harness: 'claude-code', last_prompt_at: 43, last_stop_at: 43 })
|
|
5760
5847
|
writeProjectSession(cwd, env, 'claude-concurrent', 43, 'claude-code')
|
|
5761
5848
|
io.outLines = []
|
|
5762
5849
|
|
|
@@ -5771,6 +5858,49 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5771
5858
|
).toBe('Gentle Salmon')
|
|
5772
5859
|
})
|
|
5773
5860
|
|
|
5861
|
+
it('uses exact lifecycle state across checkouts while Project stays invocation-owned', () => {
|
|
5862
|
+
const first = scratchDir('notifai-exact-session-first-')
|
|
5863
|
+
const second = scratchDir('notifai-exact-session-second-')
|
|
5864
|
+
const io = new CapturedIo()
|
|
5865
|
+
const env = {
|
|
5866
|
+
...isolatedEnv(first),
|
|
5867
|
+
CLAUDECODE: '1',
|
|
5868
|
+
CLAUDE_CODE_SESSION_ID: 'claude-cross-checkout',
|
|
5869
|
+
}
|
|
5870
|
+
const firstDeps = { ...makeDeps(io, {} as ApiClient), cwd: first, env, now: () => 42 }
|
|
5871
|
+
expect(
|
|
5872
|
+
hooksInstallCommand(firstDeps, {
|
|
5873
|
+
harness: 'claude-code',
|
|
5874
|
+
execPath,
|
|
5875
|
+
scriptPath,
|
|
5876
|
+
}),
|
|
5877
|
+
).toBe(EXIT.ok)
|
|
5878
|
+
writeSessionState('claude-cross-checkout', env, {
|
|
5879
|
+
harness: 'claude-code',
|
|
5880
|
+
activation_cwd: first,
|
|
5881
|
+
last_prompt_at: 42,
|
|
5882
|
+
last_stop_at: 41,
|
|
5883
|
+
})
|
|
5884
|
+
writeProjectSession(first, env, 'claude-cross-checkout', 42, 'claude-code')
|
|
5885
|
+
mkdirSync(path.join(second, '.notifai'), { recursive: true })
|
|
5886
|
+
writeFileSync(
|
|
5887
|
+
path.join(second, '.notifai', 'config.toml'),
|
|
5888
|
+
'project = "second-project"\nask_notifications = false\n',
|
|
5889
|
+
)
|
|
5890
|
+
mkdirSync(path.dirname(sessionConfigPath('claude-cross-checkout', env)), { recursive: true })
|
|
5891
|
+
writeFileSync(sessionConfigPath('claude-cross-checkout', env), 'ask_notifications = true\n')
|
|
5892
|
+
io.outLines = []
|
|
5893
|
+
|
|
5894
|
+
const secondDeps = { ...firstDeps, cwd: second }
|
|
5895
|
+
expect(askCommand(secondDeps, 'Use this checkout?', {})).toBe(EXIT.ok)
|
|
5896
|
+
const pending = readSessionState('claude-cross-checkout', env).pending?.[0]
|
|
5897
|
+
expect(pending?.project).toBe('second-project')
|
|
5898
|
+
expect(pending?.source).toMatchObject({
|
|
5899
|
+
session_id: 'claude-cross-checkout',
|
|
5900
|
+
harness: 'claude-code',
|
|
5901
|
+
})
|
|
5902
|
+
})
|
|
5903
|
+
|
|
5774
5904
|
it('uploads ask images before registration and freezes canonical body media', async () => {
|
|
5775
5905
|
const cwd = scratchDir('notifai-ask-media-')
|
|
5776
5906
|
const first = path.join(cwd, 'first.png')
|
|
@@ -5805,7 +5935,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5805
5935
|
|
|
5806
5936
|
expect(hooksInstallCommand(deps, { harness: 'codex', execPath, scriptPath })).toBe(EXIT.ok)
|
|
5807
5937
|
trustInstalledCodexHooks(cwd, env)
|
|
5808
|
-
writeSessionState('codex-media-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
5938
|
+
writeSessionState('codex-media-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
5809
5939
|
writeProjectSession(cwd, env, 'codex-media-thread', 42, 'codex')
|
|
5810
5940
|
io.outLines = []
|
|
5811
5941
|
|
|
@@ -5856,7 +5986,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5856
5986
|
hooksInstallCommand(deps, { harness: 'claude-code', global: true, execPath, scriptPath }),
|
|
5857
5987
|
).toBe(EXIT.ok)
|
|
5858
5988
|
trustInstalledCodexHooks(cwd, env)
|
|
5859
|
-
writeSessionState('codex-current-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
5989
|
+
writeSessionState('codex-current-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
5860
5990
|
writeProjectSession(cwd, env, 'codex-current-thread', 42, 'codex')
|
|
5861
5991
|
io.outLines = []
|
|
5862
5992
|
|
|
@@ -5886,9 +6016,9 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5886
6016
|
trustInstalledCodexHooks(cwd, env)
|
|
5887
6017
|
// Both sessions are live here. The parent fired when its own turn began;
|
|
5888
6018
|
// the child fired for the turn that is running this command.
|
|
5889
|
-
writeSessionState('codex-orchestrator', env, { last_prompt_at: 40, last_stop_at: 39 })
|
|
6019
|
+
writeSessionState('codex-orchestrator', env, { harness: 'codex', last_prompt_at: 40, last_stop_at: 39 })
|
|
5890
6020
|
writeProjectSession(cwd, env, 'codex-orchestrator', 40, 'codex')
|
|
5891
|
-
writeSessionState('claude-current', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6021
|
+
writeSessionState('claude-current', env, { harness: 'claude-code', last_prompt_at: 42, last_stop_at: 41 })
|
|
5892
6022
|
writeProjectSession(cwd, env, 'claude-current', 42, 'claude-code')
|
|
5893
6023
|
io.outLines = []
|
|
5894
6024
|
|
|
@@ -5921,7 +6051,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5921
6051
|
expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(
|
|
5922
6052
|
EXIT.ok,
|
|
5923
6053
|
)
|
|
5924
|
-
writeSessionState('orca-claude-question', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6054
|
+
writeSessionState('orca-claude-question', env, { harness: 'claude-code', last_prompt_at: 42, last_stop_at: 41 })
|
|
5925
6055
|
writeProjectSession(cwd, env, 'orca-claude-question', 42, 'claude-code')
|
|
5926
6056
|
io.outLines = []
|
|
5927
6057
|
|
|
@@ -5949,7 +6079,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5949
6079
|
hooksInstallCommand(deps, { harness: 'claude-code', global: true, execPath, scriptPath }),
|
|
5950
6080
|
).toBe(EXIT.ok)
|
|
5951
6081
|
trustInstalledCodexHooks(cwd, env)
|
|
5952
|
-
writeSessionState('codex-current-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6082
|
+
writeSessionState('codex-current-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
5953
6083
|
writeProjectSession(cwd, env, 'codex-current-thread', 42, 'codex')
|
|
5954
6084
|
|
|
5955
6085
|
const readiness = await assessReadiness(deps)
|
|
@@ -5987,7 +6117,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
5987
6117
|
|
|
5988
6118
|
io.errLines = []
|
|
5989
6119
|
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
5990
|
-
expect(io.errLines.join(' ')).toMatch(/Several harness
|
|
6120
|
+
expect(io.errLines.join(' ')).toMatch(/Several harness sessions could own this shell/i)
|
|
5991
6121
|
})
|
|
5992
6122
|
|
|
5993
6123
|
it('makes one work-resumption commitment per offered answer part of the asking turn', () => {
|
|
@@ -6004,7 +6134,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6004
6134
|
|
|
6005
6135
|
expect(hooksInstallCommand(deps, { harness: 'codex', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6006
6136
|
trustInstalledCodexHooks(cwd, env)
|
|
6007
|
-
writeSessionState('codex-commitment-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6137
|
+
writeSessionState('codex-commitment-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
6008
6138
|
writeProjectSession(cwd, env, 'codex-commitment-thread', 42, 'codex')
|
|
6009
6139
|
io.outLines = []
|
|
6010
6140
|
|
|
@@ -6040,7 +6170,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6040
6170
|
|
|
6041
6171
|
expect(hooksInstallCommand(deps, { harness: 'codex', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6042
6172
|
trustInstalledCodexHooks(cwd, env)
|
|
6043
|
-
writeSessionState('codex-free-text-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6173
|
+
writeSessionState('codex-free-text-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
6044
6174
|
writeProjectSession(cwd, env, 'codex-free-text-thread', 42, 'codex')
|
|
6045
6175
|
io.outLines = []
|
|
6046
6176
|
|
|
@@ -6072,7 +6202,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6072
6202
|
configFile,
|
|
6073
6203
|
readFileSync(configFile, 'utf8').replace(codexHookIdentityHash(stop!), 'sha256:obsolete'),
|
|
6074
6204
|
)
|
|
6075
|
-
writeSessionState('codex-current-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6205
|
+
writeSessionState('codex-current-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
6076
6206
|
writeProjectSession(cwd, env, 'codex-current-thread', 42, 'codex')
|
|
6077
6207
|
io.outLines = []
|
|
6078
6208
|
|
|
@@ -6147,12 +6277,12 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6147
6277
|
expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(
|
|
6148
6278
|
EXIT.ok,
|
|
6149
6279
|
)
|
|
6150
|
-
writeSessionState('claude-other', env, { last_prompt_at: 42 })
|
|
6280
|
+
writeSessionState('claude-other', env, { harness: 'claude-code', last_prompt_at: 42 })
|
|
6151
6281
|
writeProjectSession(cwd, env, 'claude-other', 42, 'claude-code')
|
|
6152
6282
|
io.outLines = []
|
|
6153
6283
|
|
|
6154
6284
|
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
6155
|
-
expect(io.errLines.join(' ')).toMatch(/
|
|
6285
|
+
expect(io.errLines.join(' ')).toMatch(/exact Claude Code session has not fired UserPromptSubmit/i)
|
|
6156
6286
|
})
|
|
6157
6287
|
|
|
6158
6288
|
it('accepts a Claude Code subprocess when its documented session id owns the pointer', () => {
|
|
@@ -6170,7 +6300,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6170
6300
|
expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(
|
|
6171
6301
|
EXIT.ok,
|
|
6172
6302
|
)
|
|
6173
|
-
writeSessionState('claude-parent-loop', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6303
|
+
writeSessionState('claude-parent-loop', env, { harness: 'claude-code', last_prompt_at: 42, last_stop_at: 41 })
|
|
6174
6304
|
writeProjectSession(cwd, env, 'claude-parent-loop', 42, 'claude-code')
|
|
6175
6305
|
io.outLines = []
|
|
6176
6306
|
|
|
@@ -6191,7 +6321,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6191
6321
|
expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(
|
|
6192
6322
|
EXIT.ok,
|
|
6193
6323
|
)
|
|
6194
|
-
writeSessionState('claude-last-writer', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6324
|
+
writeSessionState('claude-last-writer', env, { harness: 'claude-code', last_prompt_at: 42, last_stop_at: 41 })
|
|
6195
6325
|
writeProjectSession(cwd, env, 'claude-last-writer', 42, 'claude-code')
|
|
6196
6326
|
io.outLines = []
|
|
6197
6327
|
|
|
@@ -6220,7 +6350,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6220
6350
|
}),
|
|
6221
6351
|
})
|
|
6222
6352
|
trustInstalledCodexHooks(cwd, env)
|
|
6223
|
-
writeSessionState('codex-current-thread', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6353
|
+
writeSessionState('codex-current-thread', env, { harness: 'codex', last_prompt_at: 42, last_stop_at: 41 })
|
|
6224
6354
|
writeProjectSession(cwd, env, 'codex-current-thread', 42, 'codex')
|
|
6225
6355
|
io.outLines = []
|
|
6226
6356
|
|
|
@@ -6243,7 +6373,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6243
6373
|
expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(
|
|
6244
6374
|
EXIT.ok,
|
|
6245
6375
|
)
|
|
6246
|
-
writeSessionState('claude-current', env, { last_prompt_at: 42 })
|
|
6376
|
+
writeSessionState('claude-current', env, { harness: 'claude-code', last_prompt_at: 42 })
|
|
6247
6377
|
writeProjectSession(cwd, env, 'claude-current', 42, 'claude-code')
|
|
6248
6378
|
io.outLines = []
|
|
6249
6379
|
|
|
@@ -6268,12 +6398,12 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6268
6398
|
}
|
|
6269
6399
|
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6270
6400
|
expect(hooksInstallCommand(deps, { harness: 'opencode', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6271
|
-
writeSessionState('opencode-other', env, { last_prompt_at: 42 })
|
|
6401
|
+
writeSessionState('opencode-other', env, { harness: 'opencode', last_prompt_at: 42 })
|
|
6272
6402
|
writeProjectSession(cwd, env, 'opencode-other', 42, 'opencode')
|
|
6273
6403
|
io.outLines = []
|
|
6274
6404
|
|
|
6275
6405
|
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
6276
|
-
expect(io.errLines.join(' ')).toMatch(/
|
|
6406
|
+
expect(io.errLines.join(' ')).toMatch(/no proven answer continuation/i)
|
|
6277
6407
|
})
|
|
6278
6408
|
|
|
6279
6409
|
it('rejects OpenCode before registration even with an exact matching pointer', () => {
|
|
@@ -6288,7 +6418,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6288
6418
|
}
|
|
6289
6419
|
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6290
6420
|
expect(hooksInstallCommand(deps, { harness: 'opencode', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6291
|
-
writeSessionState('opencode-current', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6421
|
+
writeSessionState('opencode-current', env, { harness: 'opencode', last_prompt_at: 42, last_stop_at: 41 })
|
|
6292
6422
|
writeProjectSession(cwd, env, 'opencode-current', 42, 'opencode')
|
|
6293
6423
|
io.outLines = []
|
|
6294
6424
|
|
|
@@ -6307,7 +6437,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6307
6437
|
}
|
|
6308
6438
|
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6309
6439
|
expect(hooksInstallCommand(deps, { harness: 'cursor', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6310
|
-
writeSessionState('cursor-live', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
6440
|
+
writeSessionState('cursor-live', env, { harness: 'cursor', last_prompt_at: 42, last_stop_at: 41 })
|
|
6311
6441
|
writeProjectSession(cwd, env, 'cursor-live', 42, 'cursor')
|
|
6312
6442
|
io.outLines = []
|
|
6313
6443
|
|
|
@@ -6330,7 +6460,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6330
6460
|
}
|
|
6331
6461
|
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6332
6462
|
expect(hooksInstallCommand(deps, { harness: 'codex', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6333
|
-
writeSessionState('codex-live', env, { last_prompt_at: 42 })
|
|
6463
|
+
writeSessionState('codex-live', env, { harness: 'codex', last_prompt_at: 42 })
|
|
6334
6464
|
writeProjectSession(cwd, env, 'codex-live', 42, 'codex')
|
|
6335
6465
|
io.outLines = []
|
|
6336
6466
|
|
|
@@ -6361,7 +6491,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6361
6491
|
|
|
6362
6492
|
const pointer = readiness.states.find((s) => s.id === 'hooks-active-session')
|
|
6363
6493
|
expect(pointer?.status).toBe('optional-gap')
|
|
6364
|
-
expect(pointer?.detail).toMatch(/has not published
|
|
6494
|
+
expect(pointer?.detail).toMatch(/has not published exact lifecycle state/)
|
|
6365
6495
|
expect(pointer?.remedy?.summary).toMatch(/send one Claude Code prompt/)
|
|
6366
6496
|
expect(pointer?.remedy?.by === 'user-here' ? pointer.remedy.command : '').toBe(
|
|
6367
6497
|
'notifai doctor',
|
|
@@ -6580,7 +6710,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6580
6710
|
expect(payload).toMatchObject({ ok: false, exit_code: EXIT.failed })
|
|
6581
6711
|
})
|
|
6582
6712
|
|
|
6583
|
-
it('
|
|
6713
|
+
it('returns a stable exact-session failure when lifecycle context is absent', () => {
|
|
6584
6714
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-firstrun-'))
|
|
6585
6715
|
mkdirSync(path.join(cwd, '.claude'), { recursive: true })
|
|
6586
6716
|
applyPlan(path.join(cwd, '.claude', 'settings.local.json'), {
|
|
@@ -6592,13 +6722,18 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6592
6722
|
const io = new CapturedIo()
|
|
6593
6723
|
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env: { XDG_STATE_HOME: cwd } }
|
|
6594
6724
|
|
|
6595
|
-
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6725
|
+
expect(askCommand(deps, 'Ship it?', { json: true })).toBe(EXIT.usage)
|
|
6726
|
+
expect(JSON.parse(io.outLines.at(-1) ?? '{}')).toMatchObject({
|
|
6727
|
+
ok: false,
|
|
6728
|
+
registered: false,
|
|
6729
|
+
code: 'session_identity_missing',
|
|
6730
|
+
check_id: 'exact_session',
|
|
6731
|
+
exit_code: EXIT.usage,
|
|
6732
|
+
remedy: expect.stringMatching(/exact session|blocking/i),
|
|
6733
|
+
})
|
|
6599
6734
|
})
|
|
6600
6735
|
|
|
6601
|
-
it('
|
|
6736
|
+
it('does not guess a Cursor conversation when lifecycle context is absent', () => {
|
|
6602
6737
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-cursor-first-run-'))
|
|
6603
6738
|
const io = new CapturedIo()
|
|
6604
6739
|
const deps = {
|
|
@@ -6610,11 +6745,14 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6610
6745
|
expect(hooksInstallCommand(deps, { harness: 'cursor', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6611
6746
|
io.outLines = []
|
|
6612
6747
|
|
|
6613
|
-
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
6614
|
-
expect(io.
|
|
6748
|
+
expect(askCommand(deps, 'Ship it?', { json: true })).toBe(EXIT.usage)
|
|
6749
|
+
expect(JSON.parse(io.outLines.at(-1) ?? '{}')).toMatchObject({
|
|
6750
|
+
code: 'session_identity_missing',
|
|
6751
|
+
check_id: 'exact_session',
|
|
6752
|
+
})
|
|
6615
6753
|
})
|
|
6616
6754
|
|
|
6617
|
-
it('
|
|
6755
|
+
it('does not infer OpenCode from an installed adapter alone', () => {
|
|
6618
6756
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-opencode-first-run-'))
|
|
6619
6757
|
const io = new CapturedIo()
|
|
6620
6758
|
const deps = {
|
|
@@ -6629,13 +6767,14 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6629
6767
|
expect(hooksInstallCommand(deps, { harness: 'opencode', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6630
6768
|
io.outLines = []
|
|
6631
6769
|
|
|
6632
|
-
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6770
|
+
expect(askCommand(deps, 'Ship it?', { json: true })).toBe(EXIT.usage)
|
|
6771
|
+
expect(JSON.parse(io.outLines.at(-1) ?? '{}')).toMatchObject({
|
|
6772
|
+
code: 'session_identity_missing',
|
|
6773
|
+
check_id: 'exact_session',
|
|
6774
|
+
})
|
|
6636
6775
|
})
|
|
6637
6776
|
|
|
6638
|
-
it('
|
|
6777
|
+
it('does not turn missing harness identity into an installation guess', () => {
|
|
6639
6778
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-noinstall-'))
|
|
6640
6779
|
const io = new CapturedIo()
|
|
6641
6780
|
const deps = {
|
|
@@ -6650,8 +6789,11 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6650
6789
|
},
|
|
6651
6790
|
}
|
|
6652
6791
|
|
|
6653
|
-
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
6654
|
-
expect(io.
|
|
6792
|
+
expect(askCommand(deps, 'Ship it?', { json: true })).toBe(EXIT.usage)
|
|
6793
|
+
expect(JSON.parse(io.outLines.at(-1) ?? '{}')).toMatchObject({
|
|
6794
|
+
code: 'session_identity_missing',
|
|
6795
|
+
check_id: 'exact_session',
|
|
6796
|
+
})
|
|
6655
6797
|
})
|
|
6656
6798
|
})
|
|
6657
6799
|
|