@unotest/core 0.31.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 CHANGED
@@ -1,5 +1,50 @@
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
+
3
48
  ## [0.31.0] - 2026-09-04
4
49
 
5
50
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -172,6 +172,18 @@ interface RuntimeStateWriter {
172
172
  writeThrottled(): Promise<void>;
173
173
  /** Final flush — emit terminal state once more. Idempotent. */
174
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;
175
187
  }
176
188
  declare function createRuntimeStateWriter(deps: RuntimeStateWriterDeps): RuntimeStateWriter;
177
189
 
package/dist/index.js CHANGED
@@ -252,6 +252,7 @@ var JOURNAL_TEXT_CAP_BYTES = VAR_TRUNCATE_BYTES;
252
252
 
253
253
  // src/runtime-state-writer.ts
254
254
  import { RUNTIME_FILE } from "@unotest/protocol";
255
+ import { renameSync, writeFileSync } from "fs";
255
256
  import { rename as rename3, writeFile as writeFile4 } from "fs/promises";
256
257
  import { join as join5 } from "path";
257
258
  function createRuntimeStateWriter(deps) {
@@ -261,25 +262,34 @@ function createRuntimeStateWriter(deps) {
261
262
  const now = deps.nowFn ?? Date.now;
262
263
  let lastWriteAt = 0;
263
264
  let closed = false;
264
- const writeNow = /* @__PURE__ */ __name(async () => {
265
- if (closed) return;
265
+ const snapshot = /* @__PURE__ */ __name((state) => {
266
266
  const insp = deps.inspect();
267
- if (!insp) return;
267
+ if (!insp) return null;
268
268
  const file = {
269
269
  runId: deps.runId,
270
270
  scenario: insp.scenario,
271
271
  testFn: insp.testFn,
272
- state: insp.state,
272
+ state: state ?? insp.state,
273
273
  current: insp.current,
274
274
  vars: insp.vars,
275
275
  callStack: insp.callStack,
276
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] } : {},
277
282
  ...deps.extra ? deps.extra() : {},
278
283
  updatedAt: now()
279
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;
280
291
  try {
281
- const safe = deps.redact ? deps.redact(file) : file;
282
- await writeFile4(tmpPath, JSON.stringify(safe), "utf8");
292
+ await writeFile4(tmpPath, body, "utf8");
283
293
  await rename3(tmpPath, path);
284
294
  lastWriteAt = now();
285
295
  } catch {
@@ -295,6 +305,23 @@ function createRuntimeStateWriter(deps) {
295
305
  if (closed) return;
296
306
  await writeNow();
297
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
+ }
298
325
  }
299
326
  };
300
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unotest/core",
3
- "version": "0.31.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/protocol": "^0.31.0",
33
- "@unotest/dsl": "^0.31.0"
32
+ "@unotest/protocol": "^0.32.0",
33
+ "@unotest/dsl": "^0.32.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^22.10.0",