@crouton-kit/crouter 0.3.40 → 0.3.41
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/dist/build-root.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
- package/dist/clients/attach/attach-cmd.js +706 -733
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/web/server.js +5 -28
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/canvas-rebuild-index.js +5 -5
- package/dist/commands/node-snapshot.js +9 -1
- package/dist/commands/node.js +37 -17
- package/dist/commands/push.js +8 -0
- package/dist/commands/revive.d.ts +10 -0
- package/dist/commands/revive.js +28 -10
- package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/setup-core.js +3 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +94 -0
- package/dist/core/__tests__/boot.test.js +4 -4
- package/dist/core/__tests__/canvas.test.js +19 -7
- package/dist/core/__tests__/child-followup.test.js +15 -5
- package/dist/core/__tests__/daemon-boot.test.js +6 -1
- package/dist/core/__tests__/daemon-wedge.test.js +18 -1
- package/dist/core/__tests__/fault-classifier.test.js +30 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
- package/dist/core/__tests__/full/spike-harness.test.js +8 -5
- package/dist/core/__tests__/grace-clock.test.js +18 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
- package/dist/core/__tests__/prune-to-limit.test.js +14 -0
- package/dist/core/__tests__/review-model-floor.test.js +32 -0
- package/dist/core/__tests__/revive.test.js +18 -4
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/worktree.test.js +85 -0
- package/dist/core/canvas/boot.js +12 -7
- package/dist/core/canvas/canvas.d.ts +27 -8
- package/dist/core/canvas/canvas.js +54 -26
- package/dist/core/canvas/db.js +14 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/paths.d.ts +10 -9
- package/dist/core/canvas/paths.js +10 -9
- package/dist/core/canvas/pid.d.ts +155 -1
- package/dist/core/canvas/pid.js +306 -1
- package/dist/core/canvas/status-glyph.d.ts +7 -0
- package/dist/core/canvas/status-glyph.js +10 -1
- package/dist/core/canvas/types.d.ts +34 -0
- package/dist/core/fault-classifier.js +5 -1
- package/dist/core/runtime/bearings.js +4 -0
- package/dist/core/runtime/branded-host.d.ts +7 -0
- package/dist/core/runtime/branded-host.js +44 -17
- package/dist/core/runtime/broker-sdk.js +28 -68
- package/dist/core/runtime/broker.js +35 -4
- package/dist/core/runtime/host.d.ts +3 -3
- package/dist/core/runtime/host.js +148 -33
- package/dist/core/runtime/launch.d.ts +15 -15
- package/dist/core/runtime/launch.js +58 -4
- package/dist/core/runtime/naming.js +3 -2
- package/dist/core/runtime/nodes.d.ts +3 -1
- package/dist/core/runtime/nodes.js +1 -0
- package/dist/core/runtime/pi-cli.d.ts +2 -0
- package/dist/core/runtime/pi-cli.js +51 -0
- package/dist/core/runtime/placement.d.ts +1 -1
- package/dist/core/runtime/placement.js +5 -2
- package/dist/core/runtime/recap.js +2 -1
- package/dist/core/runtime/revive.d.ts +21 -15
- package/dist/core/runtime/revive.js +87 -52
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +152 -100
- package/dist/core/runtime/tmux.d.ts +7 -10
- package/dist/core/runtime/tmux.js +9 -11
- package/dist/core/worktree.d.ts +35 -0
- package/dist/core/worktree.js +158 -0
- package/dist/daemon/crtrd.d.ts +17 -8
- package/dist/daemon/crtrd.js +191 -40
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
- package/dist/pi-extensions/canvas-context-intro.js +34 -23
- package/dist/pi-extensions/canvas-stophook.js +11 -5
- package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
- package/dist/web-client/assets/index-DrkcvANq.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/docs/compat/hearth-crtr-v1.md +191 -0
- package/docs/public-api.md +75 -0
- package/package.json +3 -4
- package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
- package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
- package/dist/core/hearth/config.d.ts +0 -3
- package/dist/core/hearth/config.js +0 -108
- package/dist/core/hearth/guest-env.d.ts +0 -9
- package/dist/core/hearth/guest-env.js +0 -27
- package/dist/core/hearth/index.d.ts +0 -4
- package/dist/core/hearth/index.js +0 -4
- package/dist/core/hearth/model-auth-guest.d.ts +0 -8
- package/dist/core/hearth/model-auth-guest.js +0 -430
- package/dist/core/hearth/provider.d.ts +0 -36
- package/dist/core/hearth/provider.js +0 -10
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -362
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
- package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
- package/dist/core/hearth/providers/blaxel-home.d.ts +0 -60
- package/dist/core/hearth/providers/blaxel-home.js +0 -405
- package/dist/core/hearth/providers/blaxel.d.ts +0 -36
- package/dist/core/hearth/providers/blaxel.js +0 -364
- package/dist/core/hearth/providers/types.d.ts +0 -93
- package/dist/core/hearth/types.d.ts +0 -155
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
- package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
- package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
- package/dist/hearth/control-plane/config.d.ts +0 -21
- package/dist/hearth/control-plane/config.js +0 -77
- package/dist/hearth/control-plane/db.d.ts +0 -30
- package/dist/hearth/control-plane/db.js +0 -561
- package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
- package/dist/hearth/control-plane/hearth-target.js +0 -68
- package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
- package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
- package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
- package/dist/hearth/control-plane/ingress/route-store.js +0 -61
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
- package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
- package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
- package/dist/hearth/control-plane/main.d.ts +0 -1
- package/dist/hearth/control-plane/main.js +0 -88
- package/dist/hearth/control-plane/node-message.d.ts +0 -31
- package/dist/hearth/control-plane/node-message.js +0 -98
- package/dist/hearth/control-plane/register.d.ts +0 -15
- package/dist/hearth/control-plane/register.js +0 -34
- package/dist/hearth/control-plane/registry.d.ts +0 -22
- package/dist/hearth/control-plane/registry.js +0 -168
- package/dist/hearth/control-plane/relay.d.ts +0 -44
- package/dist/hearth/control-plane/relay.js +0 -711
- package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
- package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
- package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
- package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
- package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
- package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
- package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
- package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
- package/dist/hearth/control-plane/secrets.d.ts +0 -31
- package/dist/hearth/control-plane/secrets.js +0 -134
- package/dist/hearth/control-plane/server.d.ts +0 -27
- package/dist/hearth/control-plane/server.js +0 -482
- package/dist/hearth/control-plane/serving.d.ts +0 -15
- package/dist/hearth/control-plane/serving.js +0 -106
- package/dist/hearth/control-plane/session.d.ts +0 -68
- package/dist/hearth/control-plane/session.js +0 -273
- package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
- package/dist/hearth/control-plane/triggers/acl.js +0 -52
- package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
- package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
- package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
- package/dist/hearth/control-plane/triggers/deliver.js +0 -76
- package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
- package/dist/hearth/control-plane/triggers/envelope.js +0 -38
- package/dist/hearth/control-plane/types.d.ts +0 -86
- package/dist/hearth/control-plane/types.js +0 -1
- package/dist/hearth/control-plane/wake.d.ts +0 -86
- package/dist/hearth/control-plane/wake.js +0 -550
- package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
- /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
- /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { SandboxInstance, VolumeInstance, getVolume, listSandboxes } from '@blaxel/core';
|
|
4
|
-
import { general } from '../../errors.js';
|
|
5
|
-
const TERMINATED_STATES = new Set(['TERMINATED', 'DELETING', 'DEACTIVATED', 'DEACTIVATING', 'FAILED']);
|
|
6
|
-
const PROVISIONING_STATES = new Set(['UPLOADING', 'BUILDING', 'DEPLOYING', 'BUILT']);
|
|
7
|
-
function readCrtrVersion() {
|
|
8
|
-
const file = new URL('../../../../package.json', import.meta.url);
|
|
9
|
-
const pkg = JSON.parse(readFileSync(file, 'utf8'));
|
|
10
|
-
if (typeof pkg.version !== 'string' || pkg.version.trim() === '') {
|
|
11
|
-
throw general('missing crtr package version', { packageJson: file.pathname });
|
|
12
|
-
}
|
|
13
|
-
return pkg.version;
|
|
14
|
-
}
|
|
15
|
-
function defaultImageRef(templateVersion) {
|
|
16
|
-
return `crouter-hearth-home-${templateVersion.replace(/\./g, '-')}`;
|
|
17
|
-
}
|
|
18
|
-
function shellQuote(value) {
|
|
19
|
-
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
20
|
-
}
|
|
21
|
-
function shStatus(status) {
|
|
22
|
-
if (status === undefined || status === null || status === '')
|
|
23
|
-
return 'unknown';
|
|
24
|
-
const normalized = status.toUpperCase();
|
|
25
|
-
if (normalized === 'DEPLOYED')
|
|
26
|
-
return 'running';
|
|
27
|
-
if (normalized === 'RUNNING' || normalized === 'STANDBY')
|
|
28
|
-
return 'running';
|
|
29
|
-
if (TERMINATED_STATES.has(normalized))
|
|
30
|
-
return 'terminated';
|
|
31
|
-
if (PROVISIONING_STATES.has(normalized))
|
|
32
|
-
return 'provisioning';
|
|
33
|
-
if (normalized === 'PAUSED' || normalized === 'SUSPENDED')
|
|
34
|
-
return 'paused';
|
|
35
|
-
return status;
|
|
36
|
-
}
|
|
37
|
-
function record(value) {
|
|
38
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
39
|
-
return null;
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
function rawFromSandbox(sandbox) {
|
|
43
|
-
return {
|
|
44
|
-
metadata: sandbox.metadata,
|
|
45
|
-
status: sandbox.status ?? null,
|
|
46
|
-
spec: sandbox.spec,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function rawFromVolume(volume) {
|
|
50
|
-
return {
|
|
51
|
-
metadata: volume.metadata,
|
|
52
|
-
status: volume.status ?? null,
|
|
53
|
-
spec: volume.spec,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function toEnvVars(envs) {
|
|
57
|
-
if (envs === undefined)
|
|
58
|
-
return undefined;
|
|
59
|
-
return Object.entries(envs)
|
|
60
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
61
|
-
.map(([name, value]) => ({ name, value }));
|
|
62
|
-
}
|
|
63
|
-
function resolveExecCommand(input) {
|
|
64
|
-
const hasCommand = typeof input.command === 'string';
|
|
65
|
-
const hasArgv = input.argv !== undefined;
|
|
66
|
-
if (hasCommand === hasArgv) {
|
|
67
|
-
throw general('provider exec requires exactly one of command or argv', { hasCommand, hasArgv });
|
|
68
|
-
}
|
|
69
|
-
if (hasCommand) {
|
|
70
|
-
const command = input.command?.trim() ?? '';
|
|
71
|
-
if (command === '')
|
|
72
|
-
throw general('provider exec command must not be empty');
|
|
73
|
-
return command;
|
|
74
|
-
}
|
|
75
|
-
const argv = input.argv ?? [];
|
|
76
|
-
if (argv.length === 0)
|
|
77
|
-
throw general('provider exec argv must not be empty');
|
|
78
|
-
for (const arg of argv) {
|
|
79
|
-
if (typeof arg !== 'string' || arg === '')
|
|
80
|
-
throw general('provider exec argv contains an empty argument');
|
|
81
|
-
}
|
|
82
|
-
return argv.map(shellQuote).join(' ');
|
|
83
|
-
}
|
|
84
|
-
// The Blaxel SDK (@blaxel/core 0.2.94) does not always reject with an `Error` instance — a 404
|
|
85
|
-
// comes back as a plain object (e.g. `{code:404,error:"Sandbox not found"}`), and `String()` on
|
|
86
|
-
// such an object yields the useless literal "[object Object]", which matches none of the regex
|
|
87
|
-
// below. Recognize the structured shape directly (a numeric-or-string `404` status field) AND
|
|
88
|
-
// fall back to JSON-serializing the value (mirroring wake.ts's `serializeError`) before running
|
|
89
|
-
// the message regex, so both an `Error("... not found")` and a plain-object 404 are caught.
|
|
90
|
-
export function isMissingResourceError(error) {
|
|
91
|
-
const fields = record(error);
|
|
92
|
-
if (fields !== null) {
|
|
93
|
-
for (const key of ['code', 'status', 'status_code', 'statusCode']) {
|
|
94
|
-
const value = fields[key];
|
|
95
|
-
if (value === 404 || value === '404')
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const message = error instanceof Error ? error.message : (JSON.stringify(error) ?? String(error));
|
|
100
|
-
return /\b(not found|404|does not exist|missing)\b/i.test(message);
|
|
101
|
-
}
|
|
102
|
-
// Accepts either a live `SandboxInstance` or a raw wire `Sandbox` page item — only the
|
|
103
|
-
// `metadata`/`status`/`spec` fields are read, so mapping raw list pages avoids constructing
|
|
104
|
-
// `SandboxInstance`s (whose `list()` eagerly warms an H2 session per sandbox — pointless for a
|
|
105
|
-
// destroy sweep).
|
|
106
|
-
function toMachineRef(sandbox) {
|
|
107
|
-
const metadata = sandbox.metadata;
|
|
108
|
-
const status = sandbox.status ?? undefined;
|
|
109
|
-
return {
|
|
110
|
-
providerName: 'blaxel',
|
|
111
|
-
machineId: metadata.name,
|
|
112
|
-
state: shStatus(status),
|
|
113
|
-
updatedAt: metadata.updatedAt ?? null,
|
|
114
|
-
raw: rawFromSandbox(sandbox),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function toRouteRef(machineId, targetPort, preview, raw) {
|
|
118
|
-
const url = preview.spec.url ?? null;
|
|
119
|
-
return {
|
|
120
|
-
routeId: preview.name,
|
|
121
|
-
machineId,
|
|
122
|
-
targetPort,
|
|
123
|
-
url,
|
|
124
|
-
concreteUrl: url,
|
|
125
|
-
concreteUrlSource: 'provider',
|
|
126
|
-
raw: raw ?? null,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
export class BlaxelMachineProvider {
|
|
130
|
-
providerName = 'blaxel';
|
|
131
|
-
imageRef;
|
|
132
|
-
templateVersion;
|
|
133
|
-
volumeSizeGb;
|
|
134
|
-
config;
|
|
135
|
-
constructor(options = {}) {
|
|
136
|
-
const templateVersion = options.templateVersion ?? readCrtrVersion();
|
|
137
|
-
const volumeSizeGb = options.volumeSizeGb ?? 4;
|
|
138
|
-
if (!Number.isInteger(volumeSizeGb) || volumeSizeGb < 4) {
|
|
139
|
-
throw general('Blaxel Hearth home volume must be at least 4 GB', { volumeSizeGb });
|
|
140
|
-
}
|
|
141
|
-
this.imageRef = options.image ?? defaultImageRef(templateVersion);
|
|
142
|
-
this.templateVersion = templateVersion;
|
|
143
|
-
this.volumeSizeGb = volumeSizeGb;
|
|
144
|
-
this.config = {
|
|
145
|
-
region: options.region ?? 'us-west-2',
|
|
146
|
-
memory: options.memory ?? 2048,
|
|
147
|
-
image: this.imageRef,
|
|
148
|
-
templateVersion: this.templateVersion,
|
|
149
|
-
volumeSizeGb: this.volumeSizeGb,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
async createVolume(input) {
|
|
153
|
-
const volume = await VolumeInstance.createIfNotExists({ name: input.name, size: this.config.volumeSizeGb * 1024, region: this.config.region });
|
|
154
|
-
return {
|
|
155
|
-
volumeId: volume.metadata.name ?? input.name,
|
|
156
|
-
name: input.name,
|
|
157
|
-
raw: rawFromVolume(volume),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
async createMachine(input) {
|
|
161
|
-
const sandbox = await SandboxInstance.createIfNotExists({
|
|
162
|
-
name: input.name ?? input.tenantId,
|
|
163
|
-
// Tag every sandbox with the home's stable volume id so the recreate sweep can scope its
|
|
164
|
-
// destructive list to this home's family only (orphan-cleanup-fix MAJOR-1). Unset on
|
|
165
|
-
// callers that pass no externalId.
|
|
166
|
-
...(input.externalId === undefined ? {} : { externalId: input.externalId }),
|
|
167
|
-
// A targeted recreate (auto-upgrade roll / rollback, design §4.1) passes
|
|
168
|
-
// an explicit `image`; the normal provision/wake path leaves it unset
|
|
169
|
-
// and uses the provider's construction-time config image. `createIfNotExists`
|
|
170
|
-
// only honors this on genuine creation — the roll path destroys the old
|
|
171
|
-
// sandbox first so the name is free (blaxel-home.ts `recreateOnImage`).
|
|
172
|
-
image: input.image ?? this.config.image,
|
|
173
|
-
memory: this.config.memory,
|
|
174
|
-
region: this.config.region,
|
|
175
|
-
volumes: input.volumeMount === undefined || input.volumeMount === null ? undefined : [{ name: input.volumeMount.volumeName, mountPath: input.volumeMount.path, readOnly: false }],
|
|
176
|
-
envs: toEnvVars(input.envs),
|
|
177
|
-
ports: input.ports?.map((port) => ({ target: port.target, protocol: port.protocol })),
|
|
178
|
-
// Snapshot-never-kill (P3.4): this is the only discoverable Blaxel suspend/standby
|
|
179
|
-
// primitive (@blaxel/core 0.2.94) — there is no imperative pause()/standby() call, only
|
|
180
|
-
// this create-time runtime flag. Once set, Blaxel's platform idle timer snapshots and
|
|
181
|
-
// transparently restores the sandbox on the next request instead of tearing it down.
|
|
182
|
-
// Every Hearth home is created with this on; it is never optional per-home.
|
|
183
|
-
snapshotEnabled: true,
|
|
184
|
-
});
|
|
185
|
-
await sandbox.wait({ maxWait: 90_000, interval: 500 }).catch(() => sandbox);
|
|
186
|
-
return toMachineRef(sandbox);
|
|
187
|
-
}
|
|
188
|
-
// orphan-cleanup-fix.md §3.2 + MAJOR-1/MINOR-1: the deterministic name-family sweep enumerates
|
|
189
|
-
// a home's generations via LIST scoped to the caller-owned `externalId` tag, then destroys the
|
|
190
|
-
// ones whose name is in the family. Over-listing is harmless — a FAILED/detached record still
|
|
191
|
-
// shows up here and destroy tolerates not_found, which is exactly what closes the autonomous-
|
|
192
|
-
// standby-detach orphan gap. Drives the generated `listSandboxes` client directly (bypassing
|
|
193
|
-
// `SandboxInstance.list()`, which never follows a cursor and warms an H2 session per sandbox):
|
|
194
|
-
// loops until exhausted so a future SDK version crossing Blaxel-Version 2026-04-28 (cursor
|
|
195
|
-
// pagination, 50/page) cannot silently truncate the sweep to page 1. At the SDK's pinned
|
|
196
|
-
// 2026-04-16 the server returns a bare array (the full set) and the loop breaks on the first
|
|
197
|
-
// page.
|
|
198
|
-
async listMachines(filter) {
|
|
199
|
-
const machines = [];
|
|
200
|
-
let cursor;
|
|
201
|
-
for (;;) {
|
|
202
|
-
const { data: raw } = await listSandboxes({
|
|
203
|
-
query: {
|
|
204
|
-
limit: 200,
|
|
205
|
-
...(cursor === undefined ? {} : { cursor }),
|
|
206
|
-
...(filter?.externalId === undefined ? {} : { externalId: filter.externalId }),
|
|
207
|
-
},
|
|
208
|
-
throwOnError: true,
|
|
209
|
-
});
|
|
210
|
-
const page = Array.isArray(raw) ? raw : (raw?.data ?? []);
|
|
211
|
-
machines.push(...page.map(toMachineRef));
|
|
212
|
-
if (Array.isArray(raw))
|
|
213
|
-
break; // legacy bare array (Blaxel-Version < 2026-04-28) = full set
|
|
214
|
-
cursor = raw?.meta?.nextCursor || undefined; // empty when there are no more pages
|
|
215
|
-
if (cursor === undefined)
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
return machines;
|
|
219
|
-
}
|
|
220
|
-
async getMachine(machineId) {
|
|
221
|
-
try {
|
|
222
|
-
const sandbox = await SandboxInstance.get(machineId);
|
|
223
|
-
return toMachineRef(sandbox);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
if (!isMissingResourceError(error))
|
|
227
|
-
throw error;
|
|
228
|
-
return {
|
|
229
|
-
providerName: 'blaxel',
|
|
230
|
-
machineId,
|
|
231
|
-
state: 'terminated',
|
|
232
|
-
raw: { error: error instanceof Error ? error.message : String(error) },
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
async exec(machineId, input) {
|
|
237
|
-
if (input.stdin !== undefined && input.background === true) {
|
|
238
|
-
throw general('provider exec stdin is not supported for background commands', { machineId });
|
|
239
|
-
}
|
|
240
|
-
const sandbox = await SandboxInstance.get(machineId);
|
|
241
|
-
const stdinBody = input.stdin;
|
|
242
|
-
const stdinPath = stdinBody === undefined ? null : `/tmp/crtr-hearth-exec-${randomUUID()}.stdin`;
|
|
243
|
-
if (stdinPath !== null && stdinBody !== undefined) {
|
|
244
|
-
if (stdinBody instanceof Uint8Array) {
|
|
245
|
-
await sandbox.fs.writeBinary(stdinPath, stdinBody);
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
await sandbox.fs.write(stdinPath, stdinBody);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
const command = stdinPath === null ? resolveExecCommand(input) : `${resolveExecCommand(input)} < ${shellQuote(stdinPath)}`;
|
|
252
|
-
let response;
|
|
253
|
-
try {
|
|
254
|
-
response = await sandbox.process.exec({
|
|
255
|
-
command,
|
|
256
|
-
env: input.env,
|
|
257
|
-
timeout: Math.ceil((input.timeoutMs ?? 55_000) / 1000),
|
|
258
|
-
workingDir: input.cwd,
|
|
259
|
-
...(input.background === true ? { waitForCompletion: false } : { waitForCompletion: true }),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
if (stdinPath !== null)
|
|
264
|
-
await sandbox.fs.rm(stdinPath).catch(() => undefined);
|
|
265
|
-
}
|
|
266
|
-
const result = record(response) ?? {};
|
|
267
|
-
return {
|
|
268
|
-
exitCode: typeof result.exitCode === 'number' ? result.exitCode : null,
|
|
269
|
-
timedOut: false,
|
|
270
|
-
stdout: typeof result.stdout === 'string' ? result.stdout : typeof result.logs === 'string' ? result.logs : '',
|
|
271
|
-
stderr: typeof result.stderr === 'string' ? result.stderr : '',
|
|
272
|
-
raw: result,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
async waitForPort(machineId, port, opts) {
|
|
276
|
-
const sandbox = await SandboxInstance.get(machineId);
|
|
277
|
-
try {
|
|
278
|
-
await sandbox.process.exec({
|
|
279
|
-
command: 'true',
|
|
280
|
-
waitForPorts: [port],
|
|
281
|
-
waitForCompletion: true,
|
|
282
|
-
timeout: Math.ceil((opts?.maxWaitMs ?? 60_000) / 1000),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
throw general(`blaxel sandbox ${machineId} never opened port ${port}`, {
|
|
287
|
-
machineId,
|
|
288
|
-
port,
|
|
289
|
-
cause: error instanceof Error ? error.message : String(error),
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
async publishHttpPort(machineId, input) {
|
|
294
|
-
const sandbox = await SandboxInstance.get(machineId);
|
|
295
|
-
const previewName = input.routeName ?? input.routeId ?? `${machineId}-http`;
|
|
296
|
-
const preview = await sandbox.previews.createIfNotExists({
|
|
297
|
-
metadata: { name: previewName },
|
|
298
|
-
spec: { port: input.targetPort, public: true },
|
|
299
|
-
});
|
|
300
|
-
return toRouteRef(machineId, input.targetPort, { name: preview.name, spec: preview.spec }, {
|
|
301
|
-
preview: { metadata: preview.metadata, spec: preview.spec },
|
|
302
|
-
input,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
async health() {
|
|
306
|
-
try {
|
|
307
|
-
await VolumeInstance.list();
|
|
308
|
-
return { ok: true, providerName: 'blaxel' };
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
return {
|
|
312
|
-
ok: false,
|
|
313
|
-
providerName: 'blaxel',
|
|
314
|
-
status: error instanceof Error ? error.message : String(error),
|
|
315
|
-
raw: { error: error instanceof Error ? error.message : String(error) },
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
async destroy(machineId) {
|
|
320
|
-
try {
|
|
321
|
-
await SandboxInstance.delete(machineId);
|
|
322
|
-
return { kind: 'destroyed' };
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
if (isMissingResourceError(error))
|
|
326
|
-
return { kind: 'not_found' };
|
|
327
|
-
throw error;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
async destroyVolume(volumeId) {
|
|
331
|
-
try {
|
|
332
|
-
await VolumeInstance.delete(volumeId);
|
|
333
|
-
return { kind: 'destroyed' };
|
|
334
|
-
}
|
|
335
|
-
catch (error) {
|
|
336
|
-
if (isMissingResourceError(error))
|
|
337
|
-
return { kind: 'not_found' };
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
// recreate-race-fix.md §3.1: read the platform's own positive attachment signal
|
|
342
|
-
// (`Volume.state.attachedTo`) rather than inferring detach from a sandbox GET/delete —
|
|
343
|
-
// that read/create consistency split is exactly what raced the old same-name recreate.
|
|
344
|
-
// `VolumeInstance.get`'s backing `volume` field is TS-private (no public state getter
|
|
345
|
-
// in @blaxel/core 0.2.94), so this calls the underlying `getVolume` SDK function
|
|
346
|
-
// directly instead — same request `VolumeInstance.get` makes, with `state` exposed.
|
|
347
|
-
// A 404 (or any other failure) throws, wrapped with the volumeId for context: a missing
|
|
348
|
-
// home volume is a real incident, never something to continue past (volume-is-sacred).
|
|
349
|
-
async getVolumeAttachment(volumeId) {
|
|
350
|
-
let attachedTo;
|
|
351
|
-
try {
|
|
352
|
-
const { data } = await getVolume({ path: { volumeName: volumeId }, throwOnError: true });
|
|
353
|
-
attachedTo = data.state?.attachedTo;
|
|
354
|
-
}
|
|
355
|
-
catch (error) {
|
|
356
|
-
throw general(`blaxel volume ${volumeId} attachment check failed`, {
|
|
357
|
-
volumeId,
|
|
358
|
-
cause: error instanceof Error ? error.message : String(error),
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
const trimmed = (attachedTo ?? '').trim();
|
|
362
|
-
return trimmed === '' ? null : trimmed;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { MachineId, MachineRef, MachineState, M0Config, RouteRef } from '../types.js';
|
|
2
|
-
export interface ExecResult {
|
|
3
|
-
exitCode: number | null;
|
|
4
|
-
timedOut: boolean;
|
|
5
|
-
stdout: string;
|
|
6
|
-
stderr: string;
|
|
7
|
-
raw?: Record<string, unknown> | null;
|
|
8
|
-
}
|
|
9
|
-
export interface ExecInput {
|
|
10
|
-
command?: string;
|
|
11
|
-
argv?: readonly string[];
|
|
12
|
-
stdin?: string | Uint8Array;
|
|
13
|
-
timeoutMs?: number;
|
|
14
|
-
background?: boolean;
|
|
15
|
-
env?: Record<string, string>;
|
|
16
|
-
cwd?: string;
|
|
17
|
-
}
|
|
18
|
-
export interface VolumeRef {
|
|
19
|
-
volumeId: string;
|
|
20
|
-
name: string;
|
|
21
|
-
raw?: Record<string, unknown> | null;
|
|
22
|
-
}
|
|
23
|
-
export interface CreateMachineInput {
|
|
24
|
-
tenantId: string;
|
|
25
|
-
externalId?: string;
|
|
26
|
-
template?: string | null;
|
|
27
|
-
envs?: Record<string, string>;
|
|
28
|
-
enableNetwork?: boolean;
|
|
29
|
-
name?: string | null;
|
|
30
|
-
image?: string | null;
|
|
31
|
-
ports?: {
|
|
32
|
-
target: number;
|
|
33
|
-
protocol: 'HTTP';
|
|
34
|
-
}[];
|
|
35
|
-
volumeMount?: {
|
|
36
|
-
volumeName: string;
|
|
37
|
-
path: string;
|
|
38
|
-
} | null;
|
|
39
|
-
raw?: Record<string, unknown> | null;
|
|
40
|
-
}
|
|
41
|
-
export interface PublishHttpPortInput {
|
|
42
|
-
machineId: MachineId;
|
|
43
|
-
targetPort: number;
|
|
44
|
-
subdomain?: string | null;
|
|
45
|
-
routeId?: string | null;
|
|
46
|
-
routeName?: string | null;
|
|
47
|
-
raw?: Record<string, unknown> | null;
|
|
48
|
-
}
|
|
49
|
-
export interface HealthResult {
|
|
50
|
-
ok: boolean;
|
|
51
|
-
providerName: string;
|
|
52
|
-
status?: string | null;
|
|
53
|
-
raw?: Record<string, unknown> | null;
|
|
54
|
-
}
|
|
55
|
-
export type DestroyOutcome = {
|
|
56
|
-
kind: 'destroyed';
|
|
57
|
-
} | {
|
|
58
|
-
kind: 'not_found';
|
|
59
|
-
};
|
|
60
|
-
export interface MachineProvider {
|
|
61
|
-
readonly providerName: 'blaxel';
|
|
62
|
-
readonly imageRef: string;
|
|
63
|
-
readonly templateVersion: string;
|
|
64
|
-
readonly volumeSizeGb: number;
|
|
65
|
-
createVolume(input: {
|
|
66
|
-
name: string;
|
|
67
|
-
}): Promise<VolumeRef>;
|
|
68
|
-
createMachine(input: CreateMachineInput): Promise<MachineRef>;
|
|
69
|
-
getMachine(machineId: MachineId): Promise<MachineRef>;
|
|
70
|
-
/** List machine records, optionally scoped to a caller-owned `externalId` tag. The recreate
|
|
71
|
-
* cleanup sweep passes the home's `providerVolumeId` as the filter so only sandboxes Hearth
|
|
72
|
-
* itself tagged for this home come back, then intersects that with the generation NAME FAMILY
|
|
73
|
-
* matcher — a tag∩name intersection on a destructive op, so no id-format or workspace name
|
|
74
|
-
* discipline is load-bearing for other homes' safety (orphan-cleanup-fix MAJOR-1). The result
|
|
75
|
-
* is NOT a promise of every machine in the workspace; without a filter it lists broadly, with
|
|
76
|
-
* one it lists only tagged matches. Implementations must page until exhausted (MINOR-1). */
|
|
77
|
-
listMachines(filter?: {
|
|
78
|
-
externalId?: string;
|
|
79
|
-
}): Promise<MachineRef[]>;
|
|
80
|
-
exec(machineId: MachineId, input: ExecInput): Promise<ExecResult>;
|
|
81
|
-
waitForPort(machineId: MachineId, port: number, opts?: {
|
|
82
|
-
maxWaitMs?: number;
|
|
83
|
-
}): Promise<void>;
|
|
84
|
-
publishHttpPort(machineId: MachineId, input: PublishHttpPortInput): Promise<RouteRef>;
|
|
85
|
-
health(): Promise<HealthResult>;
|
|
86
|
-
destroy(machineId: MachineId): Promise<DestroyOutcome>;
|
|
87
|
-
destroyVolume(volumeId: string): Promise<DestroyOutcome>;
|
|
88
|
-
/** The resource currently holding this volume ("sandbox:<name>"), or null if detached.
|
|
89
|
-
* A missing volume (404) throws — a home volume disappearing is always an incident,
|
|
90
|
-
* never something to continue past (recreate-race-fix.md §3.1). */
|
|
91
|
-
getVolumeAttachment(volumeId: string): Promise<string | null>;
|
|
92
|
-
}
|
|
93
|
-
export type { MachineState, M0Config, RouteRef };
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
export type TenantId = string;
|
|
2
|
-
export type MachineId = string;
|
|
3
|
-
export type SnapshotId = string;
|
|
4
|
-
export type RouteId = string;
|
|
5
|
-
export type RelayToken = string;
|
|
6
|
-
export type GuestNodeId = string;
|
|
7
|
-
export type HomeId = string;
|
|
8
|
-
export type UserId = string;
|
|
9
|
-
export type RelayAuthRef = string;
|
|
10
|
-
export type MachineState = 'provisioning' | 'running' | 'paused' | 'stopped' | 'suspended' | 'restoring' | 'terminating' | 'terminated' | 'unknown' | string;
|
|
11
|
-
export type RouteUrlSource = 'provider' | 'config' | 'derived' | 'unknown';
|
|
12
|
-
export interface HomeStorageRef {
|
|
13
|
-
kind: 'volume';
|
|
14
|
-
durability: 'durable-volume';
|
|
15
|
-
longTermContract: true;
|
|
16
|
-
ownerMachineId: MachineId;
|
|
17
|
-
guestHome: string;
|
|
18
|
-
volumeId: string;
|
|
19
|
-
mountPath: string;
|
|
20
|
-
}
|
|
21
|
-
export interface MachineRef {
|
|
22
|
-
providerName: string;
|
|
23
|
-
machineId: MachineId;
|
|
24
|
-
state: MachineState;
|
|
25
|
-
guestIp?: string | null;
|
|
26
|
-
sshPort?: number | null;
|
|
27
|
-
updatedAt?: string | null;
|
|
28
|
-
raw?: Record<string, unknown> | null;
|
|
29
|
-
}
|
|
30
|
-
export interface SnapshotRef {
|
|
31
|
-
snapshotId: SnapshotId;
|
|
32
|
-
machineId: MachineId;
|
|
33
|
-
stateKey?: string | null;
|
|
34
|
-
memoryKey?: string | null;
|
|
35
|
-
diskKey?: string | null;
|
|
36
|
-
sizeBytes?: number | null;
|
|
37
|
-
createdAt?: string | null;
|
|
38
|
-
raw?: Record<string, unknown> | null;
|
|
39
|
-
}
|
|
40
|
-
export interface RouteRef {
|
|
41
|
-
routeId: RouteId;
|
|
42
|
-
machineId: MachineId;
|
|
43
|
-
targetPort: number;
|
|
44
|
-
url?: string | null;
|
|
45
|
-
subdomain?: string | null;
|
|
46
|
-
createdAt?: string | null;
|
|
47
|
-
concreteUrl?: string | null;
|
|
48
|
-
concreteUrlSource?: RouteUrlSource | null;
|
|
49
|
-
raw?: Record<string, unknown> | null;
|
|
50
|
-
}
|
|
51
|
-
export interface BootstrapMarker {
|
|
52
|
-
tenantId: TenantId;
|
|
53
|
-
nodeId: GuestNodeId;
|
|
54
|
-
daemonPid: number;
|
|
55
|
-
brokerPid: number;
|
|
56
|
-
crtrHome: string;
|
|
57
|
-
piAgentDir: string;
|
|
58
|
-
sessionFile?: string | null;
|
|
59
|
-
webPort: number;
|
|
60
|
-
createdAt: string;
|
|
61
|
-
}
|
|
62
|
-
export interface VerifyBaseline {
|
|
63
|
-
machineId: MachineId;
|
|
64
|
-
nodeId: GuestNodeId;
|
|
65
|
-
sessionFile: string;
|
|
66
|
-
sessionLineCount: number;
|
|
67
|
-
webPort: number;
|
|
68
|
-
capturedAt: string;
|
|
69
|
-
daemonPid?: number | null;
|
|
70
|
-
brokerPid?: number | null;
|
|
71
|
-
socketPath?: string | null;
|
|
72
|
-
}
|
|
73
|
-
export type VerifyPhase = 'pre-suspend' | 'post-restore' | 'post-recreate';
|
|
74
|
-
export interface VerifyCheck {
|
|
75
|
-
name: string;
|
|
76
|
-
status: 'PASS' | 'FAIL';
|
|
77
|
-
detail?: string;
|
|
78
|
-
value?: string | number | boolean | null;
|
|
79
|
-
}
|
|
80
|
-
export interface VerifyResult {
|
|
81
|
-
phase: VerifyPhase;
|
|
82
|
-
checks: VerifyCheck[];
|
|
83
|
-
passed: boolean;
|
|
84
|
-
machineState: MachineState;
|
|
85
|
-
nodeId?: GuestNodeId | null;
|
|
86
|
-
routeUrl?: string | null;
|
|
87
|
-
hearth_m0_verified?: boolean;
|
|
88
|
-
hearth_m0_recreate_verified?: boolean;
|
|
89
|
-
warmResume?: boolean | null;
|
|
90
|
-
restoreLatencyMs?: number | null;
|
|
91
|
-
recreateLatencyMs?: number | null;
|
|
92
|
-
baseline?: VerifyBaseline | null;
|
|
93
|
-
}
|
|
94
|
-
export interface HomeProviderDescriptor {
|
|
95
|
-
homeId: HomeId;
|
|
96
|
-
tenantId: TenantId;
|
|
97
|
-
userId?: UserId | null;
|
|
98
|
-
providerName: 'blaxel';
|
|
99
|
-
providerSandboxId: MachineId;
|
|
100
|
-
providerVolumeId: string;
|
|
101
|
-
previewEndpoint?: string | null;
|
|
102
|
-
homeAgentTarget: GuestNodeId;
|
|
103
|
-
relayAuthRef?: RelayAuthRef | null;
|
|
104
|
-
relayToken: RelayToken;
|
|
105
|
-
status?: MachineState | null;
|
|
106
|
-
templateVersion?: string | null;
|
|
107
|
-
raw?: Record<string, unknown> | null;
|
|
108
|
-
}
|
|
109
|
-
export interface HomeProviderRefresh {
|
|
110
|
-
providerName: 'blaxel';
|
|
111
|
-
providerSandboxId: MachineId;
|
|
112
|
-
providerVolumeId: string;
|
|
113
|
-
previewEndpoint?: string | null;
|
|
114
|
-
route?: RouteRef | null;
|
|
115
|
-
homeAgentTarget: GuestNodeId;
|
|
116
|
-
relayAuthRef?: RelayAuthRef | null;
|
|
117
|
-
templateVersion: string;
|
|
118
|
-
status: MachineState;
|
|
119
|
-
machine?: MachineRef | null;
|
|
120
|
-
updatedAt: string;
|
|
121
|
-
}
|
|
122
|
-
export interface HomeRecord {
|
|
123
|
-
tenantId: TenantId;
|
|
124
|
-
providerName: string;
|
|
125
|
-
machineId: MachineId;
|
|
126
|
-
homeStorage: HomeStorageRef;
|
|
127
|
-
state: MachineState;
|
|
128
|
-
nodeId?: GuestNodeId | null;
|
|
129
|
-
webRoute?: RouteRef | null;
|
|
130
|
-
latestSnapshot?: SnapshotRef | null;
|
|
131
|
-
verifyBaseline?: VerifyBaseline | null;
|
|
132
|
-
bootstrapMarker?: BootstrapMarker | null;
|
|
133
|
-
relayToken?: RelayToken | null;
|
|
134
|
-
templateVersion?: string | null;
|
|
135
|
-
warmResume?: boolean | null;
|
|
136
|
-
restoreLatencyMs?: number | null;
|
|
137
|
-
recreateLatencyMs?: number | null;
|
|
138
|
-
createdAt: string;
|
|
139
|
-
updatedAt: string;
|
|
140
|
-
raw?: Record<string, unknown> | null;
|
|
141
|
-
}
|
|
142
|
-
export interface M0Config {
|
|
143
|
-
providerName: string;
|
|
144
|
-
guestUser: string;
|
|
145
|
-
guestHome: string;
|
|
146
|
-
guestCodePath: string;
|
|
147
|
-
piPackages: string[];
|
|
148
|
-
webPort: number;
|
|
149
|
-
nodeKind: string;
|
|
150
|
-
piAuthJson?: string | null;
|
|
151
|
-
relayToken?: RelayToken | null;
|
|
152
|
-
imageRef?: string;
|
|
153
|
-
templateVersion?: string;
|
|
154
|
-
volumeSizeGb?: number;
|
|
155
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
// Bug-regression test (independent review of commit 6018050, recreate-race-fix.md §3.6):
|
|
4
|
-
// `JSON.stringify` returns `undefined` (does not throw) for `undefined`, a function, or a
|
|
5
|
-
// symbol — a naive `JSON.stringify(error)` rider would silently hand that `undefined` back to
|
|
6
|
-
// callers. In `relay.ts` that `undefined` message then makes `message.startsWith('relay ')`
|
|
7
|
-
// THROW inside the very catch block meant to handle it (a control-flow regression §3.6
|
|
8
|
-
// promised not to introduce); in `wake.ts` it would OMIT the `error` field from the
|
|
9
|
-
// `hearth.roll.incident` JSON line entirely. Both serializers must fall back to `String`
|
|
10
|
-
// whenever `JSON.stringify` yields `undefined`, not only when it throws.
|
|
11
|
-
test('serializeError (wake.ts) never returns undefined for undefined/function/symbol rejections', async () => {
|
|
12
|
-
const { serializeError } = await import('../wake.js');
|
|
13
|
-
for (const value of [undefined, function unnamed() { }, Symbol('boom')]) {
|
|
14
|
-
const result = serializeError(value);
|
|
15
|
-
assert.equal(typeof result, 'string', `serializeError(${String(value)}) must return a string, never JS \`undefined\``);
|
|
16
|
-
assert.ok(result.length > 0, `serializeError(${String(value)}) must be a non-empty string`);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
test('serializeRelayError (relay.ts) never returns undefined for undefined/function/symbol rejections, and the relay control-flow checks it feeds never throw', async () => {
|
|
20
|
-
const { serializeRelayError } = await import('../relay.js');
|
|
21
|
-
for (const value of [undefined, function unnamed() { }, Symbol('boom')]) {
|
|
22
|
-
const result = serializeRelayError(value);
|
|
23
|
-
assert.equal(typeof result, 'string');
|
|
24
|
-
assert.ok(result.length > 0, `serializeRelayError(${String(value)}) must be a non-empty string`);
|
|
25
|
-
// The exact control flow `proxyHttp`'s catch runs on this value — must not throw.
|
|
26
|
-
assert.doesNotThrow(() => result.startsWith('relay '));
|
|
27
|
-
assert.doesNotThrow(() => /timed out/i.test(result));
|
|
28
|
-
}
|
|
29
|
-
});
|