@raidiant/notifai 9.3.0 → 10.1.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 +40 -0
- package/README.md +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/commands-agent-sessions.d.ts +6 -0
- package/dist/commands-agent-sessions.js +51 -0
- package/dist/commands-agent-sessions.js.map +1 -0
- package/dist/commands-auth.js +4 -4
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +11 -4
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +5 -0
- package/dist/commands-core.js +2 -0
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.js +121 -40
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.d.ts +15 -2
- package/dist/commands-harness-context.js +40 -1
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.d.ts +4 -3
- package/dist/commands-hooks.js +139 -16
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.js +10 -5
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +18 -1
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.js +2 -2
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-sounds.d.ts +12 -0
- package/dist/commands-sounds.js +35 -0
- package/dist/commands-sounds.js.map +1 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +2 -0
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.js +5 -2
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +3 -4
- package/dist/config.js +5 -2
- package/dist/config.js.map +1 -1
- package/dist/credentials.js +4 -1
- package/dist/credentials.js.map +1 -1
- package/dist/harnesses.d.ts +49 -7
- package/dist/harnesses.js +110 -24
- package/dist/harnesses.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +2 -2
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -11
- package/dist/install-hooks.js +18 -13
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.js +20 -23
- package/dist/interactive.js.map +1 -1
- package/dist/invocation-context.d.ts +2 -2
- package/dist/openclaw-plugin.d.ts +49 -0
- package/dist/openclaw-plugin.js +350 -0
- package/dist/openclaw-plugin.js.map +1 -0
- package/dist/orca-session-title.d.ts +10 -0
- package/dist/orca-session-title.js +98 -0
- package/dist/orca-session-title.js.map +1 -0
- package/dist/program.d.ts +3 -1
- package/dist/program.js +37 -13
- package/dist/program.js.map +1 -1
- package/dist/send.js +11 -14
- package/dist/send.js.map +1 -1
- package/dist/session-activation.js +1 -1
- package/dist/session-activation.js.map +1 -1
- package/dist/session-labels.d.ts +18 -2
- package/dist/session-labels.js +79 -8
- package/dist/session-labels.js.map +1 -1
- package/dist/sound-ref.d.ts +23 -0
- package/dist/sound-ref.js +28 -0
- package/dist/sound-ref.js.map +1 -0
- package/dist/ui/help.d.ts +4 -5
- package/dist/ui/help.js +5 -6
- package/dist/ui/help.js.map +1 -1
- package/package.json +3 -3
- package/src/client.test.ts +12 -0
- package/src/client.ts +8 -0
- package/src/commands-agent-sessions.ts +72 -0
- package/src/commands-auth.ts +4 -3
- package/src/commands-config.ts +10 -4
- package/src/commands-core.ts +6 -0
- package/src/commands-doctor.ts +143 -38
- package/src/commands-harness-context.ts +56 -2
- package/src/commands-hooks.ts +180 -20
- package/src/commands-init.ts +10 -4
- package/src/commands-send-support.ts +23 -1
- package/src/commands-send.ts +2 -2
- package/src/commands-sounds.test.ts +106 -0
- package/src/commands-sounds.ts +49 -0
- package/src/commands.test.ts +529 -17
- package/src/commands.ts +5 -0
- package/src/config-schema.test.ts +1 -0
- package/src/config-schema.ts +5 -2
- package/src/config.test.ts +42 -20
- package/src/config.ts +6 -5
- package/src/credentials.test.ts +20 -0
- package/src/credentials.ts +4 -1
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +50 -0
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +57 -0
- package/src/harness-integration.test.ts +294 -0
- package/src/harnesses.test.ts +115 -8
- package/src/harnesses.ts +133 -30
- package/src/hooks.test.ts +4 -3
- package/src/hooks.ts +4 -4
- package/src/install-hooks.test.ts +213 -4
- package/src/install-hooks.ts +38 -30
- package/src/interactive.test.ts +31 -0
- package/src/interactive.ts +20 -26
- package/src/invocation-context.ts +2 -2
- package/src/openclaw-plugin.ts +409 -0
- package/src/orca-session-title.test.ts +54 -0
- package/src/orca-session-title.ts +103 -0
- package/src/program.test.ts +27 -0
- package/src/program.ts +47 -12
- package/src/send.ts +10 -19
- package/src/session-activation.ts +1 -1
- package/src/session-labels.test.ts +58 -9
- package/src/session-labels.ts +95 -12
- package/src/skill.test.ts +14 -3
- package/src/sound-ref.test.ts +31 -0
- package/src/sound-ref.ts +34 -0
- package/src/ui/config-view.test.ts +1 -0
- package/src/ui/help.test.ts +16 -0
- package/src/ui/help.ts +6 -7
package/src/commands.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { CAPABILITIES_V1, PLATFORMS, REPLY_MAX_QUESTIONS } from '@raidiant/notifai-protocol'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
2
4
|
import {
|
|
3
5
|
existsSync,
|
|
4
6
|
mkdirSync,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
acknowledgeCommand,
|
|
33
35
|
askCommand,
|
|
34
36
|
accessStatusCommand,
|
|
37
|
+
authStatusCommand,
|
|
35
38
|
buildQuestions,
|
|
36
39
|
assessReadiness,
|
|
37
40
|
capabilitiesCommand,
|
|
@@ -56,6 +59,7 @@ import {
|
|
|
56
59
|
repliesCommand,
|
|
57
60
|
sendCommand,
|
|
58
61
|
statusCommand,
|
|
62
|
+
agentSessionRenameCommand,
|
|
59
63
|
type CommandDeps,
|
|
60
64
|
type CommandIo,
|
|
61
65
|
type CommandSpinner,
|
|
@@ -443,6 +447,16 @@ describe('command contracts', () => {
|
|
|
443
447
|
expect(submitted?.draft.project).toBeUndefined()
|
|
444
448
|
expect(projectEnabled(projectBinding(cwd, env))).toBe(false)
|
|
445
449
|
})
|
|
450
|
+
it('tells an unsigned machine to run init, not login', () => {
|
|
451
|
+
const io = new CapturedIo()
|
|
452
|
+
const deps = makeDeps(io, {} as ApiClient)
|
|
453
|
+
deps.store.load = () => null
|
|
454
|
+
|
|
455
|
+
expect(authStatusCommand(deps, {})).toBe(EXIT.auth)
|
|
456
|
+
expect(io.errLines.join('\n')).toMatch(/Not signed in\. Run `notifai init`/)
|
|
457
|
+
expect(io.errLines.join('\n')).not.toContain('notifai login')
|
|
458
|
+
})
|
|
459
|
+
|
|
446
460
|
it('shows an actionable no-plan access state', async () => {
|
|
447
461
|
const io = new CapturedIo()
|
|
448
462
|
const client = {
|
|
@@ -763,6 +777,28 @@ describe('command contracts', () => {
|
|
|
763
777
|
expect(io.errLines).toEqual([])
|
|
764
778
|
})
|
|
765
779
|
|
|
780
|
+
it('sends an Account custom sound name on the draft', async () => {
|
|
781
|
+
const io = new CapturedIo()
|
|
782
|
+
let submitted: SubmitNotificationRequestT | undefined
|
|
783
|
+
const client = {
|
|
784
|
+
submit: async (body: SubmitNotificationRequestT) => {
|
|
785
|
+
submitted = body
|
|
786
|
+
return receipt
|
|
787
|
+
},
|
|
788
|
+
} as unknown as ApiClient
|
|
789
|
+
|
|
790
|
+
expect(
|
|
791
|
+
await sendCommand(makeDeps(io, client), {
|
|
792
|
+
kind: 'done',
|
|
793
|
+
title: 'Build finished',
|
|
794
|
+
body: 'All checks passed.',
|
|
795
|
+
sound: 'Kitchen timer',
|
|
796
|
+
}),
|
|
797
|
+
).toBe(EXIT.ok)
|
|
798
|
+
expect(submitted?.draft.platform?.ios?.sound).toBe('Kitchen timer')
|
|
799
|
+
expect(submitted?.draft.platform?.android?.sound).toBe('Kitchen timer')
|
|
800
|
+
})
|
|
801
|
+
|
|
766
802
|
it('prints Android downgrade warnings before submitting', async () => {
|
|
767
803
|
const io = new CapturedIo()
|
|
768
804
|
let submitCalls = 0
|
|
@@ -856,7 +892,7 @@ describe('command contracts', () => {
|
|
|
856
892
|
expect(io.errLines.join('\n')).not.toContain('opaque-claude-session-42')
|
|
857
893
|
})
|
|
858
894
|
|
|
859
|
-
it('
|
|
895
|
+
it('uses trusted Orca task context for an active Claude Agent Session label', async () => {
|
|
860
896
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-orca-claude-title-'))
|
|
861
897
|
const io = new CapturedIo()
|
|
862
898
|
let submitted: SubmitNotificationRequestT | undefined
|
|
@@ -879,6 +915,7 @@ describe('command contracts', () => {
|
|
|
879
915
|
...makeDeps(io, client),
|
|
880
916
|
cwd,
|
|
881
917
|
env,
|
|
918
|
+
orcaSessionTitle: () => 'Agent Session context labels',
|
|
882
919
|
}
|
|
883
920
|
|
|
884
921
|
expect(
|
|
@@ -891,13 +928,12 @@ describe('command contracts', () => {
|
|
|
891
928
|
|
|
892
929
|
expect(submitted?.draft.source).toMatchObject({
|
|
893
930
|
session_id: 'orca-claude-session',
|
|
931
|
+
session_label: 'Agent Session context labels',
|
|
894
932
|
harness: 'claude-code',
|
|
895
933
|
})
|
|
896
|
-
expect(submitted?.draft.source?.session_label).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/)
|
|
897
|
-
expect(submitted?.draft.source?.session_label).not.toBe('Worker - semantic session implementation')
|
|
898
934
|
})
|
|
899
935
|
|
|
900
|
-
it('
|
|
936
|
+
it('uses trusted Orca task context for an active Codex Agent Session label', async () => {
|
|
901
937
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-orca-codex-title-'))
|
|
902
938
|
const io = new CapturedIo()
|
|
903
939
|
let submitted: SubmitNotificationRequestT | undefined
|
|
@@ -918,6 +954,7 @@ describe('command contracts', () => {
|
|
|
918
954
|
TERM_PROGRAM: 'Orca',
|
|
919
955
|
ORCA_WORKTREE_ID: worktreeId,
|
|
920
956
|
},
|
|
957
|
+
orcaSessionTitle: () => 'Release verification',
|
|
921
958
|
}
|
|
922
959
|
|
|
923
960
|
expect(
|
|
@@ -930,9 +967,77 @@ describe('command contracts', () => {
|
|
|
930
967
|
|
|
931
968
|
expect(submitted?.draft.source).toMatchObject({
|
|
932
969
|
session_id: 'orca-codex-thread',
|
|
970
|
+
session_label: 'Release verification',
|
|
933
971
|
harness: 'codex',
|
|
934
972
|
})
|
|
935
|
-
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
it('sends after isolating an invalid session-name record without losing valid names', async () => {
|
|
976
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-invalid-session-name-'))
|
|
977
|
+
const io = new CapturedIo()
|
|
978
|
+
const env = {
|
|
979
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
980
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
981
|
+
CODEX_THREAD_ID: 'current-session',
|
|
982
|
+
}
|
|
983
|
+
const file = path.join(stateDir(env), 'session-labels.json')
|
|
984
|
+
const validKey = createHash('sha256').update('unrelated-session').digest('hex')
|
|
985
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
986
|
+
writeFileSync(
|
|
987
|
+
file,
|
|
988
|
+
`${JSON.stringify(
|
|
989
|
+
{
|
|
990
|
+
version: 1,
|
|
991
|
+
sessions: {
|
|
992
|
+
[validKey]: {
|
|
993
|
+
label: 'Unrelated work',
|
|
994
|
+
source: 'explicit',
|
|
995
|
+
first_seen_at: 1_777_777_777_000,
|
|
996
|
+
harness: 'codex',
|
|
997
|
+
},
|
|
998
|
+
obsolete: {
|
|
999
|
+
label: 'Old shape',
|
|
1000
|
+
source: 'legacy',
|
|
1001
|
+
first_seen_at: 'unknown',
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
null,
|
|
1006
|
+
2,
|
|
1007
|
+
)}\n`,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
let submitted: SubmitNotificationRequestT | undefined
|
|
1011
|
+
const client = {
|
|
1012
|
+
submit: async (body: SubmitNotificationRequestT) => {
|
|
1013
|
+
submitted = body
|
|
1014
|
+
return receipt
|
|
1015
|
+
},
|
|
1016
|
+
} as unknown as ApiClient
|
|
1017
|
+
|
|
1018
|
+
expect(
|
|
1019
|
+
await sendCommand(
|
|
1020
|
+
{ ...makeDeps(io, client), cwd, env },
|
|
1021
|
+
{ title: 'Store recovered', body: 'The valid Agent Session name survived.', kind: 'done' },
|
|
1022
|
+
),
|
|
1023
|
+
).toBe(EXIT.ok)
|
|
1024
|
+
expect(submitted?.draft.source).toMatchObject({
|
|
1025
|
+
session_id: 'current-session',
|
|
1026
|
+
harness: 'codex',
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
const recovered = JSON.parse(readFileSync(file, 'utf8')) as {
|
|
1030
|
+
sessions: Record<string, { label: string }>
|
|
1031
|
+
}
|
|
1032
|
+
expect(recovered.sessions[validKey]?.label).toBe('Unrelated work')
|
|
1033
|
+
expect(recovered.sessions['obsolete']).toBeUndefined()
|
|
1034
|
+
const backups = readdirSync(path.dirname(file)).filter((name) =>
|
|
1035
|
+
/^session-labels\.invalid-[a-f0-9]{64}\.json$/u.test(name),
|
|
1036
|
+
)
|
|
1037
|
+
expect(backups).toHaveLength(1)
|
|
1038
|
+
expect(readFileSync(path.join(path.dirname(file), backups[0]!), 'utf8')).toContain(
|
|
1039
|
+
'"obsolete"',
|
|
1040
|
+
)
|
|
936
1041
|
})
|
|
937
1042
|
|
|
938
1043
|
it('falls back safely when Orca returns a private path as its title', async () => {
|
|
@@ -1041,6 +1146,113 @@ describe('command contracts', () => {
|
|
|
1041
1146
|
expect(existsSync(path.join(cwd, 'state', 'notifai', 'session-labels.json'))).toBe(false)
|
|
1042
1147
|
})
|
|
1043
1148
|
|
|
1149
|
+
it('sends Hermes Source Context from HERMES_SESSION_ID and the invocation cwd', async () => {
|
|
1150
|
+
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-send-'))
|
|
1151
|
+
const repo = path.join(root, 'notifai')
|
|
1152
|
+
const worktree = path.join(root, 'hermes-topic')
|
|
1153
|
+
mkdirSync(repo)
|
|
1154
|
+
const git = (...args: string[]) => execFileSync('git', ['-C', repo, ...args], { stdio: 'ignore' })
|
|
1155
|
+
git('init')
|
|
1156
|
+
git('config', 'user.email', 'test@example.invalid')
|
|
1157
|
+
git('config', 'user.name', 'Test')
|
|
1158
|
+
git('config', 'commit.gpgsign', 'false')
|
|
1159
|
+
writeFileSync(path.join(repo, 'file'), 'x')
|
|
1160
|
+
git('add', 'file')
|
|
1161
|
+
git('commit', '-m', 'fixture')
|
|
1162
|
+
git('worktree', 'add', worktree, '-b', 'feature/hermes-send')
|
|
1163
|
+
|
|
1164
|
+
const io = new CapturedIo()
|
|
1165
|
+
let submitted: SubmitNotificationRequestT | undefined
|
|
1166
|
+
const client = {
|
|
1167
|
+
submit: async (body: SubmitNotificationRequestT) => {
|
|
1168
|
+
submitted = body
|
|
1169
|
+
return receipt
|
|
1170
|
+
},
|
|
1171
|
+
} as unknown as ApiClient
|
|
1172
|
+
const sessionId = '20260828_111302_29a404'
|
|
1173
|
+
const deps = {
|
|
1174
|
+
...makeDeps(io, client),
|
|
1175
|
+
cwd: worktree,
|
|
1176
|
+
env: {
|
|
1177
|
+
...isolatedEnv(worktree),
|
|
1178
|
+
HERMES_SESSION_ID: sessionId,
|
|
1179
|
+
},
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
expect(
|
|
1183
|
+
await sendCommand(deps, {
|
|
1184
|
+
title: 'Hermes baseline landed',
|
|
1185
|
+
body: 'Classic CLI send carries exact session identity.',
|
|
1186
|
+
kind: 'done',
|
|
1187
|
+
}),
|
|
1188
|
+
).toBe(EXIT.ok)
|
|
1189
|
+
|
|
1190
|
+
expect(submitted?.draft.source).toMatchObject({
|
|
1191
|
+
session_id: sessionId,
|
|
1192
|
+
harness: 'hermes',
|
|
1193
|
+
branch: 'feature/hermes-send',
|
|
1194
|
+
worktree: 'hermes-topic',
|
|
1195
|
+
})
|
|
1196
|
+
expect(io.outLines.join('\n')).not.toContain(sessionId)
|
|
1197
|
+
|
|
1198
|
+
submitted = undefined
|
|
1199
|
+
expect(
|
|
1200
|
+
await sendCommand(
|
|
1201
|
+
{
|
|
1202
|
+
...deps,
|
|
1203
|
+
env: {
|
|
1204
|
+
...deps.env,
|
|
1205
|
+
HERMES_SESSION_KEY: 'gateway-route-key-must-not-be-session-id',
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
title: 'Hermes deferred surface sent',
|
|
1210
|
+
body: 'The send stays universal while unproven Source Context is omitted.',
|
|
1211
|
+
kind: 'done',
|
|
1212
|
+
},
|
|
1213
|
+
),
|
|
1214
|
+
).toBe(EXIT.ok)
|
|
1215
|
+
expect(submitted?.draft.source).toMatchObject({
|
|
1216
|
+
branch: 'feature/hermes-send',
|
|
1217
|
+
worktree: 'hermes-topic',
|
|
1218
|
+
})
|
|
1219
|
+
expect(submitted?.draft.source).not.toHaveProperty('session_id')
|
|
1220
|
+
expect(submitted?.draft.source).not.toHaveProperty('harness')
|
|
1221
|
+
expect(JSON.stringify(submitted?.draft.source)).not.toContain('gateway-route-key')
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
it('does not attribute Hermes Source Context when another harness marker is nested', async () => {
|
|
1225
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-nested-send-'))
|
|
1226
|
+
const io = new CapturedIo()
|
|
1227
|
+
let submitted: SubmitNotificationRequestT | undefined
|
|
1228
|
+
const client = {
|
|
1229
|
+
submit: async (body: SubmitNotificationRequestT) => {
|
|
1230
|
+
submitted = body
|
|
1231
|
+
return receipt
|
|
1232
|
+
},
|
|
1233
|
+
} as unknown as ApiClient
|
|
1234
|
+
const deps = {
|
|
1235
|
+
...makeDeps(io, client),
|
|
1236
|
+
cwd,
|
|
1237
|
+
env: {
|
|
1238
|
+
...isolatedEnv(cwd),
|
|
1239
|
+
CLAUDECODE: '1',
|
|
1240
|
+
CLAUDE_CODE_SESSION_ID: 'claude-orchestrator',
|
|
1241
|
+
HERMES_SESSION_ID: '20260828_111302_29a404',
|
|
1242
|
+
},
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
expect(
|
|
1246
|
+
await sendCommand(deps, {
|
|
1247
|
+
title: 'Nested send stays honest',
|
|
1248
|
+
body: 'Ambiguous ownership omits harness identity.',
|
|
1249
|
+
kind: 'update',
|
|
1250
|
+
}),
|
|
1251
|
+
).toBe(EXIT.ok)
|
|
1252
|
+
expect(submitted?.draft.source?.harness).toBeUndefined()
|
|
1253
|
+
expect(submitted?.draft.source?.session_id).toBeUndefined()
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1044
1256
|
it('uploads repeatable images in order and sends only canonical media references', async () => {
|
|
1045
1257
|
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-media-order-'))
|
|
1046
1258
|
const first = path.join(root, 'first.png')
|
|
@@ -1198,6 +1410,8 @@ describe('command contracts', () => {
|
|
|
1198
1410
|
// Kind now selects the sound, so the help must say so — and must not carry
|
|
1199
1411
|
// the retired separation it replaced.
|
|
1200
1412
|
expect(source).toContain('Kind is required, and it selects the sound')
|
|
1413
|
+
expect(source).toContain('Device default')
|
|
1414
|
+
expect(source).toContain('custom name or id')
|
|
1201
1415
|
expect(source).not.toContain('it never chooses banner sound or interruption level')
|
|
1202
1416
|
expect(source).not.toContain('Kind profiles apply automatically')
|
|
1203
1417
|
})
|
|
@@ -2393,7 +2607,7 @@ describe('credential origin pinning', () => {
|
|
|
2393
2607
|
expect(io.errLines).toEqual([
|
|
2394
2608
|
'This account has no active plan or temporary Alpha access.',
|
|
2395
2609
|
'next: Ask the account owner for Alpha access.',
|
|
2396
|
-
'After access is granted, run `notifai
|
|
2610
|
+
'After access is granted, run `notifai init` again.',
|
|
2397
2611
|
])
|
|
2398
2612
|
})
|
|
2399
2613
|
|
|
@@ -3074,6 +3288,71 @@ describe('harness activation guidance', () => {
|
|
|
3074
3288
|
expect(inspectHookAdapter(deps.hookAdapterHome).problems).toEqual([])
|
|
3075
3289
|
expect(inspectHookAdapter(deps.hookAdapterHome).target?.scriptPath).toBe(scriptPath)
|
|
3076
3290
|
})
|
|
3291
|
+
|
|
3292
|
+
it('installs an owned OpenClaw plugin and reports unsupported continuation', async () => {
|
|
3293
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-openclaw-activation-'))
|
|
3294
|
+
const io = new CapturedIo()
|
|
3295
|
+
const client = {
|
|
3296
|
+
health: async () => true,
|
|
3297
|
+
capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
|
|
3298
|
+
listDevices: async () => ({ devices: [] }),
|
|
3299
|
+
} as unknown as ApiClient
|
|
3300
|
+
const home = path.join(cwd, 'home')
|
|
3301
|
+
const deps = {
|
|
3302
|
+
...makeDeps(io, client),
|
|
3303
|
+
cwd,
|
|
3304
|
+
env: {
|
|
3305
|
+
HOME: home,
|
|
3306
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
3307
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
3308
|
+
OPENCLAW_STATE_DIR: path.join(cwd, 'openclaw-home'),
|
|
3309
|
+
},
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
expect(hooksInstallCommand(deps, { harness: 'openclaw', execPath, scriptPath })).toBe(EXIT.ok)
|
|
3313
|
+
expect(io.outLines.join('\n')).toContain('Installed openclaw hooks in')
|
|
3314
|
+
expect(io.outLines.join('\n')).toContain(
|
|
3315
|
+
'Restart the OpenClaw Gateway, start one fresh Agent Session, send one prompt, then run `notifai doctor`.',
|
|
3316
|
+
)
|
|
3317
|
+
const pluginFile = path.join(cwd, '.openclaw', 'extensions', 'notifai', 'index.js')
|
|
3318
|
+
const plugin = readFileSync(pluginFile, 'utf8')
|
|
3319
|
+
expect(plugin).toContain('api.on("before_prompt_build"')
|
|
3320
|
+
expect(plugin).toContain('api.on("resolve_exec_env"')
|
|
3321
|
+
expect(plugin).not.toContain('command:stop')
|
|
3322
|
+
const config = JSON.parse(
|
|
3323
|
+
readFileSync(path.join(cwd, 'openclaw-home', 'openclaw.json'), 'utf8'),
|
|
3324
|
+
) as {
|
|
3325
|
+
plugins: { entries: { notifai: { enabled: boolean; hooks: { allowConversationAccess: boolean } } } }
|
|
3326
|
+
}
|
|
3327
|
+
expect(config.plugins.entries.notifai).toEqual({
|
|
3328
|
+
enabled: true,
|
|
3329
|
+
hooks: { allowConversationAccess: true },
|
|
3330
|
+
})
|
|
3331
|
+
|
|
3332
|
+
io.outLines = []
|
|
3333
|
+
expect(await doctorCommand(deps, {})).toBe(EXIT.failed)
|
|
3334
|
+
expect(io.outLines.join('\n')).toContain('no proven answer continuation')
|
|
3335
|
+
})
|
|
3336
|
+
|
|
3337
|
+
it('refuses to overwrite a foreign OpenClaw plugin', () => {
|
|
3338
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-openclaw-foreign-'))
|
|
3339
|
+
const plugin = path.join(cwd, '.openclaw', 'extensions', 'notifai', 'index.js')
|
|
3340
|
+
mkdirSync(path.dirname(plugin), { recursive: true })
|
|
3341
|
+
writeFileSync(plugin, 'export default { id: "foreign" }\n')
|
|
3342
|
+
const io = new CapturedIo()
|
|
3343
|
+
const deps = {
|
|
3344
|
+
...makeDeps(io, {} as ApiClient),
|
|
3345
|
+
cwd,
|
|
3346
|
+
env: {
|
|
3347
|
+
HOME: path.join(cwd, 'home'),
|
|
3348
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
3349
|
+
OPENCLAW_STATE_DIR: path.join(cwd, 'openclaw-home'),
|
|
3350
|
+
},
|
|
3351
|
+
}
|
|
3352
|
+
expect(hooksInstallCommand(deps, { harness: 'openclaw', execPath, scriptPath })).toBe(EXIT.failed)
|
|
3353
|
+
expect(io.errLines.join('\n')).toMatch(/was not written by Notifai/)
|
|
3354
|
+
expect(readFileSync(plugin, 'utf8')).toBe('export default { id: "foreign" }\n')
|
|
3355
|
+
})
|
|
3077
3356
|
})
|
|
3078
3357
|
|
|
3079
3358
|
describe('stable hook installation', () => {
|
|
@@ -3539,19 +3818,20 @@ describe('config surfaces', () => {
|
|
|
3539
3818
|
expect(io.errLines[0]).toBe('Unknown setting "ask_notification".')
|
|
3540
3819
|
})
|
|
3541
3820
|
|
|
3542
|
-
it('refuses
|
|
3543
|
-
// `config set sound whatever` used to be written straight to disk: the
|
|
3544
|
-
// typo only surfaced when a notification failed to carry the sound.
|
|
3821
|
+
it('refuses a sound that cannot be a shipped name, custom id, or custom name', async () => {
|
|
3545
3822
|
const io = new CapturedIo()
|
|
3546
|
-
expect(await configSetCommand(configDeps(io), 'sound', '
|
|
3823
|
+
expect(await configSetCommand(configDeps(io), 'sound', 'n'.repeat(200), { yes: true })).toBe(
|
|
3547
3824
|
EXIT.usage,
|
|
3548
3825
|
)
|
|
3549
|
-
expect(io.errLines[0]).toContain('
|
|
3826
|
+
expect(io.errLines[0]).toContain('custom sound')
|
|
3550
3827
|
})
|
|
3551
3828
|
|
|
3552
|
-
it('
|
|
3829
|
+
it('accepts a shipped sound and an Account custom name', async () => {
|
|
3553
3830
|
const io = new CapturedIo()
|
|
3554
3831
|
expect(await configSetCommand(configDeps(io), 'sound', 'none', { yes: true })).toBe(EXIT.ok)
|
|
3832
|
+
expect(await configSetCommand(configDeps(io), 'sound', 'Kitchen timer', { yes: true })).toBe(
|
|
3833
|
+
EXIT.ok,
|
|
3834
|
+
)
|
|
3555
3835
|
})
|
|
3556
3836
|
|
|
3557
3837
|
it('removes one machine setting, preserves its siblings, and restores default provenance', async () => {
|
|
@@ -4320,8 +4600,9 @@ describe('init', () => {
|
|
|
4320
4600
|
expect(await initCommand(deps, {})).toBe(EXIT.failed)
|
|
4321
4601
|
const out = io.outLines.join('\n')
|
|
4322
4602
|
expect(out).toContain('Next: This machine')
|
|
4323
|
-
expect(out).toContain('notifai
|
|
4324
|
-
expect(out).toContain('
|
|
4603
|
+
expect(out).toContain('notifai init')
|
|
4604
|
+
expect(out).not.toContain('notifai login')
|
|
4605
|
+
expect(out).not.toContain('Then re-run')
|
|
4325
4606
|
// The device gap is real and downstream; it must stay hidden until the
|
|
4326
4607
|
// sign-in that would let anyone actually check it has happened.
|
|
4327
4608
|
expect(out).not.toContain('companion app')
|
|
@@ -4867,7 +5148,8 @@ describe('init', () => {
|
|
|
4867
5148
|
}
|
|
4868
5149
|
|
|
4869
5150
|
expect(await initCommand(deps, { setupScope: 'project' })).toBe(EXIT.failed)
|
|
4870
|
-
expect(io.outLines.join('\n')).toContain('notifai
|
|
5151
|
+
expect(io.outLines.join('\n')).toContain('notifai init')
|
|
5152
|
+
expect(io.outLines.join('\n')).not.toContain('notifai login')
|
|
4871
5153
|
})
|
|
4872
5154
|
|
|
4873
5155
|
it('announces sign-in for a present human without re-confirming', async () => {
|
|
@@ -4900,7 +5182,8 @@ describe('init', () => {
|
|
|
4900
5182
|
const out = io.outLines.join('\n')
|
|
4901
5183
|
expect(out).toContain('Opening your browser to approve this machine — Ctrl-C to stop.')
|
|
4902
5184
|
expect(out).toContain('Next: This machine')
|
|
4903
|
-
expect(out).toContain('
|
|
5185
|
+
expect(out).toContain('notifai init')
|
|
5186
|
+
expect(out).not.toContain('notifai login')
|
|
4904
5187
|
})
|
|
4905
5188
|
|
|
4906
5189
|
it('never prompts or opens a browser when an agent runs it unattended', async () => {
|
|
@@ -5463,8 +5746,86 @@ describe('init', () => {
|
|
|
5463
5746
|
const out = io.outLines.join('\n')
|
|
5464
5747
|
expect(out).toContain('Next: Account')
|
|
5465
5748
|
expect(out).toContain('pair it again')
|
|
5466
|
-
expect(out).toContain('notifai
|
|
5749
|
+
expect(out).toContain('notifai init')
|
|
5750
|
+
expect(out).not.toContain('notifai login')
|
|
5751
|
+
expect(out.match(/^Next:/gm)).toHaveLength(1)
|
|
5752
|
+
})
|
|
5753
|
+
|
|
5754
|
+
it('names the support next action when a paired account has no active plan', async () => {
|
|
5755
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-no-plan-'))
|
|
5756
|
+
const io = new CapturedIo()
|
|
5757
|
+
const nextAction = 'Open https://app.notifai.sh/support to request Alpha access, then retry.'
|
|
5758
|
+
const client = {
|
|
5759
|
+
health: async () => true,
|
|
5760
|
+
capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
|
|
5761
|
+
listDevices: async () => {
|
|
5762
|
+
throw new ApiCallError(403, 'no_active_plan', 'No active plan.', nextAction)
|
|
5763
|
+
},
|
|
5764
|
+
accessStatus: async () => ({
|
|
5765
|
+
status: 'no_active_plan',
|
|
5766
|
+
reason: 'no_active_grant',
|
|
5767
|
+
expires_at: null,
|
|
5768
|
+
email: 'rafael@example.test',
|
|
5769
|
+
}),
|
|
5770
|
+
} as unknown as ApiClient
|
|
5771
|
+
const deps = {
|
|
5772
|
+
...makeDeps(io, client),
|
|
5773
|
+
cwd,
|
|
5774
|
+
env: isolatedEnv(cwd),
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
const readiness = await assessReadiness(deps)
|
|
5778
|
+
const auth = readiness.states.find((state) => state.id === 'auth')
|
|
5779
|
+
expect(auth).toMatchObject({
|
|
5780
|
+
status: 'gap',
|
|
5781
|
+
detail: 'no active plan or temporary Alpha access (rafael@example.test)',
|
|
5782
|
+
remedy: { by: 'user-elsewhere', summary: nextAction },
|
|
5783
|
+
})
|
|
5784
|
+
expect(auth?.remedy && 'command' in auth.remedy ? auth.remedy.command : undefined).toBeUndefined()
|
|
5785
|
+
|
|
5786
|
+
expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
|
|
5787
|
+
const out = io.outLines.join('\n')
|
|
5788
|
+
expect(out).toContain('Next: Account')
|
|
5789
|
+
expect(out).toContain(nextAction)
|
|
5790
|
+
expect(out).not.toContain('pair it again')
|
|
5791
|
+
expect(out).not.toContain('notifai login')
|
|
5467
5792
|
expect(out.match(/^Next:/gm)).toHaveLength(1)
|
|
5793
|
+
|
|
5794
|
+
const doctorIo = new CapturedIo()
|
|
5795
|
+
expect(await doctorCommand({ ...deps, io: doctorIo }, { json: true })).toBe(EXIT.failed)
|
|
5796
|
+
const payload = JSON.parse(doctorIo.outLines[0] ?? '{}') as {
|
|
5797
|
+
states: Array<{ id: string; remedy?: { summary?: string; command?: string } }>
|
|
5798
|
+
}
|
|
5799
|
+
const doctorAuth = payload.states.find((state) => state.id === 'auth')
|
|
5800
|
+
expect(doctorAuth?.remedy?.summary).toBe(nextAction)
|
|
5801
|
+
expect(doctorAuth?.remedy?.command).toBeUndefined()
|
|
5802
|
+
})
|
|
5803
|
+
|
|
5804
|
+
it('does not treat a listDevices no-plan 403 as a revoked machine when accessStatus fails', async () => {
|
|
5805
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-no-plan-devices-only-'))
|
|
5806
|
+
const io = new CapturedIo()
|
|
5807
|
+
const nextAction = 'Open https://app.notifai.sh/support to request Alpha access, then retry.'
|
|
5808
|
+
const client = {
|
|
5809
|
+
health: async () => true,
|
|
5810
|
+
listDevices: async () => {
|
|
5811
|
+
throw new ApiCallError(403, 'no_active_plan', 'No active plan.', nextAction)
|
|
5812
|
+
},
|
|
5813
|
+
accessStatus: async () => {
|
|
5814
|
+
throw new ApiCallError(500, 'internal_error', 'access status unavailable')
|
|
5815
|
+
},
|
|
5816
|
+
} as unknown as ApiClient
|
|
5817
|
+
const deps = {
|
|
5818
|
+
...makeDeps(io, client),
|
|
5819
|
+
cwd,
|
|
5820
|
+
env: isolatedEnv(cwd),
|
|
5821
|
+
}
|
|
5822
|
+
|
|
5823
|
+
const readiness = await assessReadiness(deps)
|
|
5824
|
+
const auth = readiness.states.find((state) => state.id === 'auth')
|
|
5825
|
+
expect(auth?.status).toBe('gap')
|
|
5826
|
+
expect(auth?.detail).toContain('no active plan')
|
|
5827
|
+
expect(auth?.remedy).toMatchObject({ by: 'user-elsewhere', summary: nextAction })
|
|
5828
|
+
expect(JSON.stringify(auth)).not.toContain('pair it again')
|
|
5468
5829
|
})
|
|
5469
5830
|
|
|
5470
5831
|
it('reuses proof across worktrees of one Project on the same Approved Machine', async () => {
|
|
@@ -6519,6 +6880,114 @@ describe('asking before the hooks have ever run', () => {
|
|
|
6519
6880
|
expect(readSessionState('opencode-current', env).pending).toBeUndefined()
|
|
6520
6881
|
})
|
|
6521
6882
|
|
|
6883
|
+
it('rejects OpenClaw before registration even with an exact matching pointer', () => {
|
|
6884
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-active-openclaw-unsupported-'))
|
|
6885
|
+
const io = new CapturedIo()
|
|
6886
|
+
const env = {
|
|
6887
|
+
HOME: path.join(cwd, 'home'),
|
|
6888
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
6889
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
6890
|
+
OPENCLAW_STATE_DIR: path.join(cwd, 'openclaw-home'),
|
|
6891
|
+
NOTIFAI_ACTIVE_HARNESS: 'openclaw',
|
|
6892
|
+
NOTIFAI_ACTIVE_SESSION_ID: 'agent:main:main',
|
|
6893
|
+
}
|
|
6894
|
+
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6895
|
+
expect(hooksInstallCommand(deps, { harness: 'openclaw', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6896
|
+
writeSessionState('agent:main:main', env, { harness: 'openclaw', last_prompt_at: 42, last_stop_at: 41 })
|
|
6897
|
+
writeProjectSession(cwd, env, 'agent:main:main', 42, 'openclaw')
|
|
6898
|
+
io.outLines = []
|
|
6899
|
+
|
|
6900
|
+
expect(askCommand(deps, 'Ship it?', {})).toBe(EXIT.usage)
|
|
6901
|
+
expect(io.errLines.join('\n')).toMatch(/no proven answer continuation/i)
|
|
6902
|
+
expect(readSessionState('agent:main:main', env).pending).toBeUndefined()
|
|
6903
|
+
})
|
|
6904
|
+
|
|
6905
|
+
it('refuses Hermes ask as unsupported rather than as missing hooks', () => {
|
|
6906
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-ask-unsupported-'))
|
|
6907
|
+
const io = new CapturedIo()
|
|
6908
|
+
const env = {
|
|
6909
|
+
...isolatedEnv(cwd),
|
|
6910
|
+
HERMES_SESSION_ID: '20260828_111302_29a404',
|
|
6911
|
+
}
|
|
6912
|
+
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6913
|
+
|
|
6914
|
+
expect(askCommand(deps, 'Ship it?', { json: true })).toBe(EXIT.usage)
|
|
6915
|
+
const payload = JSON.parse(io.outLines.join('\n')) as { code: string; message: string; remedy: string }
|
|
6916
|
+
expect(payload.code).toBe('question_routing_unavailable')
|
|
6917
|
+
expect(payload.message).toMatch(/no proven continuation owner/i)
|
|
6918
|
+
expect(payload.message).not.toMatch(/hooks are not installed/i)
|
|
6919
|
+
expect(payload.remedy).toMatch(/send --reply/)
|
|
6920
|
+
expect(hooksInstallCommand(deps, { harness: 'hermes', execPath, scriptPath })).toBe(EXIT.usage)
|
|
6921
|
+
expect(io.errLines.join('\n')).toMatch(/Unknown harness "hermes"/)
|
|
6922
|
+
})
|
|
6923
|
+
|
|
6924
|
+
it('fails closed when Hermes nested markers leave session ownership ambiguous', () => {
|
|
6925
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-nested-ask-'))
|
|
6926
|
+
const io = new CapturedIo()
|
|
6927
|
+
const env = {
|
|
6928
|
+
...isolatedEnv(cwd),
|
|
6929
|
+
CLAUDECODE: '1',
|
|
6930
|
+
CLAUDE_CODE_SESSION_ID: 'claude-orchestrator',
|
|
6931
|
+
HERMES_SESSION_ID: '20260828_111302_29a404',
|
|
6932
|
+
}
|
|
6933
|
+
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6934
|
+
expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(EXIT.ok)
|
|
6935
|
+
writeSessionState('claude-orchestrator', env, {
|
|
6936
|
+
harness: 'claude-code',
|
|
6937
|
+
last_prompt_at: 42,
|
|
6938
|
+
last_stop_at: 41,
|
|
6939
|
+
})
|
|
6940
|
+
writeProjectSession(cwd, env, 'claude-orchestrator', 42, 'claude-code')
|
|
6941
|
+
io.outLines = []
|
|
6942
|
+
io.errLines = []
|
|
6943
|
+
|
|
6944
|
+
expect(askCommand(deps, 'Ship it?', { json: true })).toBe(EXIT.usage)
|
|
6945
|
+
const payload = JSON.parse(io.outLines.join('\n')) as { code: string; message: string }
|
|
6946
|
+
expect(payload.code).toBe('session_identity_ambiguous')
|
|
6947
|
+
expect(payload.message).toMatch(/Several harness sessions could own this shell/i)
|
|
6948
|
+
expect(payload.message).toMatch(/Hermes/)
|
|
6949
|
+
expect(readSessionState('claude-orchestrator', env).pending).toBeUndefined()
|
|
6950
|
+
})
|
|
6951
|
+
|
|
6952
|
+
it('does not attribute doctor readiness when Hermes nested markers are ambiguous', async () => {
|
|
6953
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-nested-doctor-'))
|
|
6954
|
+
const io = new CapturedIo()
|
|
6955
|
+
const env = {
|
|
6956
|
+
...isolatedEnv(cwd),
|
|
6957
|
+
CLAUDECODE: '1',
|
|
6958
|
+
CLAUDE_CODE_SESSION_ID: 'claude-orchestrator',
|
|
6959
|
+
HERMES_SESSION_ID: '20260828_111302_29a404',
|
|
6960
|
+
}
|
|
6961
|
+
const readiness = await assessReadiness({
|
|
6962
|
+
...makeDeps(io, {} as ApiClient),
|
|
6963
|
+
cwd,
|
|
6964
|
+
env,
|
|
6965
|
+
now: () => 42,
|
|
6966
|
+
})
|
|
6967
|
+
const hooks = readiness.states.find((state) => state.id === 'hooks')
|
|
6968
|
+
expect(hooks?.status).toBe('optional-gap')
|
|
6969
|
+
expect(hooks?.detail).toMatch(/Several harness sessions could own this shell/i)
|
|
6970
|
+
expect(hooks?.detail).toMatch(/Claude Code, Hermes/)
|
|
6971
|
+
expect(hooks?.remedy).toBeUndefined()
|
|
6972
|
+
expect(JSON.stringify(hooks)).not.toContain('hooks install --harness claude-code')
|
|
6973
|
+
})
|
|
6974
|
+
|
|
6975
|
+
it('does not tell an active Hermes session to install managed hooks', async () => {
|
|
6976
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-doctor-'))
|
|
6977
|
+
const io = new CapturedIo()
|
|
6978
|
+
const env = {
|
|
6979
|
+
...isolatedEnv(cwd),
|
|
6980
|
+
HERMES_SESSION_ID: '20260828_111302_29a404',
|
|
6981
|
+
}
|
|
6982
|
+
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env, now: () => 42 }
|
|
6983
|
+
const readiness = await assessReadiness(deps)
|
|
6984
|
+
const hooks = readiness.states.find((state) => state.id === 'hooks')
|
|
6985
|
+
expect(hooks?.status).toBe('optional-gap')
|
|
6986
|
+
expect(hooks?.remedy).toBeUndefined()
|
|
6987
|
+
expect(JSON.stringify(hooks)).not.toContain('hooks install --harness hermes')
|
|
6988
|
+
expect(hooks?.detail).toMatch(/no proven continuation owner/i)
|
|
6989
|
+
})
|
|
6990
|
+
|
|
6522
6991
|
it('refuses Cursor when its active-agent marker has no exact conversation id', () => {
|
|
6523
6992
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-active-cursor-'))
|
|
6524
6993
|
const io = new CapturedIo()
|
|
@@ -7314,3 +7783,46 @@ describe('a second device that disagrees', () => {
|
|
|
7314
7783
|
expect(contradictingAnswer([])).toBeNull()
|
|
7315
7784
|
})
|
|
7316
7785
|
})
|
|
7786
|
+
|
|
7787
|
+
describe('Agent Session rename command', () => {
|
|
7788
|
+
it('renames only the exact active Agent Session and updates its local frozen label', async () => {
|
|
7789
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-session-rename-'))
|
|
7790
|
+
const io = new CapturedIo()
|
|
7791
|
+
const seen: { session_id: string; label: string }[] = []
|
|
7792
|
+
const client = {
|
|
7793
|
+
putAgentSessionLabel: async (body: { session_id: string; label: string }) => {
|
|
7794
|
+
seen.push(body)
|
|
7795
|
+
return {
|
|
7796
|
+
...body,
|
|
7797
|
+
renamed_by: 'agent' as const,
|
|
7798
|
+
updated_at: '2026-08-28T10:00:00.000Z',
|
|
7799
|
+
}
|
|
7800
|
+
},
|
|
7801
|
+
} as unknown as ApiClient
|
|
7802
|
+
const deps = {
|
|
7803
|
+
...makeDeps(io, client),
|
|
7804
|
+
cwd,
|
|
7805
|
+
env: {
|
|
7806
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
7807
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
7808
|
+
CODEX_THREAD_ID: '01a04789-9589-76b2-82c8-bfe63062c867',
|
|
7809
|
+
},
|
|
7810
|
+
}
|
|
7811
|
+
|
|
7812
|
+
expect(await agentSessionRenameCommand(deps, ' Hermes Runtime Support ')).toBe(EXIT.ok)
|
|
7813
|
+
expect(seen).toEqual([{
|
|
7814
|
+
session_id: '01a04789-9589-76b2-82c8-bfe63062c867',
|
|
7815
|
+
label: 'Hermes Runtime Support',
|
|
7816
|
+
}])
|
|
7817
|
+
expect(io.outLines.join('\n')).toContain('Hermes Runtime Support')
|
|
7818
|
+
})
|
|
7819
|
+
|
|
7820
|
+
it('fails closed when the harness does not expose an exact current Agent Session', async () => {
|
|
7821
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-session-rename-none-'))
|
|
7822
|
+
const io = new CapturedIo()
|
|
7823
|
+
const deps = { ...makeDeps(io, {} as ApiClient), cwd, env: isolatedEnv(cwd) }
|
|
7824
|
+
|
|
7825
|
+
expect(await agentSessionRenameCommand(deps, 'A new job')).toBe(EXIT.usage)
|
|
7826
|
+
expect(io.errLines.join('\n')).toMatch(/exact active Agent Session/i)
|
|
7827
|
+
})
|
|
7828
|
+
})
|