@specific.dev/spectest 0.79.0 → 0.79.1
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/harness/main.js +4 -4
- package/dist/harness/protocol.d.ts +20 -0
- package/dist/harness/protocol.js +34 -0
- package/package.json +1 -1
- package/src/harness/main.ts +4 -4
- package/src/harness/protocol.test.ts +42 -0
- package/src/harness/protocol.ts +33 -0
package/dist/harness/main.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import net from "node:net";
|
|
36
36
|
import { METHODS } from "../daemon";
|
|
37
37
|
import { codeOf } from "./methods";
|
|
38
|
-
import { PROTOCOL_VERSION, decodeFrame, encodeFrame, fail, ok, } from "./protocol";
|
|
38
|
+
import { PROTOCOL_VERSION, decodeFrame, describeThrow, encodeFrame, fail, ok, } from "./protocol";
|
|
39
39
|
/** Capabilities this build announces. Empty is fine — the slot is what
|
|
40
40
|
* matters, so a later release has something to negotiate against. */
|
|
41
41
|
const CAPABILITIES = [];
|
|
@@ -87,11 +87,11 @@ export function serve(socket, registry) {
|
|
|
87
87
|
//
|
|
88
88
|
// A refusal the method meant to make (an unknown case, a second test
|
|
89
89
|
// while one is running) reports its own kind and just its message. An
|
|
90
|
-
// unexpected throw
|
|
91
|
-
//
|
|
90
|
+
// unexpected throw carries its message AND its stack — in that order,
|
|
91
|
+
// because a stack alone is sometimes headerless (`describeThrow`).
|
|
92
92
|
const e = err;
|
|
93
93
|
const code = codeOf(err);
|
|
94
|
-
const detail = code === "handler_error" ? (
|
|
94
|
+
const detail = code === "handler_error" ? describeThrow(err) : e.message;
|
|
95
95
|
send(fail(req.id, detail, code));
|
|
96
96
|
}
|
|
97
97
|
};
|
|
@@ -86,3 +86,23 @@ export declare function ok(id: string, result?: Record<string, unknown>): Respon
|
|
|
86
86
|
/** A failed reply to `id`. Always carries the id: a request that failed must
|
|
87
87
|
* never leave its caller waiting. */
|
|
88
88
|
export declare function fail(id: string, message: string, code?: string): ResponseFrame;
|
|
89
|
+
/**
|
|
90
|
+
* Describe a thrown value for the wire: the **message first**, then the
|
|
91
|
+
* frames — never the stack on its own.
|
|
92
|
+
*
|
|
93
|
+
* `error.stack` normally opens with `Error: <message>`, which is why it
|
|
94
|
+
* looked like the richer of the two. It is not always: Bun drops that
|
|
95
|
+
* header when the rejection is observed a turn later than it settled —
|
|
96
|
+
* a promise that rejected while nobody was awaiting it yet, which is
|
|
97
|
+
* exactly what a bootstrap image build is (`daemon.ts` starts every
|
|
98
|
+
* build at once and each service awaits its own prep when the DAG
|
|
99
|
+
* reaches it). The whole diagnosis of a failed build IS the message
|
|
100
|
+
* there: `docker build for <svc> failed:` plus the build log. Sending
|
|
101
|
+
* the stack alone published four frames and nothing else — no docker,
|
|
102
|
+
* no BuildKit, no compiler error (reported by the Harmony project,
|
|
103
|
+
* 2026-09-09; the same run's log had the message intact).
|
|
104
|
+
*
|
|
105
|
+
* A stack that already carries the message is returned as it stands, so
|
|
106
|
+
* the ordinary case is unchanged.
|
|
107
|
+
*/
|
|
108
|
+
export declare function describeThrow(err: unknown): string;
|
package/dist/harness/protocol.js
CHANGED
|
@@ -94,3 +94,37 @@ export function ok(id, result = {}) {
|
|
|
94
94
|
export function fail(id, message, code) {
|
|
95
95
|
return { kind: "response", id, ok: false, error: code ? { message, code } : { message } };
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Describe a thrown value for the wire: the **message first**, then the
|
|
99
|
+
* frames — never the stack on its own.
|
|
100
|
+
*
|
|
101
|
+
* `error.stack` normally opens with `Error: <message>`, which is why it
|
|
102
|
+
* looked like the richer of the two. It is not always: Bun drops that
|
|
103
|
+
* header when the rejection is observed a turn later than it settled —
|
|
104
|
+
* a promise that rejected while nobody was awaiting it yet, which is
|
|
105
|
+
* exactly what a bootstrap image build is (`daemon.ts` starts every
|
|
106
|
+
* build at once and each service awaits its own prep when the DAG
|
|
107
|
+
* reaches it). The whole diagnosis of a failed build IS the message
|
|
108
|
+
* there: `docker build for <svc> failed:` plus the build log. Sending
|
|
109
|
+
* the stack alone published four frames and nothing else — no docker,
|
|
110
|
+
* no BuildKit, no compiler error (reported by the Harmony project,
|
|
111
|
+
* 2026-09-09; the same run's log had the message intact).
|
|
112
|
+
*
|
|
113
|
+
* A stack that already carries the message is returned as it stands, so
|
|
114
|
+
* the ordinary case is unchanged.
|
|
115
|
+
*/
|
|
116
|
+
export function describeThrow(err) {
|
|
117
|
+
const e = err;
|
|
118
|
+
const message = typeof e?.message === "string" ? e.message : "";
|
|
119
|
+
const stack = typeof e?.stack === "string" ? e.stack : "";
|
|
120
|
+
if (!message)
|
|
121
|
+
return stack || String(err);
|
|
122
|
+
if (stack.includes(message))
|
|
123
|
+
return stack;
|
|
124
|
+
const name = typeof e?.name === "string" && e.name.length > 0 ? e.name : "Error";
|
|
125
|
+
// The headerless form is `<Name>\n at …`. Drop that bare first line
|
|
126
|
+
// and write the header ourselves, so the result reads like the stack a
|
|
127
|
+
// reader expects rather than a message with a stray `Error` in it.
|
|
128
|
+
const frames = stack.startsWith(`${name}\n`) ? stack.slice(name.length + 1) : stack;
|
|
129
|
+
return frames.length > 0 ? `${name}: ${message}\n${frames}` : `${name}: ${message}`;
|
|
130
|
+
}
|
package/package.json
CHANGED
package/src/harness/main.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { codeOf } from "./methods";
|
|
|
40
40
|
import {
|
|
41
41
|
PROTOCOL_VERSION,
|
|
42
42
|
decodeFrame,
|
|
43
|
+
describeThrow,
|
|
43
44
|
encodeFrame,
|
|
44
45
|
fail,
|
|
45
46
|
ok,
|
|
@@ -115,12 +116,11 @@ export function serve(
|
|
|
115
116
|
//
|
|
116
117
|
// A refusal the method meant to make (an unknown case, a second test
|
|
117
118
|
// while one is running) reports its own kind and just its message. An
|
|
118
|
-
// unexpected throw
|
|
119
|
-
//
|
|
119
|
+
// unexpected throw carries its message AND its stack — in that order,
|
|
120
|
+
// because a stack alone is sometimes headerless (`describeThrow`).
|
|
120
121
|
const e = err as Error;
|
|
121
122
|
const code = codeOf(err);
|
|
122
|
-
const detail =
|
|
123
|
-
code === "handler_error" ? (e?.stack ?? e?.message ?? String(err)) : e.message;
|
|
123
|
+
const detail = code === "handler_error" ? describeThrow(err) : e.message;
|
|
124
124
|
send(fail(req.id, detail, code));
|
|
125
125
|
}
|
|
126
126
|
};
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
KNOWN_METHODS,
|
|
19
19
|
PROTOCOL_VERSION,
|
|
20
20
|
decodeFrame,
|
|
21
|
+
describeThrow,
|
|
21
22
|
encodeFrame,
|
|
22
23
|
fail,
|
|
23
24
|
ok,
|
|
@@ -146,3 +147,44 @@ describe("replies", () => {
|
|
|
146
147
|
expect(fail("7", "boom", "c")!.error!.code).toBe("c");
|
|
147
148
|
});
|
|
148
149
|
});
|
|
150
|
+
|
|
151
|
+
describe("describeThrow", () => {
|
|
152
|
+
test("an ordinary stack, which already opens with the message, is untouched", () => {
|
|
153
|
+
const err = new Error("docker build for web failed:\nCOPY: not found");
|
|
154
|
+
expect(describeThrow(err)).toBe(err.stack);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The shape this function exists for, produced rather than written by
|
|
159
|
+
* hand: a promise that rejects while nobody awaits it yet and is
|
|
160
|
+
* observed a turn later comes back with a headerless stack. That is a
|
|
161
|
+
* bootstrap image build (`daemon.ts` starts every build at once and a
|
|
162
|
+
* service awaits its own prep when the DAG reaches it), and the build
|
|
163
|
+
* log lives in the message it drops.
|
|
164
|
+
*/
|
|
165
|
+
test("a headerless stack gets its message back", async () => {
|
|
166
|
+
const message = "docker build for client-app failed:\n#12 ERROR: killed";
|
|
167
|
+
const rejected = (async () => {
|
|
168
|
+
throw new Error(message);
|
|
169
|
+
})();
|
|
170
|
+
rejected.catch(() => undefined);
|
|
171
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
172
|
+
let caught: unknown;
|
|
173
|
+
try {
|
|
174
|
+
await rejected;
|
|
175
|
+
} catch (e) {
|
|
176
|
+
caught = e;
|
|
177
|
+
}
|
|
178
|
+
const described = describeThrow(caught);
|
|
179
|
+
expect(described).toContain(message);
|
|
180
|
+
expect(described.startsWith(`Error: ${message}`)).toBe(true);
|
|
181
|
+
// The frames survive, and the bare `Error` line they hung under does not.
|
|
182
|
+
expect(described).toContain(" at ");
|
|
183
|
+
expect(described).not.toContain("\nError\n");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a non-Error keeps whatever it can say for itself", () => {
|
|
187
|
+
expect(describeThrow("plain string")).toBe("plain string");
|
|
188
|
+
expect(describeThrow({ message: "no stack here" })).toBe("Error: no stack here");
|
|
189
|
+
});
|
|
190
|
+
});
|
package/src/harness/protocol.ts
CHANGED
|
@@ -161,3 +161,36 @@ export function ok(id: string, result: Record<string, unknown> = {}): ResponseFr
|
|
|
161
161
|
export function fail(id: string, message: string, code?: string): ResponseFrame {
|
|
162
162
|
return { kind: "response", id, ok: false, error: code ? { message, code } : { message } };
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Describe a thrown value for the wire: the **message first**, then the
|
|
167
|
+
* frames — never the stack on its own.
|
|
168
|
+
*
|
|
169
|
+
* `error.stack` normally opens with `Error: <message>`, which is why it
|
|
170
|
+
* looked like the richer of the two. It is not always: Bun drops that
|
|
171
|
+
* header when the rejection is observed a turn later than it settled —
|
|
172
|
+
* a promise that rejected while nobody was awaiting it yet, which is
|
|
173
|
+
* exactly what a bootstrap image build is (`daemon.ts` starts every
|
|
174
|
+
* build at once and each service awaits its own prep when the DAG
|
|
175
|
+
* reaches it). The whole diagnosis of a failed build IS the message
|
|
176
|
+
* there: `docker build for <svc> failed:` plus the build log. Sending
|
|
177
|
+
* the stack alone published four frames and nothing else — no docker,
|
|
178
|
+
* no BuildKit, no compiler error (reported by the Harmony project,
|
|
179
|
+
* 2026-09-09; the same run's log had the message intact).
|
|
180
|
+
*
|
|
181
|
+
* A stack that already carries the message is returned as it stands, so
|
|
182
|
+
* the ordinary case is unchanged.
|
|
183
|
+
*/
|
|
184
|
+
export function describeThrow(err: unknown): string {
|
|
185
|
+
const e = err as { message?: unknown; stack?: unknown; name?: unknown } | null | undefined;
|
|
186
|
+
const message = typeof e?.message === "string" ? e.message : "";
|
|
187
|
+
const stack = typeof e?.stack === "string" ? e.stack : "";
|
|
188
|
+
if (!message) return stack || String(err);
|
|
189
|
+
if (stack.includes(message)) return stack;
|
|
190
|
+
const name = typeof e?.name === "string" && e.name.length > 0 ? e.name : "Error";
|
|
191
|
+
// The headerless form is `<Name>\n at …`. Drop that bare first line
|
|
192
|
+
// and write the header ourselves, so the result reads like the stack a
|
|
193
|
+
// reader expects rather than a message with a stray `Error` in it.
|
|
194
|
+
const frames = stack.startsWith(`${name}\n`) ? stack.slice(name.length + 1) : stack;
|
|
195
|
+
return frames.length > 0 ? `${name}: ${message}\n${frames}` : `${name}: ${message}`;
|
|
196
|
+
}
|