@raidiant/notifai 10.1.0 → 10.1.2
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 +37 -0
- package/README.md +3 -0
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +29 -11
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +9 -2
- package/dist/commands-core.js +11 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +93 -49
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.js +47 -10
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -93
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +19 -0
- package/dist/node-floor.js +39 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/program.d.ts +2 -1
- package/dist/program.js +22 -5
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -38
- package/src/cli-bin.ts +0 -67
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -345
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -297
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -205
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -298
- package/src/commands-devices.test.ts +0 -15
- package/src/commands-devices.ts +0 -173
- package/src/commands-doctor.ts +0 -1440
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2268
- package/src/commands-init.ts +0 -878
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -769
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands.test.ts +0 -7828
- package/src/commands.ts +0 -75
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -410
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5549
- package/src/hooks.ts +0 -3759
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main.ts +0 -36
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -282
- package/src/program.ts +0 -818
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -179
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/skill.test.ts +0 -415
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/src/cli-release.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { packageVersion } from './release.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The newest published CLI, read from the registry that publishes it.
|
|
5
|
-
*
|
|
6
|
-
* Which version is newest is a registry fact. Doctor consults it as a
|
|
7
|
-
* best-effort, cached, short-timeout hint: never fatal, and never on a
|
|
8
|
-
* non-interactive or CI invocation — those callers already get structured
|
|
9
|
-
* support state from the server when they are signed in.
|
|
10
|
-
*/
|
|
11
|
-
const DIST_TAGS_URL = 'https://registry.npmjs.org/-/package/@raidiant/notifai/dist-tags'
|
|
12
|
-
const REQUEST_TIMEOUT_MS = 2_000
|
|
13
|
-
const CACHE_TTL_MS = 60 * 60 * 1000
|
|
14
|
-
|
|
15
|
-
let cached: { value: string | null; at: number } | null = null
|
|
16
|
-
|
|
17
|
-
export function compareCliSemVer(left: string, right: string): number | null {
|
|
18
|
-
const parse = (value: string): [number, number, number] | null => {
|
|
19
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(value)
|
|
20
|
-
if (!match) return null
|
|
21
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
22
|
-
}
|
|
23
|
-
const a = parse(left)
|
|
24
|
-
const b = parse(right)
|
|
25
|
-
if (a === null || b === null) return null
|
|
26
|
-
for (let index = 0; index < 3; index += 1) {
|
|
27
|
-
if (a[index] !== b[index]) return a[index]! - b[index]!
|
|
28
|
-
}
|
|
29
|
-
return 0
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function shouldConsultCliRegistry(input: {
|
|
33
|
-
interactive?: boolean
|
|
34
|
-
json?: boolean
|
|
35
|
-
env?: NodeJS.ProcessEnv
|
|
36
|
-
}): boolean {
|
|
37
|
-
if (input.json === true) return false
|
|
38
|
-
if (input.interactive !== true) return false
|
|
39
|
-
const ci = input.env?.['CI']
|
|
40
|
-
return ci !== '1' && ci !== 'true'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function latestPublishedCliVersion(
|
|
44
|
-
fetchImpl: typeof fetch = fetch,
|
|
45
|
-
): Promise<string | null> {
|
|
46
|
-
if (cached !== null && Date.now() - cached.at < CACHE_TTL_MS) return cached.value
|
|
47
|
-
try {
|
|
48
|
-
const response = await fetchImpl(DIST_TAGS_URL, {
|
|
49
|
-
headers: { accept: 'application/json' },
|
|
50
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
51
|
-
})
|
|
52
|
-
if (!response.ok) return cached?.value ?? null
|
|
53
|
-
const body: unknown = await response.json()
|
|
54
|
-
if (typeof body !== 'object' || body === null) return cached?.value ?? null
|
|
55
|
-
const latest = (body as { latest?: unknown }).latest
|
|
56
|
-
if (typeof latest !== 'string' || compareCliSemVer(latest, '0.0.0') === null) {
|
|
57
|
-
return cached?.value ?? null
|
|
58
|
-
}
|
|
59
|
-
cached = { value: latest, at: Date.now() }
|
|
60
|
-
return latest
|
|
61
|
-
} catch {
|
|
62
|
-
return cached?.value ?? null
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function newerPublishedCli(local: string | null, latest: string | null): string | null {
|
|
67
|
-
if (local === null || latest === null) return null
|
|
68
|
-
const comparison = compareCliSemVer(latest, local)
|
|
69
|
-
return comparison !== null && comparison > 0 ? latest : null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function thisCliVersion(): string | null {
|
|
73
|
-
return packageVersion()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function resetLatestPublishedCliVersionForTest(): void {
|
|
77
|
-
cached = null
|
|
78
|
-
}
|
package/src/client.test.ts
DELETED
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
import { createServer, type Server } from 'node:http'
|
|
2
|
-
import type { AddressInfo } from 'node:net'
|
|
3
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
|
-
import { CAPABILITIES_HEADER, CLI_VERSION_HEADER } from '@raidiant/notifai-protocol'
|
|
5
|
-
import { ApiCallError, NetworkError, createClient } from './client.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* These run against a real socket rather than a stubbed fetch, because the
|
|
9
|
-
* failure being fixed is a property of the transport: a server that accepts the
|
|
10
|
-
* connection and then says nothing. A mock that resolves or rejects cannot
|
|
11
|
-
* reproduce it.
|
|
12
|
-
*/
|
|
13
|
-
let server: Server | undefined
|
|
14
|
-
|
|
15
|
-
afterEach(async () => {
|
|
16
|
-
if (server) {
|
|
17
|
-
const closing = server
|
|
18
|
-
server = undefined
|
|
19
|
-
await new Promise<void>((resolve) => closing.close(() => resolve()))
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
/** Starts a server with the given behaviour and returns its base URL. */
|
|
24
|
-
async function serving(handler: Parameters<typeof createServer>[1]): Promise<string> {
|
|
25
|
-
server = createServer(handler)
|
|
26
|
-
await new Promise<void>((resolve) => server?.listen(0, '127.0.0.1', resolve))
|
|
27
|
-
const { port } = server.address() as AddressInfo
|
|
28
|
-
return `http://127.0.0.1:${port}`
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('a server that never answers', () => {
|
|
32
|
-
it('gives up instead of hanging until the harness kills the hook', async () => {
|
|
33
|
-
// Accepts the request, holds the socket open, writes nothing — ever.
|
|
34
|
-
const held: unknown[] = []
|
|
35
|
-
const baseUrl = await serving((_request, response) => {
|
|
36
|
-
held.push(response)
|
|
37
|
-
})
|
|
38
|
-
const client = createClient(baseUrl, null, { timeoutMs: 300 })
|
|
39
|
-
|
|
40
|
-
const started = Date.now()
|
|
41
|
-
await expect(client.listDevices()).rejects.toBeInstanceOf(NetworkError)
|
|
42
|
-
// The point is that it returns at all; the bound is loose so a busy CI box
|
|
43
|
-
// does not make this flaky.
|
|
44
|
-
expect(Date.now() - started).toBeLessThan(5_000)
|
|
45
|
-
expect(held).toHaveLength(1)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('says the server went quiet rather than reporting a bare abort', async () => {
|
|
49
|
-
const baseUrl = await serving(() => {})
|
|
50
|
-
const client = createClient(baseUrl, null, { timeoutMs: 300 })
|
|
51
|
-
|
|
52
|
-
await expect(client.listDevices()).rejects.toThrow(/did not respond within/)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('clamps a late request to the caller whole-operation deadline', async () => {
|
|
56
|
-
const baseUrl = await serving(() => {})
|
|
57
|
-
const started = Date.now()
|
|
58
|
-
const client = createClient(baseUrl, null, {
|
|
59
|
-
timeoutMs: 5_000,
|
|
60
|
-
deadlineAt: started + 200,
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
await expect(client.listDevices()).rejects.toBeInstanceOf(NetworkError)
|
|
64
|
-
expect(Date.now() - started).toBeLessThan(2_000)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('allows for the long poll it asked the server to hold', async () => {
|
|
68
|
-
// The deadline is per request, so a 25s long poll must not be cut off by
|
|
69
|
-
// the ordinary budget — otherwise waiting for a reply could never work.
|
|
70
|
-
let seen: string | undefined
|
|
71
|
-
const baseUrl = await serving((request, response) => {
|
|
72
|
-
seen = request.url
|
|
73
|
-
response.setHeader('content-type', 'application/json')
|
|
74
|
-
response.end(JSON.stringify({ request_id: 'req_1', reply_expires_at: null, replies: [] }))
|
|
75
|
-
})
|
|
76
|
-
const client = createClient(baseUrl, null, { timeoutMs: 300 })
|
|
77
|
-
|
|
78
|
-
const result = await client.replies('req_1', { waitSeconds: 25, afterSeq: 0 })
|
|
79
|
-
|
|
80
|
-
expect(seen).toContain('wait_seconds=25')
|
|
81
|
-
expect(result.replies).toEqual([])
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('encodes pairing and evidence path segments', async () => {
|
|
85
|
-
const seen: string[] = []
|
|
86
|
-
const baseUrl = await serving((request, response) => {
|
|
87
|
-
seen.push(`${request.method} ${request.url}`)
|
|
88
|
-
response.setHeader('content-type', 'application/json')
|
|
89
|
-
if (request.url?.includes('/poll')) {
|
|
90
|
-
response.end(JSON.stringify({ status: 'pending' }))
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
response.end(
|
|
94
|
-
JSON.stringify({
|
|
95
|
-
request_id: 'req/encoded',
|
|
96
|
-
overall: 'accepted',
|
|
97
|
-
deliveries: [],
|
|
98
|
-
warnings: [],
|
|
99
|
-
}),
|
|
100
|
-
)
|
|
101
|
-
})
|
|
102
|
-
const client = createClient(baseUrl, 'Bearer test')
|
|
103
|
-
|
|
104
|
-
await client.pollPairing('pair/id', 'verifier')
|
|
105
|
-
await client.evidence('req/encoded')
|
|
106
|
-
|
|
107
|
-
expect(seen).toEqual([
|
|
108
|
-
'POST /api/v1/pairings/pair%2Fid/poll',
|
|
109
|
-
'GET /api/v1/notifications/req%2Fencoded',
|
|
110
|
-
])
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('lists Account custom sounds from the catalog endpoint', async () => {
|
|
114
|
-
let seen: string | undefined
|
|
115
|
-
const baseUrl = await serving((request, response) => {
|
|
116
|
-
seen = `${request.method} ${request.url}`
|
|
117
|
-
response.setHeader('content-type', 'application/json')
|
|
118
|
-
response.end(JSON.stringify({ sounds: [] }))
|
|
119
|
-
})
|
|
120
|
-
const result = await createClient(baseUrl, 'Bearer test').listSounds()
|
|
121
|
-
expect(seen).toBe('GET /api/v1/sounds')
|
|
122
|
-
expect(result).toEqual({ sounds: [] })
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('refuses redirects on bearer-authenticated calls', async () => {
|
|
126
|
-
let followed = false
|
|
127
|
-
const baseUrl = await serving((request, response) => {
|
|
128
|
-
if (request.url === '/api/v1/devices') {
|
|
129
|
-
response.statusCode = 302
|
|
130
|
-
response.setHeader('location', '/stolen')
|
|
131
|
-
response.end()
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
followed = true
|
|
135
|
-
response.end('stolen')
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
await expect(createClient(baseUrl, 'Bearer secret').listDevices()).rejects.toBeInstanceOf(
|
|
139
|
-
NetworkError,
|
|
140
|
-
)
|
|
141
|
-
expect(followed).toBe(false)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('still follows redirects when the call has no bearer', async () => {
|
|
145
|
-
const baseUrl = await serving((request, response) => {
|
|
146
|
-
if (request.url === '/healthz') {
|
|
147
|
-
response.statusCode = 302
|
|
148
|
-
response.setHeader('location', '/healthz-ok')
|
|
149
|
-
response.end()
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
response.setHeader('content-type', 'application/json')
|
|
153
|
-
response.end('{}')
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
await expect(createClient(baseUrl, null).health()).resolves.toBe(true)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('puts and fetches Agent Acknowledgements on the encoded request path', async () => {
|
|
160
|
-
const seen: { method?: string; url?: string; body?: unknown }[] = []
|
|
161
|
-
const baseUrl = await serving((request, response) => {
|
|
162
|
-
let raw = ''
|
|
163
|
-
request.on('data', (chunk) => {
|
|
164
|
-
raw += String(chunk)
|
|
165
|
-
})
|
|
166
|
-
request.on('end', () => {
|
|
167
|
-
seen.push({
|
|
168
|
-
method: request.method,
|
|
169
|
-
url: request.url,
|
|
170
|
-
...(raw === '' ? {} : { body: JSON.parse(raw) as unknown }),
|
|
171
|
-
})
|
|
172
|
-
response.setHeader('content-type', 'application/json')
|
|
173
|
-
if (request.method === 'PUT') {
|
|
174
|
-
response.end(
|
|
175
|
-
JSON.stringify({
|
|
176
|
-
status: 'recorded',
|
|
177
|
-
agent_acknowledgement: {
|
|
178
|
-
text: 'I will deploy.',
|
|
179
|
-
created_at: '2026-08-13T12:01:00.000Z',
|
|
180
|
-
},
|
|
181
|
-
}),
|
|
182
|
-
)
|
|
183
|
-
} else {
|
|
184
|
-
response.end(
|
|
185
|
-
JSON.stringify({
|
|
186
|
-
request_id: 'req/encoded',
|
|
187
|
-
agent_acknowledgement_required: true,
|
|
188
|
-
agent_acknowledgement: null,
|
|
189
|
-
}),
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
const client = createClient(baseUrl, 'Bearer test')
|
|
195
|
-
|
|
196
|
-
await client.putAgentAcknowledgement('req/encoded', { text: 'I will deploy.' })
|
|
197
|
-
await client.agentAcknowledgement('req/encoded', { waitSeconds: 25 })
|
|
198
|
-
|
|
199
|
-
expect(seen).toEqual([
|
|
200
|
-
{
|
|
201
|
-
method: 'PUT',
|
|
202
|
-
url: '/api/v1/notifications/req%2Fencoded/agent-acknowledgement',
|
|
203
|
-
body: { text: 'I will deploy.' },
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
method: 'GET',
|
|
207
|
-
url: '/api/v1/notifications/req%2Fencoded/agent-acknowledgement?wait_seconds=25',
|
|
208
|
-
},
|
|
209
|
-
])
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('finalizes provider metadata before treating a storage upload as usable', async () => {
|
|
213
|
-
const seen: { method?: string; url?: string; authorization?: string; bytes: number }[] = []
|
|
214
|
-
const baseUrl = await serving((request, response) => {
|
|
215
|
-
const chunks: Buffer[] = []
|
|
216
|
-
request.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
|
|
217
|
-
request.on('end', () => {
|
|
218
|
-
seen.push({
|
|
219
|
-
method: request.method,
|
|
220
|
-
url: request.url,
|
|
221
|
-
...(request.headers.authorization
|
|
222
|
-
? { authorization: request.headers.authorization }
|
|
223
|
-
: {}),
|
|
224
|
-
bytes: Buffer.concat(chunks).byteLength,
|
|
225
|
-
})
|
|
226
|
-
if (request.method === 'POST') {
|
|
227
|
-
response.setHeader('content-type', 'application/json')
|
|
228
|
-
response.end(JSON.stringify({ media_id: 'med_1', size_bytes: 4, status: 'ready' }))
|
|
229
|
-
} else {
|
|
230
|
-
response.statusCode = 200
|
|
231
|
-
response.end()
|
|
232
|
-
}
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
const client = createClient(baseUrl, 'Bearer machine')
|
|
236
|
-
|
|
237
|
-
await client.uploadMedia(
|
|
238
|
-
{
|
|
239
|
-
media_id: 'med_1',
|
|
240
|
-
upload_url: `${baseUrl}/signed-upload`,
|
|
241
|
-
upload_headers: { 'content-type': 'image/png' },
|
|
242
|
-
expires_at: '2026-08-14T12:00:00.000Z',
|
|
243
|
-
},
|
|
244
|
-
new Uint8Array([1, 2, 3, 4]),
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
expect(seen).toEqual([
|
|
248
|
-
{ method: 'PUT', url: '/signed-upload', bytes: 4 },
|
|
249
|
-
{
|
|
250
|
-
method: 'POST',
|
|
251
|
-
url: '/api/v1/media/med_1/finalize',
|
|
252
|
-
authorization: 'Bearer machine',
|
|
253
|
-
bytes: 0,
|
|
254
|
-
},
|
|
255
|
-
])
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('refuses a signed upload redirect without replaying its headers or finalizing', async () => {
|
|
259
|
-
let followed = false
|
|
260
|
-
let finalized = false
|
|
261
|
-
const baseUrl = await serving((request, response) => {
|
|
262
|
-
if (request.url === '/signed-upload') {
|
|
263
|
-
response.statusCode = 302
|
|
264
|
-
response.setHeader('location', `${baseUrl}/stolen`)
|
|
265
|
-
response.end()
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
if (request.url === '/stolen') followed = true
|
|
269
|
-
if (request.url === '/api/v1/media/med_1/finalize') finalized = true
|
|
270
|
-
response.statusCode = 200
|
|
271
|
-
response.end()
|
|
272
|
-
})
|
|
273
|
-
const client = createClient(baseUrl, 'Bearer machine')
|
|
274
|
-
|
|
275
|
-
await expect(
|
|
276
|
-
client.uploadMedia(
|
|
277
|
-
{
|
|
278
|
-
media_id: 'med_1',
|
|
279
|
-
upload_url: `${baseUrl}/signed-upload`,
|
|
280
|
-
upload_headers: { authorization: 'Signed secret' },
|
|
281
|
-
expires_at: '2026-08-14T12:00:00.000Z',
|
|
282
|
-
},
|
|
283
|
-
new Uint8Array([1, 2, 3, 4]),
|
|
284
|
-
),
|
|
285
|
-
).rejects.toThrow(/redirect is refused/i)
|
|
286
|
-
expect(followed).toBe(false)
|
|
287
|
-
expect(finalized).toBe(false)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it('treats a truncated body as a transport failure, not a crash', async () => {
|
|
291
|
-
// A body that stops mid-JSON used to escape as a raw parse error outside
|
|
292
|
-
// the retry path, so nothing backed off and retried it.
|
|
293
|
-
const baseUrl = await serving((_request, response) => {
|
|
294
|
-
response.setHeader('content-type', 'application/json')
|
|
295
|
-
response.write('{"devices": [')
|
|
296
|
-
response.destroy()
|
|
297
|
-
})
|
|
298
|
-
const client = createClient(baseUrl, null, { timeoutMs: 500 })
|
|
299
|
-
|
|
300
|
-
await expect(client.listDevices()).rejects.toBeInstanceOf(NetworkError)
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
describe('compatibility metadata transport', () => {
|
|
305
|
-
it('advertises named CLI capabilities only on authenticated traffic', async () => {
|
|
306
|
-
const seen: Array<{
|
|
307
|
-
url: string | undefined
|
|
308
|
-
cliVersion: string | string[] | undefined
|
|
309
|
-
capabilities: string | string[] | undefined
|
|
310
|
-
}> = []
|
|
311
|
-
const baseUrl = await serving((request, response) => {
|
|
312
|
-
seen.push({
|
|
313
|
-
url: request.url,
|
|
314
|
-
cliVersion: request.headers[CLI_VERSION_HEADER],
|
|
315
|
-
capabilities: request.headers[CAPABILITIES_HEADER],
|
|
316
|
-
})
|
|
317
|
-
response.setHeader('content-type', 'application/json')
|
|
318
|
-
response.end(
|
|
319
|
-
JSON.stringify({
|
|
320
|
-
schema_version: 1,
|
|
321
|
-
platform: 'macos',
|
|
322
|
-
payload_limit_bytes: 4096,
|
|
323
|
-
sounds: [],
|
|
324
|
-
interruption_levels: [],
|
|
325
|
-
fields: [],
|
|
326
|
-
}),
|
|
327
|
-
)
|
|
328
|
-
})
|
|
329
|
-
const options = {
|
|
330
|
-
cliVersion: '5.0.0',
|
|
331
|
-
capabilities: ['agent_acknowledgement'] as const,
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
await createClient(baseUrl, 'Bearer machine', options).capabilities('macos', '0.1.0', '42')
|
|
335
|
-
await createClient(baseUrl, null, options).capabilities('macos')
|
|
336
|
-
|
|
337
|
-
expect(seen).toEqual([
|
|
338
|
-
{
|
|
339
|
-
url: '/api/v1/capabilities/macos?app_version=0.1.0&app_build=42',
|
|
340
|
-
cliVersion: '5.0.0',
|
|
341
|
-
capabilities: 'agent_acknowledgement',
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
url: '/api/v1/capabilities/macos',
|
|
345
|
-
cliVersion: undefined,
|
|
346
|
-
capabilities: undefined,
|
|
347
|
-
},
|
|
348
|
-
])
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
it('keeps recovery typed when a feature is unavailable', async () => {
|
|
352
|
-
const baseUrl = await serving((_request, response) => {
|
|
353
|
-
response.statusCode = 422
|
|
354
|
-
response.setHeader('content-type', 'application/json')
|
|
355
|
-
response.end(
|
|
356
|
-
JSON.stringify({
|
|
357
|
-
error: {
|
|
358
|
-
code: 'feature_unavailable',
|
|
359
|
-
message: "The selected device can't answer questions.",
|
|
360
|
-
next_action: 'run an untrusted server command',
|
|
361
|
-
recovery_action: 'update_companion',
|
|
362
|
-
details: {
|
|
363
|
-
affected_operation: 'answer_questions',
|
|
364
|
-
missing_capabilities: ['answer'],
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
}),
|
|
368
|
-
)
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
let caught: unknown
|
|
372
|
-
try {
|
|
373
|
-
await createClient(baseUrl, 'Bearer machine').compatibility()
|
|
374
|
-
} catch (error) {
|
|
375
|
-
caught = error
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
expect(caught).toBeInstanceOf(ApiCallError)
|
|
379
|
-
expect(caught).toMatchObject({
|
|
380
|
-
status: 422,
|
|
381
|
-
code: 'feature_unavailable',
|
|
382
|
-
recoveryAction: 'update_companion',
|
|
383
|
-
nextAction: 'run an untrusted server command',
|
|
384
|
-
details: {
|
|
385
|
-
affected_operation: 'answer_questions',
|
|
386
|
-
missing_capabilities: ['answer'],
|
|
387
|
-
},
|
|
388
|
-
})
|
|
389
|
-
})
|
|
390
|
-
})
|