@glubean/runner 0.5.0 → 0.8.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/dist/engine-bridge.d.ts +55 -0
- package/dist/engine-bridge.d.ts.map +1 -0
- package/dist/engine-bridge.js +219 -0
- package/dist/engine-bridge.js.map +1 -0
- package/dist/executor.d.ts +70 -2
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +21 -226
- package/dist/executor.js.map +1 -1
- package/dist/generate_summary.d.ts +15 -0
- package/dist/generate_summary.d.ts.map +1 -1
- package/dist/generate_summary.js +52 -1
- package/dist/generate_summary.js.map +1 -1
- package/dist/harness.js +257 -21
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/load/continuation-pool.d.ts +82 -0
- package/dist/load/continuation-pool.d.ts.map +1 -0
- package/dist/load/continuation-pool.js +154 -0
- package/dist/load/continuation-pool.js.map +1 -0
- package/dist/load/execute-iteration.d.ts +126 -0
- package/dist/load/execute-iteration.d.ts.map +1 -0
- package/dist/load/execute-iteration.js +367 -0
- package/dist/load/execute-iteration.js.map +1 -0
- package/dist/load/histogram.d.ts +63 -0
- package/dist/load/histogram.d.ts.map +1 -0
- package/dist/load/histogram.js +149 -0
- package/dist/load/histogram.js.map +1 -0
- package/dist/load/orchestrator.d.ts +55 -0
- package/dist/load/orchestrator.d.ts.map +1 -0
- package/dist/load/orchestrator.js +571 -0
- package/dist/load/orchestrator.js.map +1 -0
- package/dist/load/reducer.d.ts +109 -0
- package/dist/load/reducer.d.ts.map +1 -0
- package/dist/load/reducer.js +718 -0
- package/dist/load/reducer.js.map +1 -0
- package/dist/load/route-key.d.ts +38 -0
- package/dist/load/route-key.d.ts.map +1 -0
- package/dist/load/route-key.js +107 -0
- package/dist/load/route-key.js.map +1 -0
- package/dist/load/samples.d.ts +83 -0
- package/dist/load/samples.d.ts.map +1 -0
- package/dist/load/samples.js +269 -0
- package/dist/load/samples.js.map +1 -0
- package/dist/load/sink.d.ts +127 -0
- package/dist/load/sink.d.ts.map +1 -0
- package/dist/load/sink.js +351 -0
- package/dist/load/sink.js.map +1 -0
- package/dist/load/subprocess.d.ts +83 -0
- package/dist/load/subprocess.d.ts.map +1 -0
- package/dist/load/subprocess.js +229 -0
- package/dist/load/subprocess.js.map +1 -0
- package/dist/load/threshold.d.ts +44 -0
- package/dist/load/threshold.d.ts.map +1 -0
- package/dist/load/threshold.js +197 -0
- package/dist/load/threshold.js.map +1 -0
- package/dist/load/timeline.d.ts +36 -0
- package/dist/load/timeline.d.ts.map +1 -0
- package/dist/load/timeline.js +158 -0
- package/dist/load/timeline.js.map +1 -0
- package/dist/load-harness.d.ts +2 -0
- package/dist/load-harness.d.ts.map +1 -0
- package/dist/load-harness.js +105 -0
- package/dist/load-harness.js.map +1 -0
- package/dist/resolve.d.ts +10 -11
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +28 -9
- package/dist/resolve.js.map +1 -1
- package/dist/runner-resolve.d.ts +53 -0
- package/dist/runner-resolve.d.ts.map +1 -0
- package/dist/runner-resolve.js +264 -0
- package/dist/runner-resolve.js.map +1 -0
- package/dist/workflow/event-timeline.d.ts +3 -0
- package/dist/workflow/event-timeline.d.ts.map +1 -0
- package/dist/workflow/event-timeline.js +72 -0
- package/dist/workflow/event-timeline.js.map +1 -0
- package/dist/workflow/execute.d.ts +267 -0
- package/dist/workflow/execute.d.ts.map +1 -0
- package/dist/workflow/execute.js +1475 -0
- package/dist/workflow/execute.js.map +1 -0
- package/package.json +8 -4
package/dist/harness.js
CHANGED
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
10
|
import { inferJsonSchema, truncateDeep } from "./schema_inference.js";
|
|
11
|
+
import { USE_ENGINE, createEngineCore, runViaEngine, engineRoutesId } from "./engine-bridge.js";
|
|
11
12
|
import { bootstrap } from "./bootstrap.js";
|
|
12
13
|
import { loadProjectOverlays } from "@glubean/scanner";
|
|
13
14
|
import { setRuntime, setExplicitInput, setBootstrapInput, setForceStandalone, } from "@glubean/sdk/internal";
|
|
15
|
+
// Workflow executor — now node-only and owned by this package (plan 0007). The host
|
|
16
|
+
// drives a built workflow via runWorkflow (invocation inversion) and attributes inline
|
|
17
|
+
// ctx.http traces to the active node scope via __activeWorkflowNodeCtx (the ALS rebind).
|
|
18
|
+
import { runWorkflow, WorkflowPhaseFailedError, __activeWorkflowNodeCtx, } from "./workflow/execute.js";
|
|
19
|
+
// workflow per-node evidence → first-class timeline events; co-located with the
|
|
20
|
+
// executor that emits those events (plan 0007), not in this generic harness.
|
|
21
|
+
import { workflowEventToTimeline } from "./workflow/event-timeline.js";
|
|
14
22
|
import ky from "ky";
|
|
15
23
|
import { isTestBranchStep, isTestPollStep } from "@glubean/sdk";
|
|
16
24
|
import { Expectation } from "@glubean/sdk/expect";
|
|
@@ -189,14 +197,57 @@ function incrAssertions(passed) {
|
|
|
189
197
|
* Use this for all test-scoped event output to ensure concurrent events can be
|
|
190
198
|
* attributed to the correct test.
|
|
191
199
|
*/
|
|
200
|
+
/** Events that STEER THE PARENT while the test is still running — the
|
|
201
|
+
* executor re-arms the subprocess timeout on `timeout_update`, and
|
|
202
|
+
* ProjectRunner forwards `session:set` to sibling files. Holding them in the
|
|
203
|
+
* parallel buffer until the test finishes would defeat them (a long test
|
|
204
|
+
* would be killed at the OLD deadline — codex S2.12 R24 P2), so they bypass
|
|
205
|
+
* buffering. Out-of-order arrival is fine: both are keyed/merged by the
|
|
206
|
+
* parent, not attributed to a contiguous test block. */
|
|
207
|
+
const CONTROL_EVENT_TYPES = new Set(["timeout_update", "session:set"]);
|
|
192
208
|
function emitEvent(event) {
|
|
193
209
|
const trc = currentTestCtx();
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
const json = JSON.stringify(trc ? { ...event, testId: trc.testId } : event);
|
|
211
|
+
if (CONTROL_EVENT_TYPES.has(event.type)) {
|
|
212
|
+
console.log(json);
|
|
213
|
+
return;
|
|
196
214
|
}
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
writeEventLine(json);
|
|
216
|
+
}
|
|
217
|
+
// ── Parallel-batch event buffering (codex S2.12 R22 P1) ─────────────────────
|
|
218
|
+
// Under batchConcurrency > 1, tests run concurrently and their stdout events
|
|
219
|
+
// would interleave — downstream collectors (CLI render, result JSON) keep
|
|
220
|
+
// per-test state that assumes each test's start..status block is CONTIGUOUS.
|
|
221
|
+
// Each parallel task runs inside this ALS with its own line buffer; the task
|
|
222
|
+
// flushes the whole buffer atomically (sync loop — no awaits) when it
|
|
223
|
+
// finishes. Sequential runs never enter the ALS: zero behavior change.
|
|
224
|
+
const parallelEventBuffer = new AsyncLocalStorage();
|
|
225
|
+
function writeEventLine(json) {
|
|
226
|
+
const buf = parallelEventBuffer.getStore();
|
|
227
|
+
if (buf)
|
|
228
|
+
buf.push(json);
|
|
229
|
+
else
|
|
230
|
+
console.log(json);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Write a wire event produced by the engine path (runner-on-engine, plan 0005).
|
|
234
|
+
* Mirrors emitEvent's control-event bypass, but the event already carries its
|
|
235
|
+
* testId (the engine path runs outside the testContext ALS, so testId is sourced
|
|
236
|
+
* from the engine event's own id, not currentTestCtx()).
|
|
237
|
+
*/
|
|
238
|
+
function emitEngineWire(ev) {
|
|
239
|
+
// Mirror the legacy ctx.session.set: update the subprocess-local sessionData so
|
|
240
|
+
// sibling tests in this process see it (batch mode), in addition to forwarding
|
|
241
|
+
// the control event to the parent (codex P2).
|
|
242
|
+
if (ev.type === "session:set") {
|
|
243
|
+
sessionData[ev.key] = ev.value;
|
|
244
|
+
}
|
|
245
|
+
const json = JSON.stringify(ev);
|
|
246
|
+
if (CONTROL_EVENT_TYPES.has(ev.type)) {
|
|
247
|
+
console.log(json);
|
|
248
|
+
return;
|
|
199
249
|
}
|
|
250
|
+
writeEventLine(json);
|
|
200
251
|
}
|
|
201
252
|
/**
|
|
202
253
|
* Start monitoring memory usage.
|
|
@@ -552,8 +603,18 @@ const ctx = {
|
|
|
552
603
|
...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
|
|
553
604
|
});
|
|
554
605
|
},
|
|
555
|
-
// Structured event emission
|
|
606
|
+
// Structured event emission. The vNext workflow executor's per-node
|
|
607
|
+
// evidence (§17 #9) rides this channel as namespaced GlubeanEvents — the
|
|
608
|
+
// harness UNWRAPS the three known types into first-class timeline events
|
|
609
|
+
// (node id + grade reach generateSummary / the Cloud payload directly,
|
|
610
|
+
// not as a double-wrapped custom blob). Any other type — including other
|
|
611
|
+
// `workflow:*` names — stays a generic pass-through event.
|
|
556
612
|
event: (ev) => {
|
|
613
|
+
const firstClass = workflowEventToTimeline(ev);
|
|
614
|
+
if (firstClass) {
|
|
615
|
+
emitEvent(firstClass);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
557
618
|
emitEvent({
|
|
558
619
|
type: "event",
|
|
559
620
|
data: ev,
|
|
@@ -657,6 +718,24 @@ const ctx = {
|
|
|
657
718
|
},
|
|
658
719
|
};
|
|
659
720
|
const requestTraceMap = new WeakMap();
|
|
721
|
+
// Capture the outgoing request body for full-trace mode. ky 2 no longer exposes
|
|
722
|
+
// `options.json`, so read it off a clone of the Request (parsed if JSON).
|
|
723
|
+
async function captureRequestBody(request) {
|
|
724
|
+
try {
|
|
725
|
+
const text = await request.clone().text();
|
|
726
|
+
if (!text)
|
|
727
|
+
return undefined;
|
|
728
|
+
try {
|
|
729
|
+
return JSON.parse(text);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
return text;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
660
739
|
/** Max serialized body size (chars) to include in trace events. */
|
|
661
740
|
const TRACE_BODY_MAX_SIZE = 1_048_576; // 1MB
|
|
662
741
|
/**
|
|
@@ -722,18 +801,18 @@ const kyInstance = ky.create({
|
|
|
722
801
|
retry: 0,
|
|
723
802
|
hooks: {
|
|
724
803
|
beforeRequest: [
|
|
725
|
-
(
|
|
726
|
-
requestTraceMap.set(options, {
|
|
804
|
+
async ({ request, options }) => {
|
|
805
|
+
requestTraceMap.set(options.context, {
|
|
727
806
|
startTime: performance.now(),
|
|
728
|
-
body: emitFullTrace
|
|
729
|
-
? (options.json ?? options.body ?? undefined)
|
|
730
|
-
: undefined,
|
|
807
|
+
body: emitFullTrace ? await captureRequestBody(request) : undefined,
|
|
731
808
|
});
|
|
732
809
|
},
|
|
733
810
|
],
|
|
734
811
|
afterResponse: [
|
|
735
|
-
async (request,
|
|
736
|
-
|
|
812
|
+
async ({ request, options, response }) => {
|
|
813
|
+
// `request` here is the final (possibly hook-replaced) request — correct
|
|
814
|
+
// for the trace target; the trace state is keyed by the stable context.
|
|
815
|
+
const trace = requestTraceMap.get(options.context);
|
|
737
816
|
const duration = Math.round(performance.now() - (trace?.startTime ?? performance.now()));
|
|
738
817
|
// Increment HTTP counters for summary
|
|
739
818
|
{
|
|
@@ -807,17 +886,22 @@ const kyInstance = ky.create({
|
|
|
807
886
|
}
|
|
808
887
|
// Per-request state is on the options object; no global cleanup needed.
|
|
809
888
|
}
|
|
810
|
-
|
|
889
|
+
// Attribute to the active workflow node's scope when one is executing
|
|
890
|
+
// (the SDK's ctx.http rebind, §17 #10/#12): inline HTTP inside a
|
|
891
|
+
// workflow node promotes its grade and obeys the late-evidence
|
|
892
|
+
// quarantine. Outside a workflow node this is the closure ctx as ever.
|
|
893
|
+
const sink = __activeWorkflowNodeCtx() ?? ctx;
|
|
894
|
+
sink.trace(traceData);
|
|
811
895
|
// Auto-metric for response time
|
|
812
896
|
try {
|
|
813
897
|
const pathname = new URL(request.url).pathname;
|
|
814
|
-
|
|
898
|
+
sink.metric("http_duration_ms", duration, {
|
|
815
899
|
unit: "ms",
|
|
816
900
|
tags: { method: request.method, path: pathname },
|
|
817
901
|
});
|
|
818
902
|
}
|
|
819
903
|
catch {
|
|
820
|
-
|
|
904
|
+
sink.metric("http_duration_ms", duration, {
|
|
821
905
|
unit: "ms",
|
|
822
906
|
tags: { method: request.method },
|
|
823
907
|
});
|
|
@@ -844,6 +928,13 @@ function normalizeOptions(options) {
|
|
|
844
928
|
if (!options)
|
|
845
929
|
return options;
|
|
846
930
|
const normalized = { ...options };
|
|
931
|
+
// ky 2 renamed `prefixUrl` → `prefix` (with the same join semantics we rely on
|
|
932
|
+
// for "users" / "/users"). Glubean keeps `prefixUrl` as its public option and
|
|
933
|
+
// translates here at the ky boundary (codex ky2 P2-5; not `baseUrl`).
|
|
934
|
+
if (normalized.prefixUrl !== undefined) {
|
|
935
|
+
normalized.prefix = normalized.prefixUrl;
|
|
936
|
+
delete normalized.prefixUrl;
|
|
937
|
+
}
|
|
847
938
|
// Remove empty searchParams so ky doesn't append a bare '?'
|
|
848
939
|
if (normalized.searchParams != null) {
|
|
849
940
|
if (normalized.searchParams instanceof URLSearchParams) {
|
|
@@ -947,7 +1038,7 @@ function wrapKy(instance, label = "base") {
|
|
|
947
1038
|
const responseHeadersSchema = normalized?.schema?.responseHeaders;
|
|
948
1039
|
if (responseHeadersSchema) {
|
|
949
1040
|
const { schema, severity } = resolveSchemaEntry(responseHeadersSchema);
|
|
950
|
-
const headersHook = (
|
|
1041
|
+
const headersHook = ({ response }) => {
|
|
951
1042
|
const headersObj = normalizeHeadersForValidation(response.headers);
|
|
952
1043
|
runSchemaValidation(headersObj, schema, "response headers", severity);
|
|
953
1044
|
};
|
|
@@ -1007,6 +1098,28 @@ function withEnvFallback(explicit) {
|
|
|
1007
1098
|
},
|
|
1008
1099
|
});
|
|
1009
1100
|
}
|
|
1101
|
+
// runner-on-engine (plan 0005): when GLUBEAN_USE_ENGINE=1 the engine drives the
|
|
1102
|
+
// run-loop (executeNewTest delegates to RunnerCore). Construct it BEFORE
|
|
1103
|
+
// setRuntime so RunnerCore's ALS carrier is the one the SDK runtime fallback gets
|
|
1104
|
+
// set on — module-load configure() and the engine's runWithRuntime() then share a
|
|
1105
|
+
// single carrier (plan 0005 §接缝设计 / codex P1-5). Default OFF → no behavior change.
|
|
1106
|
+
const engineCore = USE_ENGINE
|
|
1107
|
+
? createEngineCore(emitEngineWire, {
|
|
1108
|
+
// Pass the SAME fallback Proxies the legacy ctx/runtime use (.env →
|
|
1109
|
+
// process.env), so engine-mode ctx.vars/secrets keep the system-env fallback
|
|
1110
|
+
// (codex P2 / plan 0005 §E). The engine layers per-run input over these
|
|
1111
|
+
// without destroying the Proxy.
|
|
1112
|
+
vars: withEnvFallback(rawVars),
|
|
1113
|
+
secrets: withEnvFallback(rawSecrets),
|
|
1114
|
+
// RAW vars (un-proxied) so ctx.vars.all() returns {...rawVars} exactly — legacy
|
|
1115
|
+
// parity for empty/overlay vars (codex Phase-8 P2).
|
|
1116
|
+
varsRaw: rawVars,
|
|
1117
|
+
// Forward the trace policy so the engine's ky auto-trace capture matches legacy.
|
|
1118
|
+
emitFullTrace: !!emitFullTrace,
|
|
1119
|
+
inferSchema: !!inferSchema,
|
|
1120
|
+
truncateArrays: !!truncateArrays,
|
|
1121
|
+
})
|
|
1122
|
+
: undefined;
|
|
1010
1123
|
setRuntime({
|
|
1011
1124
|
vars: withEnvFallback(rawVars),
|
|
1012
1125
|
secrets: withEnvFallback(rawSecrets),
|
|
@@ -1269,8 +1382,8 @@ try {
|
|
|
1269
1382
|
testObj = findTestByExport(userModule, exportNamesMap[id]);
|
|
1270
1383
|
}
|
|
1271
1384
|
if (!testObj) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1385
|
+
writeEventLine(JSON.stringify({ type: "start", id, name: id, testId: id }));
|
|
1386
|
+
writeEventLine(JSON.stringify({ type: "status", status: "failed", id, testId: id, error: `Test "${id}" not found in module` }));
|
|
1274
1387
|
hasFailure = true;
|
|
1275
1388
|
return;
|
|
1276
1389
|
}
|
|
@@ -1279,12 +1392,12 @@ try {
|
|
|
1279
1392
|
}
|
|
1280
1393
|
catch (error) {
|
|
1281
1394
|
if (error instanceof SkipError) {
|
|
1282
|
-
|
|
1395
|
+
writeEventLine(JSON.stringify({ type: "status", status: "skipped", id, testId: id, reason: error.reason }));
|
|
1283
1396
|
}
|
|
1284
1397
|
else {
|
|
1285
1398
|
hasFailure = true;
|
|
1286
1399
|
const reason = classifyErrorReason(error);
|
|
1287
|
-
|
|
1400
|
+
writeEventLine(JSON.stringify({
|
|
1288
1401
|
type: "status", status: "failed", id, testId: id,
|
|
1289
1402
|
error: error instanceof Error ? error.message : String(error),
|
|
1290
1403
|
stack: error instanceof Error ? error.stack : undefined,
|
|
@@ -1334,7 +1447,20 @@ try {
|
|
|
1334
1447
|
const { default: PQueue } = await import("p-queue");
|
|
1335
1448
|
const queue = new PQueue({ concurrency: batchConcurrency });
|
|
1336
1449
|
for (const id of expandedIds) {
|
|
1337
|
-
void queue.add(() =>
|
|
1450
|
+
void queue.add(() => {
|
|
1451
|
+
const buf = [];
|
|
1452
|
+
return parallelEventBuffer.run(buf, async () => {
|
|
1453
|
+
try {
|
|
1454
|
+
await runOneTest(id);
|
|
1455
|
+
}
|
|
1456
|
+
finally {
|
|
1457
|
+
// Atomic flush: the loop is synchronous, so this test's whole
|
|
1458
|
+
// event block lands contiguously on stdout.
|
|
1459
|
+
for (const line of buf)
|
|
1460
|
+
console.log(line);
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1338
1464
|
}
|
|
1339
1465
|
await queue.onIdle();
|
|
1340
1466
|
}
|
|
@@ -1483,7 +1609,96 @@ async function withFixtures(fixtures, baseCtx, runTest) {
|
|
|
1483
1609
|
*
|
|
1484
1610
|
* @param test The Test object to execute
|
|
1485
1611
|
*/
|
|
1612
|
+
/**
|
|
1613
|
+
* Structural gate for the engine path (plan 0005): Phase 0 routes ONLY simple
|
|
1614
|
+
* tests. Steps stay on legacy until the engine emits step_start/step_end + per-step
|
|
1615
|
+
* index/retry/timeout (Phase 1); branch/poll (Phase 2/3); test.extend() fixtures
|
|
1616
|
+
* and workflow later. Routing a steps test now would lose its step timeline and
|
|
1617
|
+
* skew summaries (codex P2). ctx-surface gaps (validate/metric/…) are managed by
|
|
1618
|
+
* only exercising migrated features under the flag.
|
|
1619
|
+
*/
|
|
1620
|
+
function engineSupports(test) {
|
|
1621
|
+
if (test.fixtures && Object.keys(test.fixtures).length > 0)
|
|
1622
|
+
return false;
|
|
1623
|
+
// Built workflow / contract wrappers are simple-shaped Tests, but their fn emits
|
|
1624
|
+
// workflow:* events the LEGACY harness unwraps into node/poll timeline events (and
|
|
1625
|
+
// inbound contract is node-only) — the browser-safe engine doesn't, so keep them on
|
|
1626
|
+
// legacy even under route-all (codex Phase-3 P2; plan 0005 §scope). The SDK marks
|
|
1627
|
+
// these wrappers with __glubean_kind.
|
|
1628
|
+
if (test.__glubean_kind)
|
|
1629
|
+
return false;
|
|
1630
|
+
if (test.type === "simple")
|
|
1631
|
+
return true;
|
|
1632
|
+
if (test.type === "steps") {
|
|
1633
|
+
// Linear steps + retry/timeout (Phase 1) + branch (Phase 2) + poll (Phase 3).
|
|
1634
|
+
return stepsEngineSupported(test.steps ?? []);
|
|
1635
|
+
}
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
function stepsEngineSupported(steps) {
|
|
1639
|
+
for (const step of steps) {
|
|
1640
|
+
// poll steps (Phase 3) are now engine-supported; recurse into branch cases for
|
|
1641
|
+
// any still-unsupported shape (none today — kept as a structural guard).
|
|
1642
|
+
if (isTestBranchStep(step)) {
|
|
1643
|
+
for (const c of step.branch.cases)
|
|
1644
|
+
if (!stepsEngineSupported(c.steps))
|
|
1645
|
+
return false;
|
|
1646
|
+
if (!stepsEngineSupported(step.branch.default ?? []))
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1486
1652
|
async function executeNewTest(test) {
|
|
1653
|
+
// runner-on-engine (plan 0005): route to the engine only when (a) the engine is
|
|
1654
|
+
// active, (b) this test id is on the per-test allowlist (GLUBEAN_ENGINE_TESTIDS;
|
|
1655
|
+
// "*" = all at cutover) — so the flag never sends arbitrary production tests
|
|
1656
|
+
// through an incomplete ctx (codex), and (c) the test's shape is supported. The
|
|
1657
|
+
// engine owns its scope/carrier/per-event-id and runs OUTSIDE the legacy
|
|
1658
|
+
// testContext ALS (testId comes from each event's own id). Anything else → legacy.
|
|
1659
|
+
if (engineCore && engineRoutesId(test.meta.id) && engineSupports(test)) {
|
|
1660
|
+
// Wrap with the same memory monitoring as the legacy path so the final status
|
|
1661
|
+
// carries peakMemoryBytes/peakMemoryMB (codex P2).
|
|
1662
|
+
startMemoryMonitoring();
|
|
1663
|
+
const result = await runViaEngine(engineCore, test, { session: sessionData, retryCount });
|
|
1664
|
+
const peakBytes = stopMemoryMonitoring();
|
|
1665
|
+
// Status emission mirrors the legacy split: a throw re-raises so the dispatcher
|
|
1666
|
+
// reports "failed" + exit 1 (the engine swallows throws into a result); success
|
|
1667
|
+
// and skip emit their status here (plan 0005 / codex P2). A soft assertion
|
|
1668
|
+
// failure is NOT a throw → it "completed" (pass/fail is derived from the
|
|
1669
|
+
// assertion events downstream, as in the legacy path).
|
|
1670
|
+
if (result.threw) {
|
|
1671
|
+
const err = new Error(result.error ?? "test threw");
|
|
1672
|
+
// Re-raise with the user's original name + stack so classifyErrorReason() and
|
|
1673
|
+
// diagnostics match the legacy path (codex P2).
|
|
1674
|
+
if (result.errorName)
|
|
1675
|
+
err.name = result.errorName;
|
|
1676
|
+
if (result.errorStack)
|
|
1677
|
+
err.stack = result.errorStack;
|
|
1678
|
+
throw err;
|
|
1679
|
+
}
|
|
1680
|
+
if (result.stepsFailed) {
|
|
1681
|
+
// Node parity (harness.ts:2686): a steps test with any failed step throws after
|
|
1682
|
+
// teardown so the dispatcher reports failed + exit 1 — unlike a simple test's
|
|
1683
|
+
// soft assertion ("completes"). A branch-decision failure carries its own message.
|
|
1684
|
+
throw new Error(result.stepsFailMessage ?? "One or more steps failed");
|
|
1685
|
+
}
|
|
1686
|
+
if (result.status === "skipped") {
|
|
1687
|
+
// Re-raise a harness SkipError(reason) so the SAME dispatcher catch that
|
|
1688
|
+
// handles a legacy ctx.skip() emits the skipped status (with `reason`) — byte
|
|
1689
|
+
// parity with legacy, which throws skipRequest out to the dispatcher (plan 0005).
|
|
1690
|
+
throw new SkipError(result.skipReason);
|
|
1691
|
+
}
|
|
1692
|
+
emitEngineWire({
|
|
1693
|
+
type: "status",
|
|
1694
|
+
status: "completed",
|
|
1695
|
+
id: test.meta.id,
|
|
1696
|
+
testId: test.meta.id,
|
|
1697
|
+
peakMemoryBytes: peakBytes,
|
|
1698
|
+
peakMemoryMB: (peakBytes / 1024 / 1024).toFixed(2),
|
|
1699
|
+
});
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1487
1702
|
const testTags = normalizeTestTags(test.meta.tags);
|
|
1488
1703
|
const testMeta = { id: test.meta.id, tags: testTags };
|
|
1489
1704
|
const trc = new TestRunContext(test.meta.id, testMeta);
|
|
@@ -1506,6 +1721,27 @@ async function executeNewTest(test) {
|
|
|
1506
1721
|
// Core test body — receives the effective ctx (base or fixture-augmented)
|
|
1507
1722
|
const runTestBody = async (effectiveCtx) => {
|
|
1508
1723
|
if (test.type === "simple") {
|
|
1724
|
+
// Invocation inversion (plan 0007): a built workflow is a first-class DEF, not a
|
|
1725
|
+
// self-executing test. The SDK marks the wrapper `__glubean_kind === "workflow"`
|
|
1726
|
+
// and attaches the Workflow IR to `__glubean_workflow`; the host run-loop drives
|
|
1727
|
+
// it through the workflow executor here — like RunnerCore.run() dispatches simple
|
|
1728
|
+
// vs steps. This replaces the SDK's old wfTest.fn (which called runWorkflow
|
|
1729
|
+
// itself); the VERDICT mapping is identical, just host-owned: a skipped run →
|
|
1730
|
+
// ctx.skip(reason) (throws GlubeanSkipError, caught by the same dispatcher); a
|
|
1731
|
+
// failed run → rethrow the cause so the dispatcher reports failed + exit 1.
|
|
1732
|
+
const wfIr = test.__glubean_workflow;
|
|
1733
|
+
if (wfIr) {
|
|
1734
|
+
const result = await runWorkflow(wfIr, effectiveCtx);
|
|
1735
|
+
if (result.status === "skipped") {
|
|
1736
|
+
// Prefer the user-authored runtime ctx.skip(reason), then the authored
|
|
1737
|
+
// meta.skip, then a generic fallback (parity with the old wfTest.fn).
|
|
1738
|
+
effectiveCtx.skip(result.skipReason ?? wfIr.meta.skip ?? `workflow "${wfIr.meta.id}" skipped`);
|
|
1739
|
+
}
|
|
1740
|
+
if (result.status === "failed") {
|
|
1741
|
+
throw result.error ?? new WorkflowPhaseFailedError(wfIr.meta.id, "workflow");
|
|
1742
|
+
}
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1509
1745
|
if (!test.fn) {
|
|
1510
1746
|
throw new Error(`Invalid test "${test.meta.id}": missing fn`);
|
|
1511
1747
|
}
|