@unotest/core 0.30.0 → 0.32.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/CHANGELOG.md +73 -0
- package/dist/index.d.ts +32 -3
- package/dist/index.js +57 -10
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.32.0] - 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 047ca68: A failure somebody paused on stays a failure, with the evidence to show for it.
|
|
8
|
+
|
|
9
|
+
A run driven through the debugger — `run_test` then `resume`, or the viewer's
|
|
10
|
+
Continue — reported `completed` after a failure it had paused on, wrote no
|
|
11
|
+
failure bundle and no `failure/` artifacts, and left `list_failures` with
|
|
12
|
+
nothing to show. Continuing past a failure is how it gets inspected; it was
|
|
13
|
+
never meant to retract it. The verdict is now decided where the run's own
|
|
14
|
+
events are seen, so the journal, `runtime.json` and the reply agree, and a
|
|
15
|
+
debug run leaves the same evidence a plain `unotest-web e2e` run does.
|
|
16
|
+
|
|
17
|
+
`abort_runtime` (and Stop, and SIGTERM) now also ends the run in
|
|
18
|
+
`runtime.json`, not only in `steps.jsonl`: the control file used to keep
|
|
19
|
+
saying `paused-step` about a run that was over, so anything reading it rather
|
|
20
|
+
than the journal saw a pause that never ended.
|
|
21
|
+
|
|
22
|
+
- 047ca68: A `step.soft(...)` failure no longer stops a debug run — or goes missing from it.
|
|
23
|
+
|
|
24
|
+
Under a debugger (`run_test`, or `e2e --debug`), every soft failure raised
|
|
25
|
+
the debug wheel: the run stopped on each one and an agent had to `resume`
|
|
26
|
+
its way through them. Worse, it was then lost — a paused failure is consumed
|
|
27
|
+
where it paused, so it never reached the `step.soft` envelope that records
|
|
28
|
+
it, the envelope closed as if the block had passed, and a run with three
|
|
29
|
+
soft failures could finish green with none of them listed.
|
|
30
|
+
|
|
31
|
+
Pausing is now for a failure that ENDS the run. A failure under any
|
|
32
|
+
enclosing soft step is recorded and stepped over exactly as it is on the
|
|
33
|
+
command line, and `runtime.json` — which the `run_test` reply is built from —
|
|
34
|
+
carries every soft failure of the run, not just the last stop.
|
|
35
|
+
|
|
36
|
+
A hard failure still pauses: that is what the debugger is for.
|
|
37
|
+
|
|
38
|
+
The same holds on mobile: a `step.soft(...)` failure no longer stops a run
|
|
39
|
+
under the debugger, and every soft failure of a run is now part of its
|
|
40
|
+
runtime state instead of being lost at the pause.
|
|
41
|
+
|
|
42
|
+
- Updated dependencies [8aa0f30]
|
|
43
|
+
- Updated dependencies [047ca68]
|
|
44
|
+
- Updated dependencies [047ca68]
|
|
45
|
+
- @unotest/protocol@0.32.0
|
|
46
|
+
- @unotest/dsl@0.32.0
|
|
47
|
+
|
|
48
|
+
## [0.31.0] - 2026-09-04
|
|
49
|
+
|
|
50
|
+
### Minor Changes
|
|
51
|
+
|
|
52
|
+
- 100ab9d: `buildRuntimeInspection` keeps `stepTag` and `soft` on `lastFailure` and
|
|
53
|
+
passes the runtime's `softFailures` list through into `runtime.json`, so
|
|
54
|
+
`inspect_runtime` and `run_test` can report the case a data-driven step
|
|
55
|
+
was on and every soft step that failed.
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- b8e105b: `capUtf8(text, maxBytes)` — cut a string to a UTF-8 byte budget without
|
|
60
|
+
splitting a code point — exported next to `JOURNAL_TEXT_CAP_BYTES`
|
|
61
|
+
(4 KB). The runtime-state var snapshot now caps by bytes through it (it
|
|
62
|
+
used to count UTF-16 units); the run journal's `note` / `log` payloads
|
|
63
|
+
use the same cap.
|
|
64
|
+
- Updated dependencies [b8e105b]
|
|
65
|
+
- Updated dependencies [9ae6d59]
|
|
66
|
+
- Updated dependencies [56784b3]
|
|
67
|
+
- Updated dependencies [e048334]
|
|
68
|
+
- Updated dependencies [100ab9d]
|
|
69
|
+
- Updated dependencies [100ab9d]
|
|
70
|
+
- Updated dependencies [ad7c918]
|
|
71
|
+
- Updated dependencies [ad7c918]
|
|
72
|
+
- Updated dependencies [dcf4c9e]
|
|
73
|
+
- @unotest/protocol@0.31.0
|
|
74
|
+
- @unotest/dsl@0.31.0
|
|
75
|
+
|
|
3
76
|
## [0.30.0] - 2026-09-03
|
|
4
77
|
|
|
5
78
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RunArtifact, RunManifest, RunSources, RuntimeStateFile, RuntimeState, DbgCommand, PickerMode, DbgCommandRecord, DbgCommandProbe, IRunQueue, EnqueueRunInput, RunQueueTicketHandle, QueueSnapshot, QueueTicket, RunQueueLease, BundleGitInfo, BundleManifest, ScheduleEntry } from '@unotest/protocol';
|
|
1
|
+
import { RunArtifact, RunManifest, RunSources, RuntimeStateFile, SoftFailureRecord, RuntimeState, DbgCommand, PickerMode, DbgCommandRecord, DbgCommandProbe, IRunQueue, EnqueueRunInput, RunQueueTicketHandle, QueueSnapshot, QueueTicket, RunQueueLease, BundleGitInfo, BundleManifest, ScheduleEntry } from '@unotest/protocol';
|
|
2
2
|
export { isRecord } from '@unotest/protocol';
|
|
3
3
|
import { ExecutionEvent } from '@unotest/dsl/executor';
|
|
4
4
|
|
|
@@ -91,7 +91,7 @@ type RuntimeExecState = "starting" | "running" | "paused-step" | "paused-failure
|
|
|
91
91
|
/** Normalized, protocol-shaped runtime view assembled from a manager snapshot.
|
|
92
92
|
* Feeds straight into `RuntimeStateFile` (the writer adds runId / wsEndpoint /
|
|
93
93
|
* wheel / updatedAt) — hence a Pick, not a re-spelled mirror. */
|
|
94
|
-
type RuntimeInspection = Pick<RuntimeStateFile, "scenario" | "testFn" | "state" | "current" | "vars" | "callStack" | "lastFailure">;
|
|
94
|
+
type RuntimeInspection = Pick<RuntimeStateFile, "scenario" | "testFn" | "state" | "current" | "vars" | "callStack" | "lastFailure" | "softFailures">;
|
|
95
95
|
/** Raw inputs from a `TestRuntimeManager.inspect()` snapshot, before
|
|
96
96
|
* protocol-normalization. `lastFailure` is the manager's richer record; only
|
|
97
97
|
* line/col/message survive into the protocol form. */
|
|
@@ -108,7 +108,13 @@ interface RuntimeInspectionInput {
|
|
|
108
108
|
message: string;
|
|
109
109
|
name?: string;
|
|
110
110
|
};
|
|
111
|
+
/** Evaluated `{tag}` of the enclosing `step(...)`, when any. */
|
|
112
|
+
stepTag?: string;
|
|
113
|
+
/** The failure sat inside a `step.soft(...)` — the run went on. */
|
|
114
|
+
soft?: boolean;
|
|
111
115
|
} | null;
|
|
116
|
+
/** Failed `step.soft(...)` blocks so far, in run order. */
|
|
117
|
+
softFailures?: readonly SoftFailureRecord[];
|
|
112
118
|
}
|
|
113
119
|
declare function buildRuntimeInspection(input: RuntimeInspectionInput, stringifyVar: (v: unknown) => string): RuntimeInspection;
|
|
114
120
|
declare function toProtocolRuntimeState(state: RuntimeExecState, lastEvent: ExecutionEvent | null): RuntimeState;
|
|
@@ -121,6 +127,17 @@ declare function extractCallStack(ev: ExecutionEvent | null): {
|
|
|
121
127
|
file: string;
|
|
122
128
|
line: number;
|
|
123
129
|
}[];
|
|
130
|
+
/** Cut `s` to at most `maxBytes` of UTF-8 without splitting a code point.
|
|
131
|
+
* Shared by the runtime-state var snapshot and the run journal's `note` /
|
|
132
|
+
* `log` events, so "what fits in a line" means one thing everywhere. */
|
|
133
|
+
declare function capUtf8(s: string, maxBytes: number): {
|
|
134
|
+
text: string;
|
|
135
|
+
truncated: boolean;
|
|
136
|
+
};
|
|
137
|
+
/** The journal's cap for one `note` / `log` payload — the same 4 KB the
|
|
138
|
+
* var snapshot uses: a small object or a response body fits, an HTML
|
|
139
|
+
* document does not. */
|
|
140
|
+
declare const JOURNAL_TEXT_CAP_BYTES = 4096;
|
|
124
141
|
|
|
125
142
|
/** Runner-specific extras merged into every snapshot. Evaluated on each write
|
|
126
143
|
* so live values (the current wheel owner) stay fresh. */
|
|
@@ -155,6 +172,18 @@ interface RuntimeStateWriter {
|
|
|
155
172
|
writeThrottled(): Promise<void>;
|
|
156
173
|
/** Final flush — emit terminal state once more. Idempotent. */
|
|
157
174
|
close(): Promise<void>;
|
|
175
|
+
/** Final flush from a path that cannot await: an abort or a signal
|
|
176
|
+
* handler that ends with `process.exit`, where a promise never
|
|
177
|
+
* settles and the file would keep whatever the run last wrote —
|
|
178
|
+
* `paused-step` forever, on a run that is over.
|
|
179
|
+
*
|
|
180
|
+
* `state` names the terminal state explicitly, because the writer
|
|
181
|
+
* cannot infer it: on the signal path the runtime is still `running`
|
|
182
|
+
* as far as it knows, and the truth is that we are killing it.
|
|
183
|
+
*
|
|
184
|
+
* Shares `close()`'s idempotence — whichever runs first wins, and the
|
|
185
|
+
* other is a no-op. On a signal the order is not ours to choose. */
|
|
186
|
+
closeSync(state?: RuntimeStateFile["state"]): void;
|
|
158
187
|
}
|
|
159
188
|
declare function createRuntimeStateWriter(deps: RuntimeStateWriterDeps): RuntimeStateWriter;
|
|
160
189
|
|
|
@@ -498,4 +527,4 @@ declare class BundleIdMismatchError extends UnotestError {
|
|
|
498
527
|
constructor(claimed: string, computed: string);
|
|
499
528
|
}
|
|
500
529
|
|
|
501
|
-
export { type ArtifactRedactor, type BundleFile, BundleFormatError, BundleIdMismatchError, BundlePathError, type BundleProvenance, BundleTooLargeError, type DebugCommandsWatcherDeps, type DebugControlTarget, type DebugWatcherLogger, type E2EFlags, FsRunQueue, type FsRunQueueOptions, type GitignoreUpdate, type GlobalPoolOptions, type IRunArtifactWriter, JsonlRunArtifactWriter, type JsonlRunArtifactWriterOptions, type PackedBundle, QueueTicketLostError, QueueUnavailableError, QueueWaitAbortedError, type RunHeartbeat, type RuntimeExecState, type RuntimeInspection, type RuntimeInspectionInput, type RuntimeStateExtra, type RuntimeStateWriter, type RuntimeStateWriterDeps, type TarEntry, UnotestError, type UnpackOptions, type UnpackedBundle, type WriteManifestInput, type WriteRunSourcesInput, appendUniqueLines, applyEnvLayers, assertSafeBundlePath, buildRuntimeInspection, computeBundleId, computeSchedulesFingerprint, computeTreeSha256, createRunArtifactWriter, createRuntimeStateWriter, currentLocation, extractCallStack, globalPoolFromEnv, leaseEnv, levenshteinDistance, packBundle, parseE2EFlags, readDebuggerBreakpoints, readEnvFile, readEnvLayers, readTar, startDebugCommandsWatcher, startRunHeartbeat, suggestClosest, toProtocolRuntimeState, unpackBundle, writeRunManifest, writeRunSources, writeTar };
|
|
530
|
+
export { type ArtifactRedactor, type BundleFile, BundleFormatError, BundleIdMismatchError, BundlePathError, type BundleProvenance, BundleTooLargeError, type DebugCommandsWatcherDeps, type DebugControlTarget, type DebugWatcherLogger, type E2EFlags, FsRunQueue, type FsRunQueueOptions, type GitignoreUpdate, type GlobalPoolOptions, type IRunArtifactWriter, JOURNAL_TEXT_CAP_BYTES, JsonlRunArtifactWriter, type JsonlRunArtifactWriterOptions, type PackedBundle, QueueTicketLostError, QueueUnavailableError, QueueWaitAbortedError, type RunHeartbeat, type RuntimeExecState, type RuntimeInspection, type RuntimeInspectionInput, type RuntimeStateExtra, type RuntimeStateWriter, type RuntimeStateWriterDeps, type TarEntry, UnotestError, type UnpackOptions, type UnpackedBundle, type WriteManifestInput, type WriteRunSourcesInput, appendUniqueLines, applyEnvLayers, assertSafeBundlePath, buildRuntimeInspection, capUtf8, computeBundleId, computeSchedulesFingerprint, computeTreeSha256, createRunArtifactWriter, createRuntimeStateWriter, currentLocation, extractCallStack, globalPoolFromEnv, leaseEnv, levenshteinDistance, packBundle, parseE2EFlags, readDebuggerBreakpoints, readEnvFile, readEnvLayers, readTar, startDebugCommandsWatcher, startRunHeartbeat, suggestClosest, toProtocolRuntimeState, unpackBundle, writeRunManifest, writeRunSources, writeTar };
|
package/dist/index.js
CHANGED
|
@@ -171,8 +171,11 @@ function buildRuntimeInspection(input, stringifyVar) {
|
|
|
171
171
|
lastFailure: input.lastFailure ? {
|
|
172
172
|
line: input.lastFailure.line,
|
|
173
173
|
col: input.lastFailure.col,
|
|
174
|
-
error: { message: input.lastFailure.error.message }
|
|
175
|
-
|
|
174
|
+
error: { message: input.lastFailure.error.message },
|
|
175
|
+
...input.lastFailure.stepTag !== void 0 ? { stepTag: input.lastFailure.stepTag } : {},
|
|
176
|
+
...input.lastFailure.soft ? { soft: true } : {}
|
|
177
|
+
} : null,
|
|
178
|
+
...input.softFailures && input.softFailures.length > 0 ? { softFailures: input.softFailures.map((f) => ({ ...f })) } : {}
|
|
176
179
|
};
|
|
177
180
|
}
|
|
178
181
|
__name(buildRuntimeInspection, "buildRuntimeInspection");
|
|
@@ -227,13 +230,29 @@ function serializeVars(vars, stringifyVar) {
|
|
|
227
230
|
}
|
|
228
231
|
__name(serializeVars, "serializeVars");
|
|
229
232
|
function truncate(s) {
|
|
230
|
-
|
|
231
|
-
return
|
|
233
|
+
const capped = capUtf8(s, VAR_TRUNCATE_BYTES);
|
|
234
|
+
return capped.truncated ? `${capped.text}\u2026` : capped.text;
|
|
232
235
|
}
|
|
233
236
|
__name(truncate, "truncate");
|
|
237
|
+
function capUtf8(s, maxBytes) {
|
|
238
|
+
const encoder = new TextEncoder();
|
|
239
|
+
if (encoder.encode(s).byteLength <= maxBytes) return { text: s, truncated: false };
|
|
240
|
+
let bytes = 0;
|
|
241
|
+
let end = 0;
|
|
242
|
+
for (const ch of s) {
|
|
243
|
+
const size = encoder.encode(ch).byteLength;
|
|
244
|
+
if (bytes + size > maxBytes) break;
|
|
245
|
+
bytes += size;
|
|
246
|
+
end += ch.length;
|
|
247
|
+
}
|
|
248
|
+
return { text: s.slice(0, end), truncated: true };
|
|
249
|
+
}
|
|
250
|
+
__name(capUtf8, "capUtf8");
|
|
251
|
+
var JOURNAL_TEXT_CAP_BYTES = VAR_TRUNCATE_BYTES;
|
|
234
252
|
|
|
235
253
|
// src/runtime-state-writer.ts
|
|
236
254
|
import { RUNTIME_FILE } from "@unotest/protocol";
|
|
255
|
+
import { renameSync, writeFileSync } from "fs";
|
|
237
256
|
import { rename as rename3, writeFile as writeFile4 } from "fs/promises";
|
|
238
257
|
import { join as join5 } from "path";
|
|
239
258
|
function createRuntimeStateWriter(deps) {
|
|
@@ -243,25 +262,34 @@ function createRuntimeStateWriter(deps) {
|
|
|
243
262
|
const now = deps.nowFn ?? Date.now;
|
|
244
263
|
let lastWriteAt = 0;
|
|
245
264
|
let closed = false;
|
|
246
|
-
const
|
|
247
|
-
if (closed) return;
|
|
265
|
+
const snapshot = /* @__PURE__ */ __name((state) => {
|
|
248
266
|
const insp = deps.inspect();
|
|
249
|
-
if (!insp) return;
|
|
267
|
+
if (!insp) return null;
|
|
250
268
|
const file = {
|
|
251
269
|
runId: deps.runId,
|
|
252
270
|
scenario: insp.scenario,
|
|
253
271
|
testFn: insp.testFn,
|
|
254
|
-
state: insp.state,
|
|
272
|
+
state: state ?? insp.state,
|
|
255
273
|
current: insp.current,
|
|
256
274
|
vars: insp.vars,
|
|
257
275
|
callStack: insp.callStack,
|
|
258
276
|
lastFailure: insp.lastFailure,
|
|
277
|
+
// Every soft failure the run went past, not just the last one it
|
|
278
|
+
// stopped at: a soft step records and carries on, so `lastFailure`
|
|
279
|
+
// alone leaves a reader (the viewer, `inspect_runtime`, the agent's
|
|
280
|
+
// run reply) believing a run with three of them had none.
|
|
281
|
+
...insp.softFailures && insp.softFailures.length > 0 ? { softFailures: [...insp.softFailures] } : {},
|
|
259
282
|
...deps.extra ? deps.extra() : {},
|
|
260
283
|
updatedAt: now()
|
|
261
284
|
};
|
|
285
|
+
return JSON.stringify(deps.redact ? deps.redact(file) : file);
|
|
286
|
+
}, "snapshot");
|
|
287
|
+
const writeNow = /* @__PURE__ */ __name(async () => {
|
|
288
|
+
if (closed) return;
|
|
289
|
+
const body = snapshot();
|
|
290
|
+
if (body === null) return;
|
|
262
291
|
try {
|
|
263
|
-
|
|
264
|
-
await writeFile4(tmpPath, JSON.stringify(safe), "utf8");
|
|
292
|
+
await writeFile4(tmpPath, body, "utf8");
|
|
265
293
|
await rename3(tmpPath, path);
|
|
266
294
|
lastWriteAt = now();
|
|
267
295
|
} catch {
|
|
@@ -277,6 +305,23 @@ function createRuntimeStateWriter(deps) {
|
|
|
277
305
|
if (closed) return;
|
|
278
306
|
await writeNow();
|
|
279
307
|
closed = true;
|
|
308
|
+
},
|
|
309
|
+
closeSync(state) {
|
|
310
|
+
if (closed) return;
|
|
311
|
+
closed = true;
|
|
312
|
+
let body;
|
|
313
|
+
try {
|
|
314
|
+
body = snapshot(state);
|
|
315
|
+
} catch {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (body === null) return;
|
|
319
|
+
try {
|
|
320
|
+
writeFileSync(tmpPath, body, "utf8");
|
|
321
|
+
renameSync(tmpPath, path);
|
|
322
|
+
lastWriteAt = now();
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
280
325
|
}
|
|
281
326
|
};
|
|
282
327
|
}
|
|
@@ -1578,6 +1623,7 @@ export {
|
|
|
1578
1623
|
BundlePathError,
|
|
1579
1624
|
BundleTooLargeError,
|
|
1580
1625
|
FsRunQueue,
|
|
1626
|
+
JOURNAL_TEXT_CAP_BYTES,
|
|
1581
1627
|
JsonlRunArtifactWriter,
|
|
1582
1628
|
QueueTicketLostError,
|
|
1583
1629
|
QueueUnavailableError,
|
|
@@ -1587,6 +1633,7 @@ export {
|
|
|
1587
1633
|
applyEnvLayers,
|
|
1588
1634
|
assertSafeBundlePath,
|
|
1589
1635
|
buildRuntimeInspection,
|
|
1636
|
+
capUtf8,
|
|
1590
1637
|
computeBundleId,
|
|
1591
1638
|
computeSchedulesFingerprint,
|
|
1592
1639
|
computeTreeSha256,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Shared runner-side utilities for the @unotest ecosystem: JSONL run-artifact writer (steps.jsonl + heartbeat), atomic run-manifest + run-sources writers, atomic runtime-state writer with a protocol-normalizing runtime-inspection helper, layered .env reader/applier (target + environment axes), idempotent .gitignore updater. Used by @unotest/web and @unotest/mobile; depends on @unotest/protocol plus a type-only import of @unotest/dsl/executor event types (M-20). Unlike protocol (pure data), this package owns the thin filesystem layer both runners need.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"chokidar": "^4.0.3",
|
|
32
|
-
"@unotest/
|
|
33
|
-
"@unotest/
|
|
32
|
+
"@unotest/protocol": "^0.32.0",
|
|
33
|
+
"@unotest/dsl": "^0.32.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.10.0",
|