@mycadet/cli 0.12.10 → 0.13.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/README.md +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/package.json +3 -2
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +64 -2
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +233 -36
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +8 -7
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +17 -7
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +13 -3
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +117 -47
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/hatch.ts +10 -52
- package/src/commands/pair.ts +9 -3
- package/src/commands/ps.ts +4 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/tunnel.ts +43 -4
- package/src/commands/upgrade.ts +84 -157
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +20 -23
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +5 -2
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +65 -34
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +18 -52
- package/src/lib/hatch-next-steps.ts +15 -2
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +3 -3
- package/src/lib/local.ts +10 -8
- package/src/lib/nginx-ingress.ts +254 -36
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/tunnel.ts +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +23 -1
- package/src/lib/xdg-log.ts +11 -0
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
package/src/commands/unpair.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `cadet unpair <name> [--yes]`
|
|
3
|
-
*
|
|
4
|
-
* Forget a pairing imported from another machine via `cadet connect import`:
|
|
5
|
-
* remove its lockfile entry and stored guardian token from THIS machine. Only
|
|
6
|
-
* paired assistants (`cloud: "paired"`) can be unpaired — `cadet retire` owns
|
|
7
|
-
* local and cloud assistants.
|
|
8
|
-
*
|
|
9
|
-
* This is client-side only: it forgets the connection here but does not revoke
|
|
10
|
-
* the device on the host. (Host-side revocation is `cadet devices`.)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
formatAssistantLookupError,
|
|
15
|
-
getAssistantDisplayName,
|
|
16
|
-
lookupAssistantByIdentifier,
|
|
17
|
-
removeAssistantEntry,
|
|
18
|
-
} from "../lib/assistant-config";
|
|
19
|
-
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
20
|
-
import {
|
|
21
|
-
canPromptForConfirmation,
|
|
22
|
-
confirmAction,
|
|
23
|
-
} from "../lib/confirm-action.js";
|
|
24
|
-
import { deleteGuardianToken } from "../lib/guardian-token";
|
|
25
|
-
|
|
26
|
-
function printUsage(): void {
|
|
27
|
-
console.log(`cadet unpair [beta] - Forget a paired assistant imported from another machine
|
|
28
|
-
|
|
29
|
-
USAGE:
|
|
30
|
-
cadet unpair <name> [--yes]
|
|
31
|
-
|
|
32
|
-
ARGUMENTS:
|
|
33
|
-
<name> Name or id of the paired assistant to forget
|
|
34
|
-
|
|
35
|
-
OPTIONS:
|
|
36
|
-
--yes Skip the interactive confirmation prompt (for automation)
|
|
37
|
-
|
|
38
|
-
Removes the local connection (lockfile entry + stored token). Only paired
|
|
39
|
-
assistants (imported via 'cadet connect import') can be unpaired; use
|
|
40
|
-
'cadet retire' for local or cloud assistants.
|
|
41
|
-
|
|
42
|
-
EXAMPLES:
|
|
43
|
-
cadet unpair paired-desk
|
|
44
|
-
cadet unpair "Desk Box"
|
|
45
|
-
cadet unpair paired-desk --yes
|
|
46
|
-
`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const unpair = Object.assign(
|
|
50
|
-
async function unpair(): Promise<void> {
|
|
51
|
-
const args = process.argv.slice(3);
|
|
52
|
-
|
|
53
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
54
|
-
printUsage();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const yes = args.includes("--yes");
|
|
59
|
-
const name = parseAssistantTargetArg(args, []);
|
|
60
|
-
if (!name) {
|
|
61
|
-
console.error("Error: assistant name or id is required.");
|
|
62
|
-
printUsage();
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const lookup = lookupAssistantByIdentifier(name);
|
|
67
|
-
if (lookup.status !== "found") {
|
|
68
|
-
console.error(formatAssistantLookupError(name, lookup));
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
const entry = lookup.entry;
|
|
72
|
-
|
|
73
|
-
if (entry.cloud !== "paired") {
|
|
74
|
-
console.error(
|
|
75
|
-
`Error: '${name}' is not a paired assistant. Use \`cadet retire\` to remove a local or cloud assistant.`,
|
|
76
|
-
);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Print the resolved identity before acting (cli/AGENTS.md).
|
|
81
|
-
const displayName = getAssistantDisplayName(entry);
|
|
82
|
-
console.log("Pairing to unpair:");
|
|
83
|
-
if (displayName !== entry.assistantId) {
|
|
84
|
-
console.log(` Name: ${displayName}`);
|
|
85
|
-
}
|
|
86
|
-
console.log(` ID: ${entry.assistantId}`);
|
|
87
|
-
if (entry.runtimeUrl) {
|
|
88
|
-
console.log(` Host: ${entry.runtimeUrl}`);
|
|
89
|
-
}
|
|
90
|
-
console.log("");
|
|
91
|
-
|
|
92
|
-
if (!yes) {
|
|
93
|
-
if (!canPromptForConfirmation()) {
|
|
94
|
-
console.error(
|
|
95
|
-
"Error: Refusing to unpair without confirmation in a non-interactive terminal.",
|
|
96
|
-
);
|
|
97
|
-
console.error("Re-run with --yes to confirm from automation.");
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
const confirmed = await confirmAction(
|
|
101
|
-
"Press Enter to unpair, or Esc/q to cancel: ",
|
|
102
|
-
);
|
|
103
|
-
if (!confirmed) {
|
|
104
|
-
console.log("Unpair cancelled.");
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
removeAssistantEntry(entry.assistantId);
|
|
110
|
-
deleteGuardianToken(entry.assistantId);
|
|
111
|
-
|
|
112
|
-
console.log(
|
|
113
|
-
`Unpaired '${name}' — removed the local connection (lockfile entry + token).`,
|
|
114
|
-
);
|
|
115
|
-
console.log("");
|
|
116
|
-
console.log(
|
|
117
|
-
"Note: this only forgets the connection on this machine. The assistant's host can fully revoke this device from its side.",
|
|
118
|
-
);
|
|
119
|
-
},
|
|
120
|
-
{ description: "Forget a paired assistant imported from another machine [beta]" },
|
|
121
|
-
);
|
|
122
|
-
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { pollJobUntilDone } from "../job-polling.js";
|
|
4
|
-
import type { UnifiedJobStatus } from "../cloud-client.js";
|
|
5
|
-
|
|
6
|
-
describe("pollJobUntilDone", () => {
|
|
7
|
-
test("returns terminal 'complete' after N processing polls", async () => {
|
|
8
|
-
const statuses: UnifiedJobStatus[] = [
|
|
9
|
-
{ jobId: "j1", type: "export", status: "processing" },
|
|
10
|
-
{ jobId: "j1", type: "export", status: "processing" },
|
|
11
|
-
{
|
|
12
|
-
jobId: "j1",
|
|
13
|
-
type: "export",
|
|
14
|
-
status: "complete",
|
|
15
|
-
bundleKey: "bundles/j1.tar.gz",
|
|
16
|
-
},
|
|
17
|
-
];
|
|
18
|
-
let i = 0;
|
|
19
|
-
const result = await pollJobUntilDone({
|
|
20
|
-
poll: async () => statuses[i++]!,
|
|
21
|
-
intervalMs: 1,
|
|
22
|
-
timeoutMs: 1_000,
|
|
23
|
-
label: "test export",
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
expect(result.status).toBe("complete");
|
|
27
|
-
if (result.status === "complete") {
|
|
28
|
-
expect(result.bundleKey).toBe("bundles/j1.tar.gz");
|
|
29
|
-
}
|
|
30
|
-
expect(i).toBe(3);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("propagates terminal 'failed' status to caller without throwing", async () => {
|
|
34
|
-
const result = await pollJobUntilDone({
|
|
35
|
-
poll: async () => ({
|
|
36
|
-
jobId: "j2",
|
|
37
|
-
type: "import",
|
|
38
|
-
status: "failed",
|
|
39
|
-
error: "bad bundle",
|
|
40
|
-
}),
|
|
41
|
-
intervalMs: 1,
|
|
42
|
-
timeoutMs: 1_000,
|
|
43
|
-
label: "test import",
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(result.status).toBe("failed");
|
|
47
|
-
if (result.status === "failed") {
|
|
48
|
-
expect(result.error).toBe("bad bundle");
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("throws with label when polling exceeds timeoutMs", async () => {
|
|
53
|
-
let calls = 0;
|
|
54
|
-
await expect(
|
|
55
|
-
pollJobUntilDone({
|
|
56
|
-
poll: async () => {
|
|
57
|
-
calls += 1;
|
|
58
|
-
return { jobId: "j3", type: "export", status: "processing" };
|
|
59
|
-
},
|
|
60
|
-
intervalMs: 20,
|
|
61
|
-
timeoutMs: 10,
|
|
62
|
-
label: "slow export",
|
|
63
|
-
}),
|
|
64
|
-
).rejects.toThrow(/slow export/);
|
|
65
|
-
|
|
66
|
-
// The loop does one poll before checking the deadline, so calls ≥ 1.
|
|
67
|
-
expect(calls).toBeGreaterThanOrEqual(1);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("uses defaults when intervalMs/timeoutMs are omitted (fast path)", async () => {
|
|
71
|
-
// Fast path: first poll is already terminal so neither default matters.
|
|
72
|
-
const result = await pollJobUntilDone({
|
|
73
|
-
poll: async () => ({ jobId: "j4", type: "export", status: "complete" }),
|
|
74
|
-
label: "defaults test",
|
|
75
|
-
});
|
|
76
|
-
expect(result.status).toBe("complete");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe("transient-error retry", () => {
|
|
80
|
-
let warnSpy: ReturnType<typeof spyOn>;
|
|
81
|
-
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
afterEach(() => {
|
|
87
|
-
warnSpy.mockRestore();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("retries N-1 transient errors then returns terminal status", async () => {
|
|
91
|
-
const maxTransientErrors = 3;
|
|
92
|
-
let calls = 0;
|
|
93
|
-
const result = await pollJobUntilDone({
|
|
94
|
-
label: "flaky export",
|
|
95
|
-
intervalMs: 1,
|
|
96
|
-
timeoutMs: 1_000,
|
|
97
|
-
maxTransientErrors,
|
|
98
|
-
poll: async () => {
|
|
99
|
-
calls += 1;
|
|
100
|
-
if (calls < maxTransientErrors) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
`Local job status check failed: 503 Service Unavailable`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
jobId: "j5",
|
|
107
|
-
type: "export",
|
|
108
|
-
status: "complete",
|
|
109
|
-
} as UnifiedJobStatus;
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
expect(result.status).toBe("complete");
|
|
113
|
-
expect(calls).toBe(maxTransientErrors);
|
|
114
|
-
// One warning per retried transient error (first two attempts).
|
|
115
|
-
expect(warnSpy).toHaveBeenCalledTimes(maxTransientErrors - 1);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("propagates the last error once maxTransientErrors is exceeded", async () => {
|
|
119
|
-
const maxTransientErrors = 2;
|
|
120
|
-
let calls = 0;
|
|
121
|
-
await expect(
|
|
122
|
-
pollJobUntilDone({
|
|
123
|
-
label: "always broken",
|
|
124
|
-
intervalMs: 1,
|
|
125
|
-
timeoutMs: 1_000,
|
|
126
|
-
maxTransientErrors,
|
|
127
|
-
poll: async () => {
|
|
128
|
-
calls += 1;
|
|
129
|
-
throw new Error(`Local job status check failed: 502 Bad Gateway`);
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
).rejects.toThrow(/502 Bad Gateway/);
|
|
133
|
-
// Helper makes `maxTransientErrors + 1` attempts before giving up: the
|
|
134
|
-
// first attempt plus N retries, counted against the budget.
|
|
135
|
-
expect(calls).toBe(maxTransientErrors + 1);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("permanent 4xx errors (except 429) propagate immediately", async () => {
|
|
139
|
-
let calls = 0;
|
|
140
|
-
await expect(
|
|
141
|
-
pollJobUntilDone({
|
|
142
|
-
label: "auth broken",
|
|
143
|
-
intervalMs: 1,
|
|
144
|
-
timeoutMs: 1_000,
|
|
145
|
-
maxTransientErrors: 5,
|
|
146
|
-
poll: async () => {
|
|
147
|
-
calls += 1;
|
|
148
|
-
throw new Error(`Local job status check failed: 403 Forbidden`);
|
|
149
|
-
},
|
|
150
|
-
}),
|
|
151
|
-
).rejects.toThrow(/403 Forbidden/);
|
|
152
|
-
expect(calls).toBe(1);
|
|
153
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("429 rate-limit is retried as transient", async () => {
|
|
157
|
-
let calls = 0;
|
|
158
|
-
const result = await pollJobUntilDone({
|
|
159
|
-
label: "rate limited",
|
|
160
|
-
intervalMs: 1,
|
|
161
|
-
timeoutMs: 1_000,
|
|
162
|
-
maxTransientErrors: 3,
|
|
163
|
-
poll: async () => {
|
|
164
|
-
calls += 1;
|
|
165
|
-
if (calls === 1) {
|
|
166
|
-
throw new Error(`Local job status check failed: 429 Too Many`);
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
jobId: "j6",
|
|
170
|
-
type: "export",
|
|
171
|
-
status: "complete",
|
|
172
|
-
} as UnifiedJobStatus;
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
expect(result.status).toBe("complete");
|
|
176
|
-
expect(calls).toBe(2);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("refreshOn401 is invoked on 401 and polling continues after refresh", async () => {
|
|
180
|
-
let calls = 0;
|
|
181
|
-
let refreshes = 0;
|
|
182
|
-
const result = await pollJobUntilDone({
|
|
183
|
-
label: "expiring auth",
|
|
184
|
-
intervalMs: 1,
|
|
185
|
-
timeoutMs: 1_000,
|
|
186
|
-
maxTransientErrors: 0,
|
|
187
|
-
refreshOn401: async () => {
|
|
188
|
-
refreshes += 1;
|
|
189
|
-
},
|
|
190
|
-
poll: async () => {
|
|
191
|
-
calls += 1;
|
|
192
|
-
if (calls === 1) {
|
|
193
|
-
throw new Error("Local job status check failed: 401 Unauthorized");
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
jobId: "j401",
|
|
197
|
-
type: "export",
|
|
198
|
-
status: "complete",
|
|
199
|
-
} as UnifiedJobStatus;
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
expect(result.status).toBe("complete");
|
|
203
|
-
expect(refreshes).toBe(1);
|
|
204
|
-
expect(calls).toBe(2);
|
|
205
|
-
// The 401 branch logs its own distinct warning (not the generic
|
|
206
|
-
// "polling failed:" one) so operators can distinguish an
|
|
207
|
-
// auth refresh from a transient-error retry in the output.
|
|
208
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
209
|
-
expect.stringContaining("refreshing auth"),
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("propagates 401 once maxAuthRefreshes is exceeded", async () => {
|
|
214
|
-
const maxAuthRefreshes = 2;
|
|
215
|
-
let calls = 0;
|
|
216
|
-
let refreshes = 0;
|
|
217
|
-
await expect(
|
|
218
|
-
pollJobUntilDone({
|
|
219
|
-
label: "persistently unauthorized",
|
|
220
|
-
intervalMs: 1,
|
|
221
|
-
timeoutMs: 1_000,
|
|
222
|
-
maxAuthRefreshes,
|
|
223
|
-
refreshOn401: async () => {
|
|
224
|
-
refreshes += 1;
|
|
225
|
-
},
|
|
226
|
-
poll: async () => {
|
|
227
|
-
calls += 1;
|
|
228
|
-
throw new Error("Local job status check failed: 401 Unauthorized");
|
|
229
|
-
},
|
|
230
|
-
}),
|
|
231
|
-
).rejects.toThrow(/401 Unauthorized/);
|
|
232
|
-
// Helper allows `maxAuthRefreshes` successful refresh-and-retry cycles
|
|
233
|
-
// (each counted against the budget after the poll fails), plus one
|
|
234
|
-
// final attempt on the refreshed token that exceeds the budget.
|
|
235
|
-
expect(calls).toBe(maxAuthRefreshes + 1);
|
|
236
|
-
expect(refreshes).toBe(maxAuthRefreshes);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("without refreshOn401, 401 still propagates as a permanent 4xx", async () => {
|
|
240
|
-
let calls = 0;
|
|
241
|
-
await expect(
|
|
242
|
-
pollJobUntilDone({
|
|
243
|
-
label: "no refresh hook",
|
|
244
|
-
intervalMs: 1,
|
|
245
|
-
timeoutMs: 1_000,
|
|
246
|
-
poll: async () => {
|
|
247
|
-
calls += 1;
|
|
248
|
-
throw new Error("Local job status check failed: 401 Unauthorized");
|
|
249
|
-
},
|
|
250
|
-
}),
|
|
251
|
-
).rejects.toThrow(/401 Unauthorized/);
|
|
252
|
-
expect(calls).toBe(1);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test("unclassified network-style errors are treated as transient", async () => {
|
|
256
|
-
let calls = 0;
|
|
257
|
-
const result = await pollJobUntilDone({
|
|
258
|
-
label: "network blip",
|
|
259
|
-
intervalMs: 1,
|
|
260
|
-
timeoutMs: 1_000,
|
|
261
|
-
maxTransientErrors: 3,
|
|
262
|
-
poll: async () => {
|
|
263
|
-
calls += 1;
|
|
264
|
-
if (calls === 1) {
|
|
265
|
-
throw new Error("fetch failed");
|
|
266
|
-
}
|
|
267
|
-
return {
|
|
268
|
-
jobId: "j7",
|
|
269
|
-
type: "export",
|
|
270
|
-
status: "complete",
|
|
271
|
-
} as UnifiedJobStatus;
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
expect(result.status).toBe("complete");
|
|
275
|
-
expect(calls).toBe(2);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
});
|