@unotest/core 0.31.0 → 0.33.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 +84 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +48 -13
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.33.0] - 2026-09-06
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8846a06: Viewer: a run started the moment the viewer came up is shown, not lost
|
|
8
|
+
until the next change.
|
|
9
|
+
|
|
10
|
+
Watching a directory becomes live a moment after the watcher reports it
|
|
11
|
+
is ready, and a run directory created inside that gap was not merely
|
|
12
|
+
late — it was invisible until something else changed in the same day,
|
|
13
|
+
which in a quiet environment is the next run. Measured at 1-3% on macOS
|
|
14
|
+
with a busy machine, and the window is exactly "open the viewer, start a
|
|
15
|
+
run". The viewer now proves the watch delivers before it declares itself
|
|
16
|
+
up: it creates a directory of its own and waits to hear about it, so
|
|
17
|
+
boot completes on evidence rather than on a promise. A tree it cannot
|
|
18
|
+
write to, or a probe that never comes back, costs a warning in the log
|
|
19
|
+
and boots as before.
|
|
20
|
+
|
|
21
|
+
A directory whose name starts with a dot is never reported as a run — an
|
|
22
|
+
editor's leftovers, or the watcher's own probe, can no longer appear in
|
|
23
|
+
the run list as a phantom.
|
|
24
|
+
|
|
25
|
+
Run queue: a waiter whose process paused is no longer mistaken for dead
|
|
26
|
+
— the queue stamps its own files with its own clock.
|
|
27
|
+
|
|
28
|
+
A run waiting for a slot proves it is alive by touching its ticket, and
|
|
29
|
+
what it is judged against was the modification time the filesystem
|
|
30
|
+
wrote: on a network share that is the server's clock, and against a
|
|
31
|
+
machine busy enough to keep a process off the CPU, a ticket written a
|
|
32
|
+
moment ago could look like it belonged to a process that died. The queue
|
|
33
|
+
now stamps every file it creates with the same clock it judges them by.
|
|
34
|
+
|
|
35
|
+
- Updated dependencies [2abfd4d]
|
|
36
|
+
- Updated dependencies [b3e1220]
|
|
37
|
+
- Updated dependencies [b3e1220]
|
|
38
|
+
- Updated dependencies [5d1bde3]
|
|
39
|
+
- @unotest/protocol@0.33.0
|
|
40
|
+
- @unotest/dsl@0.33.0
|
|
41
|
+
|
|
42
|
+
## [0.32.0] - 2026-09-05
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- 047ca68: A failure somebody paused on stays a failure, with the evidence to show for it.
|
|
47
|
+
|
|
48
|
+
A run driven through the debugger — `run_test` then `resume`, or the viewer's
|
|
49
|
+
Continue — reported `completed` after a failure it had paused on, wrote no
|
|
50
|
+
failure bundle and no `failure/` artifacts, and left `list_failures` with
|
|
51
|
+
nothing to show. Continuing past a failure is how it gets inspected; it was
|
|
52
|
+
never meant to retract it. The verdict is now decided where the run's own
|
|
53
|
+
events are seen, so the journal, `runtime.json` and the reply agree, and a
|
|
54
|
+
debug run leaves the same evidence a plain `unotest-web e2e` run does.
|
|
55
|
+
|
|
56
|
+
`abort_runtime` (and Stop, and SIGTERM) now also ends the run in
|
|
57
|
+
`runtime.json`, not only in `steps.jsonl`: the control file used to keep
|
|
58
|
+
saying `paused-step` about a run that was over, so anything reading it rather
|
|
59
|
+
than the journal saw a pause that never ended.
|
|
60
|
+
|
|
61
|
+
- 047ca68: A `step.soft(...)` failure no longer stops a debug run — or goes missing from it.
|
|
62
|
+
|
|
63
|
+
Under a debugger (`run_test`, or `e2e --debug`), every soft failure raised
|
|
64
|
+
the debug wheel: the run stopped on each one and an agent had to `resume`
|
|
65
|
+
its way through them. Worse, it was then lost — a paused failure is consumed
|
|
66
|
+
where it paused, so it never reached the `step.soft` envelope that records
|
|
67
|
+
it, the envelope closed as if the block had passed, and a run with three
|
|
68
|
+
soft failures could finish green with none of them listed.
|
|
69
|
+
|
|
70
|
+
Pausing is now for a failure that ENDS the run. A failure under any
|
|
71
|
+
enclosing soft step is recorded and stepped over exactly as it is on the
|
|
72
|
+
command line, and `runtime.json` — which the `run_test` reply is built from —
|
|
73
|
+
carries every soft failure of the run, not just the last stop.
|
|
74
|
+
|
|
75
|
+
A hard failure still pauses: that is what the debugger is for.
|
|
76
|
+
|
|
77
|
+
The same holds on mobile: a `step.soft(...)` failure no longer stops a run
|
|
78
|
+
under the debugger, and every soft failure of a run is now part of its
|
|
79
|
+
runtime state instead of being lost at the pause.
|
|
80
|
+
|
|
81
|
+
- Updated dependencies [8aa0f30]
|
|
82
|
+
- Updated dependencies [047ca68]
|
|
83
|
+
- Updated dependencies [047ca68]
|
|
84
|
+
- @unotest/protocol@0.32.0
|
|
85
|
+
- @unotest/dsl@0.32.0
|
|
86
|
+
|
|
3
87
|
## [0.31.0] - 2026-09-04
|
|
4
88
|
|
|
5
89
|
### 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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -688,7 +715,7 @@ import {
|
|
|
688
715
|
writeFile as writeFile6
|
|
689
716
|
} from "fs/promises";
|
|
690
717
|
import { dirname as dirname3, join as join8 } from "path";
|
|
691
|
-
async function createExclusive(path, contents) {
|
|
718
|
+
async function createExclusive(path, contents, now) {
|
|
692
719
|
let handle;
|
|
693
720
|
try {
|
|
694
721
|
handle = await open2(path, "wx");
|
|
@@ -698,27 +725,33 @@ async function createExclusive(path, contents) {
|
|
|
698
725
|
}
|
|
699
726
|
try {
|
|
700
727
|
await handle.writeFile(contents, "utf8");
|
|
728
|
+
await handle.utimes(...stampOf(now));
|
|
701
729
|
} finally {
|
|
702
730
|
await handle.close();
|
|
703
731
|
}
|
|
704
732
|
return true;
|
|
705
733
|
}
|
|
706
734
|
__name(createExclusive, "createExclusive");
|
|
707
|
-
async function writeFileAtomic(path, contents, tmpToken) {
|
|
735
|
+
async function writeFileAtomic(path, contents, tmpToken, now) {
|
|
708
736
|
const tmp = join8(dirname3(path), `.tmp-${tmpToken}`);
|
|
709
737
|
await writeFile6(tmp, contents, "utf8");
|
|
738
|
+
await utimes2(tmp, ...stampOf(now));
|
|
710
739
|
await rename4(tmp, path);
|
|
711
740
|
}
|
|
712
741
|
__name(writeFileAtomic, "writeFileAtomic");
|
|
713
742
|
async function touchFile(path, now) {
|
|
714
|
-
const seconds = now / 1e3;
|
|
715
743
|
try {
|
|
716
|
-
await utimes2(path,
|
|
744
|
+
await utimes2(path, ...stampOf(now));
|
|
717
745
|
} catch (e) {
|
|
718
746
|
if (!isErrno(e, "ENOENT")) throw e;
|
|
719
747
|
}
|
|
720
748
|
}
|
|
721
749
|
__name(touchFile, "touchFile");
|
|
750
|
+
function stampOf(now) {
|
|
751
|
+
const seconds = now / 1e3;
|
|
752
|
+
return [seconds, seconds];
|
|
753
|
+
}
|
|
754
|
+
__name(stampOf, "stampOf");
|
|
722
755
|
async function fileAgeMs(path, now) {
|
|
723
756
|
try {
|
|
724
757
|
const s = await stat(path);
|
|
@@ -847,7 +880,8 @@ var SlotPool = class {
|
|
|
847
880
|
for (let n = 0; n < this.opts.total && taken.length < count; n++) {
|
|
848
881
|
await keepAlive?.();
|
|
849
882
|
const name = `${SLOT_PREFIX}${n}`;
|
|
850
|
-
|
|
883
|
+
const path = join9(this.opts.dir, name);
|
|
884
|
+
if (await createExclusive(path, body, this.opts.now())) {
|
|
851
885
|
taken.push(name);
|
|
852
886
|
}
|
|
853
887
|
}
|
|
@@ -888,7 +922,7 @@ var SlotPool = class {
|
|
|
888
922
|
});
|
|
889
923
|
const token = this.opts.token();
|
|
890
924
|
for (let attempt = 0; attempt < mutex.attempts; attempt++) {
|
|
891
|
-
if (await createExclusive(mutex.path, token)) {
|
|
925
|
+
if (await createExclusive(mutex.path, token, this.opts.now())) {
|
|
892
926
|
try {
|
|
893
927
|
return await body(() => touchFile(mutex.path, this.opts.now()));
|
|
894
928
|
} finally {
|
|
@@ -1022,7 +1056,8 @@ var FsRunQueue = class {
|
|
|
1022
1056
|
await writeFileAtomic(
|
|
1023
1057
|
join10(this.ticketsDir, name),
|
|
1024
1058
|
JSON.stringify(ticket),
|
|
1025
|
-
`${name}-${this.random()}
|
|
1059
|
+
`${name}-${this.random()}`,
|
|
1060
|
+
createdAt
|
|
1026
1061
|
);
|
|
1027
1062
|
} catch (e) {
|
|
1028
1063
|
throw new QueueUnavailableError(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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/dsl": "^0.33.0",
|
|
33
|
+
"@unotest/protocol": "^0.33.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.10.0",
|