@jterrazz/test 18.0.3 → 18.1.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/config.js +10 -4
- package/dist/in-place.global-setup.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -6
- package/dist/services.js +1 -1
- package/dist/shared-appium.adapter.js +1 -1
- package/dist/shared-artifacts.js +3 -1
- package/dist/shared-index.services.d.ts +1 -1
- package/dist/shared-ledger.js +1 -1
- package/dist/shared-playwright.adapter.js +1 -1
- package/dist/shared-serve.js +179 -99
- package/dist/shared-session-ledger.js +1 -1
- package/dist/vitest.d.ts +4 -1
- package/dist/vitest.js +10 -2
- package/docs/guides/cli-specs.md +1 -1
- package/docs/guides/lint-specs.md +4 -0
- package/docs/reference/assertions.md +1 -1
- package/docs/reference/services.md +1 -1
- package/docs/start/set-up-a-project.md +9 -8
- package/package.json +2 -2
package/dist/config.js
CHANGED
|
@@ -19,10 +19,16 @@ var standard_default = defineStandard({ test: {
|
|
|
19
19
|
"mobile",
|
|
20
20
|
"website"
|
|
21
21
|
],
|
|
22
|
-
projects: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
projects: {
|
|
23
|
+
lint: {
|
|
24
|
+
isolate: false,
|
|
25
|
+
reason: "a lint spec lays its own fixture tree and runs the linters as processes of their own, so a worker runs its files in one module graph"
|
|
26
|
+
},
|
|
27
|
+
unit: {
|
|
28
|
+
isolate: false,
|
|
29
|
+
reason: "a module test leaves no state behind in a module, so a worker runs its files in one module graph"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
26
32
|
} });
|
|
27
33
|
//#endregion
|
|
28
34
|
//#region lint/kit/project/loaded-packages.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as writePendingRewrites } from "./shared-in-place.js";
|
|
2
|
-
import {
|
|
2
|
+
import { d as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
|
|
3
3
|
import { t as setupEntry } from "./shared-setup-entry.js";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
package/dist/index.d.ts
CHANGED
|
@@ -227,7 +227,7 @@ declare class CliResult extends BaseResult {
|
|
|
227
227
|
get filesystem(): FilesystemAccessor;
|
|
228
228
|
/** The requests the binary sent the declared backend during the run, in order, each `{ body, headers, method, url }`, the body parsed when JSON; empty without a backend. */
|
|
229
229
|
get requests(): SentRequest[];
|
|
230
|
-
/** A container the command spawned, by its name-label; `.exists` is `false` when none carries it. */
|
|
230
|
+
/** A container the command spawned, by its name-label; `.exists` is `false` when none carries it, and two carrying it are refused. */
|
|
231
231
|
container(name: string): ContainerAccessor;
|
|
232
232
|
/** All captured container IDs. Triggers the same lazy query as `container()`. */
|
|
233
233
|
get containerIds(): string[];
|
package/dist/index.js
CHANGED
|
@@ -572,19 +572,25 @@ var CliResult = class extends BaseResult {
|
|
|
572
572
|
headers: { ...request.headers }
|
|
573
573
|
}));
|
|
574
574
|
}
|
|
575
|
-
/** A container the command spawned, by its name-label; `.exists` is `false` when none carries it. */
|
|
575
|
+
/** A container the command spawned, by its name-label; `.exists` is `false` when none carries it, and two carrying it are refused. */
|
|
576
576
|
container(name) {
|
|
577
|
-
const captured = this.capturedContainers("container").get(name);
|
|
577
|
+
const [captured, ...others] = this.capturedContainers("container").get(name) ?? [];
|
|
578
578
|
if (!captured) return containerAt(null, null, this.testDir, this.transform);
|
|
579
|
+
if (others.length > 0) throw new Error(refusal$1({
|
|
580
|
+
fix: "give each container of the run its own name-label value, or read them by id: `result.containerIds`, then `docker(id)`",
|
|
581
|
+
found: `CliResult.container('${name}'), which ${String(others.length + 1)} containers of the run carry: ${[captured, ...others].map(described).join(", ")}`,
|
|
582
|
+
read: "docs/guides/cli-specs.md#docker-aware-mode",
|
|
583
|
+
why: "A name reads one container, and two carrying it leave which one unsaid."
|
|
584
|
+
}));
|
|
579
585
|
return containerAt(captured.id, captured.inspect, this.testDir, this.transform);
|
|
580
586
|
}
|
|
581
587
|
/** All captured container IDs. Triggers the same lazy query as `container()`. */
|
|
582
588
|
get containerIds() {
|
|
583
|
-
return [...this.capturedContainers("containerIds").values()].map((c) => c.id);
|
|
589
|
+
return [...this.capturedContainers("containerIds").values()].flat().map((c) => c.id);
|
|
584
590
|
}
|
|
585
591
|
async [Symbol.asyncDispose]() {
|
|
586
592
|
if (!this.dockerConfig || this.testRunId === void 0) return;
|
|
587
|
-
const ids = this.containersCache === null ? this.containers.labelled(this.dockerConfig.testRunLabel, this.testRunId) : [...this.containersCache.values()].map((c) => c.id);
|
|
593
|
+
const ids = this.containersCache === null ? this.containers.labelled(this.dockerConfig.testRunLabel, this.testRunId) : [...this.containersCache.values()].flat().map((c) => c.id);
|
|
588
594
|
this.containers.remove(ids);
|
|
589
595
|
}
|
|
590
596
|
capturedContainers(member) {
|
|
@@ -606,15 +612,19 @@ var CliResult = class extends BaseResult {
|
|
|
606
612
|
}
|
|
607
613
|
const nameValue = labelsOf(inspect)[nameLabel];
|
|
608
614
|
const key = typeof nameValue === "string" && nameValue.length > 0 ? nameValue : id;
|
|
609
|
-
map.set(key, {
|
|
615
|
+
map.set(key, [...map.get(key) ?? [], {
|
|
610
616
|
id,
|
|
611
617
|
inspect
|
|
612
|
-
});
|
|
618
|
+
}]);
|
|
613
619
|
}
|
|
614
620
|
this.containersCache = map;
|
|
615
621
|
return map;
|
|
616
622
|
}
|
|
617
623
|
};
|
|
624
|
+
function described({ id, inspect }) {
|
|
625
|
+
const name = fieldOf(inspect, "Name");
|
|
626
|
+
return typeof name === "string" ? `${id} (${name.slice(1)})` : id;
|
|
627
|
+
}
|
|
618
628
|
function labelsOf(inspect) {
|
|
619
629
|
const labels = fieldOf(fieldOf(inspect, "Config", "config"), "Labels", "labels");
|
|
620
630
|
return isRecord(labels) ? labels : {};
|
package/dist/services.js
CHANGED
|
@@ -3,7 +3,7 @@ import { n as refusal, t as invariant } from "./shared-refusal.js";
|
|
|
3
3
|
import { t as pause } from "./shared-pause.js";
|
|
4
4
|
import { n as processService, t as ProcessHandle } from "./shared-process.adapter.js";
|
|
5
5
|
import { n as requireBuiltPeer, t as loadPeer } from "./shared-peer.js";
|
|
6
|
-
import {
|
|
6
|
+
import { u as SQLITE_TEMPLATE_DIR } from "./shared-artifacts.js";
|
|
7
7
|
import { t as SERVICE_IMAGES } from "./shared-service-images.js";
|
|
8
8
|
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as refusal } from "./shared-refusal.js";
|
|
2
2
|
import { n as untilDeadline } from "./shared-pause.js";
|
|
3
|
-
import {
|
|
3
|
+
import { f as evidenceFile } from "./shared-artifacts.js";
|
|
4
4
|
import { n as SERVER_CALL_TIMEOUT_MS } from "./shared-session-ledger.js";
|
|
5
5
|
import { c as scopeChain } from "./shared-landmarks.js";
|
|
6
6
|
import { a as ambiguityError, c as asWrittenUnscoped, i as shortened, s as asWritten, t as scenarioFailure } from "./shared-scenario-failure.js";
|
package/dist/shared-artifacts.js
CHANGED
|
@@ -6,6 +6,8 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
const ARTIFACTS_DIR = ".artifacts";
|
|
7
7
|
const VITEST_ARTIFACTS_DIR = `${ARTIFACTS_DIR}/vitest`;
|
|
8
8
|
const COVERAGE_DIR = `${VITEST_ARTIFACTS_DIR}/coverage`;
|
|
9
|
+
/** Vitest's transformed-module cache, which it would share at the workspace root's `node_modules/.vitest-cache` between the members of a monorepo running side by side. */
|
|
10
|
+
const MODULE_CACHE_DIR = `${VITEST_ARTIFACTS_DIR}/modules`;
|
|
9
11
|
const SQLITE_TEMPLATE_DIR = `${VITEST_ARTIFACTS_DIR}/sqlite`;
|
|
10
12
|
/** Attachments and Browser Mode failure screenshots, which Vitest 5 would put in `.vitest/`. */
|
|
11
13
|
const ATTACHMENTS_DIR = `${VITEST_ARTIFACTS_DIR}/attachments`;
|
|
@@ -29,6 +31,6 @@ const REPORTER_OUTPUT_FILES = {
|
|
|
29
31
|
junit: `${VITEST_ARTIFACTS_DIR}/junit/output.xml`
|
|
30
32
|
};
|
|
31
33
|
//#endregion
|
|
32
|
-
export { EVIDENCE_DIR as a,
|
|
34
|
+
export { EVIDENCE_DIR as a, REPORTER_OUTPUT_FILES as c, VITEST_ARTIFACTS_DIR as d, evidenceFile as f, COVERAGE_DIR as i, SCREENSHOTS_DIR as l, ATTACHMENTS_DIR as n, MODULE_CACHE_DIR as o, BLOB_DIR as r, NODE_COMPILE_CACHE_DIR as s, ARTIFACTS_DIR as t, SQLITE_TEMPLATE_DIR as u };
|
|
33
35
|
|
|
34
36
|
//# sourceMappingURL=shared-artifacts.js.map
|
|
@@ -290,7 +290,7 @@ type ProcessOptions = {
|
|
|
290
290
|
}>) => Record<string, string>) | Record<string, string>;
|
|
291
291
|
/** Fixed port. Default: an OS-assigned free port, injected as `PORT`. */
|
|
292
292
|
port?: number;
|
|
293
|
-
/** Readiness: a path polled until any HTTP status answers (`PORT` injected), or a regex over the output whose one group captures the port. */
|
|
293
|
+
/** Readiness: a path polled until any HTTP status answers from the command's own process group (`PORT` injected), or a regex over the output whose one group captures the port. */
|
|
294
294
|
ready?: RegExp | string;
|
|
295
295
|
/** Readiness budget in milliseconds. Default 30 000. */
|
|
296
296
|
timeout?: number;
|
package/dist/shared-ledger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as isRecord } from "./shared-json-shape.js";
|
|
2
|
-
import {
|
|
2
|
+
import { d as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
|
|
3
3
|
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as refusal } from "./shared-refusal.js";
|
|
2
2
|
import { n as untilDeadline } from "./shared-pause.js";
|
|
3
|
-
import {
|
|
3
|
+
import { f as evidenceFile } from "./shared-artifacts.js";
|
|
4
4
|
import { c as scopeChain } from "./shared-landmarks.js";
|
|
5
5
|
import { n as CANDIDATE_LIMITS, o as ambiguousTargetError, r as candidatesOf, s as asWritten, t as scenarioFailure } from "./shared-scenario-failure.js";
|
|
6
6
|
//#region src/specifications/website/browser/page-scripts.ts
|
package/dist/shared-serve.js
CHANGED
|
@@ -3,7 +3,8 @@ import { n as untilDeadline } from "./shared-pause.js";
|
|
|
3
3
|
import "./shared-listened-port.js";
|
|
4
4
|
import "node:module";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
-
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { execFile, execSync, spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { readFile, readdir, readlink } from "node:fs/promises";
|
|
7
8
|
import { randomInt } from "node:crypto";
|
|
8
9
|
import { Socket, connect } from "node:net";
|
|
9
10
|
import { once } from "node:events";
|
|
@@ -20,6 +21,169 @@ var __exportAll = (all, no_symbols) => {
|
|
|
20
21
|
return target;
|
|
21
22
|
};
|
|
22
23
|
//#endregion
|
|
24
|
+
//#region src/services/process/process-group.ts
|
|
25
|
+
const KILL_GRACE_MS = 2e3;
|
|
26
|
+
/** outside: Windows has no process groups to signal through a negative pid. */
|
|
27
|
+
const SIGNALS_GROUPS = process.platform !== "win32";
|
|
28
|
+
const GROUP_POLL_MS = 1e3;
|
|
29
|
+
const GROUP_SETTLE_MS = 50;
|
|
30
|
+
const STRAYS_SOURCE = `
|
|
31
|
+
const { execFileSync } = require('node:child_process');
|
|
32
|
+
const strays = JSON.parse(process.argv[1] ?? '[]');
|
|
33
|
+
const listed = (column) => {
|
|
34
|
+
let listing = '';
|
|
35
|
+
try { listing = execFileSync('ps', ['-A', '-o', 'pid=,' + column + '='], { encoding: 'utf8', maxBuffer: 67108864 }); } catch {}
|
|
36
|
+
return new Map(listing.split('\\n').flatMap((line) => {
|
|
37
|
+
const found = /^\\s*(\\d+)\\s(.*)$/u.exec(line);
|
|
38
|
+
return found === null ? [] : [[Number(found[1]), found[2]]];
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
const names = (path, program) => path === program || path.endsWith('/' + program);
|
|
42
|
+
const strayPids = () => {
|
|
43
|
+
const commands = listed('command');
|
|
44
|
+
return [...listed('comm')]
|
|
45
|
+
.filter(([pid, executable]) => {
|
|
46
|
+
const command = commands.get(pid) ?? '';
|
|
47
|
+
return pid !== process.pid && strays.some((stray) =>
|
|
48
|
+
(names(executable, stray.program) || names(command.split(' ')[0], stray.program)) && stray.holds.every((word) => command.includes(word)));
|
|
49
|
+
})
|
|
50
|
+
.map(([pid]) => pid);
|
|
51
|
+
};
|
|
52
|
+
const signalStrays = (signal) => {
|
|
53
|
+
for (const pid of strays.length === 0 ? [] : strayPids()) {
|
|
54
|
+
try { process.kill(-pid, signal); } catch {}
|
|
55
|
+
try { process.kill(pid, signal); } catch {}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
`;
|
|
59
|
+
/** Its arguments are the strays, then the group. With strays to reap, it outlives the group: a stray outlives the group that spawned it. */
|
|
60
|
+
const WATCHDOG = `${STRAYS_SOURCE}
|
|
61
|
+
const group = -Number(process.argv[2]);
|
|
62
|
+
const gone = () => { try { process.kill(group, 0); return false; } catch { return true; } };
|
|
63
|
+
const end = () => {
|
|
64
|
+
try { process.kill(group, 'SIGTERM'); } catch { if (strays.length === 0) process.exit(0); }
|
|
65
|
+
signalStrays('SIGTERM');
|
|
66
|
+
setTimeout(() => { try { process.kill(group, 'SIGKILL'); } catch {} signalStrays('SIGKILL'); process.exit(0); }, ${KILL_GRACE_MS});
|
|
67
|
+
};
|
|
68
|
+
process.stdin.on('end', end);
|
|
69
|
+
process.stdin.on('error', end);
|
|
70
|
+
process.stdin.resume();
|
|
71
|
+
setInterval(() => { if (strays.length === 0 && gone()) process.exit(0); }, ${GROUP_POLL_MS});
|
|
72
|
+
`;
|
|
73
|
+
/** Its argument is the strays: each gets SIGTERM, and SIGKILL when it outlives the grace. */
|
|
74
|
+
const REAPER = `${STRAYS_SOURCE}
|
|
75
|
+
signalStrays('SIGTERM');
|
|
76
|
+
const deadline = Date.now() + ${KILL_GRACE_MS};
|
|
77
|
+
const settle = () => {
|
|
78
|
+
if (strayPids().length === 0) return;
|
|
79
|
+
if (Date.now() >= deadline) { signalStrays('SIGKILL'); return; }
|
|
80
|
+
setTimeout(settle, ${GROUP_SETTLE_MS});
|
|
81
|
+
};
|
|
82
|
+
settle();
|
|
83
|
+
`;
|
|
84
|
+
function groupRunning(group) {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(group, 0);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function groupEnded(group, withinMs) {
|
|
93
|
+
return await untilDeadline(() => !groupRunning(group) || void 0, {
|
|
94
|
+
interval: GROUP_SETTLE_MS,
|
|
95
|
+
timeout: withinMs
|
|
96
|
+
}) === true;
|
|
97
|
+
}
|
|
98
|
+
/** Resolves once the child is reaped, at once for one reaped already. */
|
|
99
|
+
async function reaped(child) {
|
|
100
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
101
|
+
await once(child, "exit");
|
|
102
|
+
}
|
|
103
|
+
/** The child's whole group where groups are signalled, the child alone where they are not or the group is gone; nothing for a child that never started. */
|
|
104
|
+
function signalGroup(child, signal = "SIGTERM") {
|
|
105
|
+
if (child.pid === void 0) return false;
|
|
106
|
+
if (SIGNALS_GROUPS) try {
|
|
107
|
+
process.kill(-child.pid, signal);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {}
|
|
110
|
+
return child.kill(signal);
|
|
111
|
+
}
|
|
112
|
+
async function endGroup(child) {
|
|
113
|
+
if (child.pid === void 0) return;
|
|
114
|
+
const leaderReaped = reaped(child);
|
|
115
|
+
signalGroup(child, "SIGTERM");
|
|
116
|
+
if (!await groupEnded(-child.pid, 2e3)) signalGroup(child, "SIGKILL");
|
|
117
|
+
await leaderReaped;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/services/process/listener-owner.ts
|
|
121
|
+
function ownsFromLsof(output, group) {
|
|
122
|
+
return owns(output.split("\n").filter((line) => line.startsWith("g")).map((line) => line.slice(1)), /* @__PURE__ */ new Set([String(group)]));
|
|
123
|
+
}
|
|
124
|
+
/** Owned when a listener is the group's and none it can see is outside it. */
|
|
125
|
+
function owns(listeners, owned) {
|
|
126
|
+
return listeners.length > 0 && listeners.every((listener) => owned.has(listener));
|
|
127
|
+
}
|
|
128
|
+
const LISTEN_STATE = "0A";
|
|
129
|
+
function listeningInodes(table, port) {
|
|
130
|
+
return table.split("\n").map((row) => row.trim().split(/\s+/u)).filter(([, local, , state]) => {
|
|
131
|
+
const [, hex] = String(local).split(":");
|
|
132
|
+
return state === LISTEN_STATE && Number.parseInt(String(hex), 16) === port;
|
|
133
|
+
}).map((fields) => String(fields[9]));
|
|
134
|
+
}
|
|
135
|
+
function processGroupOf(stat) {
|
|
136
|
+
return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[2]);
|
|
137
|
+
}
|
|
138
|
+
function socketInode(link) {
|
|
139
|
+
return /^socket:\[(?<inode>\d+)\]$/u.exec(link)?.groups?.inode;
|
|
140
|
+
}
|
|
141
|
+
const NODE_PROC = {
|
|
142
|
+
entries: async (path) => await readdir(path).catch(() => []),
|
|
143
|
+
link: async (path) => await readlink(path).catch(() => ""),
|
|
144
|
+
text: async (path) => await readFile(path, "utf8").catch(() => "")
|
|
145
|
+
};
|
|
146
|
+
async function groupInodes(group, proc) {
|
|
147
|
+
const pids = await proc.entries("/proc");
|
|
148
|
+
const owned = /* @__PURE__ */ new Set();
|
|
149
|
+
for (const pid of pids) {
|
|
150
|
+
if (processGroupOf(await proc.text(`/proc/${pid}/stat`)) !== group) continue;
|
|
151
|
+
for (const fd of await proc.entries(`/proc/${pid}/fd`)) {
|
|
152
|
+
const inode = socketInode(await proc.link(`/proc/${pid}/fd/${fd}`));
|
|
153
|
+
if (inode !== void 0) owned.add(inode);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return owned;
|
|
157
|
+
}
|
|
158
|
+
/** Linux: the listening sockets of `/proc/net/tcp` and `tcp6` on the port, against the sockets the group's processes hold open. */
|
|
159
|
+
async function ownsOnLinux(port, group, proc = NODE_PROC) {
|
|
160
|
+
return owns((await Promise.all(["/proc/net/tcp", "/proc/net/tcp6"].map(proc.text))).flatMap((table) => listeningInodes(table, port)), await groupInodes(group, proc));
|
|
161
|
+
}
|
|
162
|
+
/** macOS: the groups `lsof` names for the port's listeners; undefined where `program` cannot run. */
|
|
163
|
+
async function ownsOnDarwin(port, group, program = "lsof") {
|
|
164
|
+
const listed = await new Promise((resolve) => {
|
|
165
|
+
execFile(program, [
|
|
166
|
+
"-nP",
|
|
167
|
+
"-a",
|
|
168
|
+
`-iTCP:${String(port)}`,
|
|
169
|
+
"-sTCP:LISTEN",
|
|
170
|
+
"-Fpg"
|
|
171
|
+
], (error, stdout) => {
|
|
172
|
+
resolve(error === null || error.code === 1 ? stdout : void 0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
return listed === void 0 ? void 0 : ownsFromLsof(listed, group);
|
|
176
|
+
}
|
|
177
|
+
/** Whether `port` is listened on by the process group `group` alone, as far as this process can see; undefined where the platform cannot tell. */
|
|
178
|
+
async function groupOwnsPort(port, group, platform = process.platform) {
|
|
179
|
+
if (platform === "linux") return await ownsOnLinux(port, group);
|
|
180
|
+
if (platform === "darwin") return await ownsOnDarwin(port, group);
|
|
181
|
+
}
|
|
182
|
+
async function childOwnsPort(port, child) {
|
|
183
|
+
const group = SIGNALS_GROUPS ? child?.pid : void 0;
|
|
184
|
+
return group === void 0 ? void 0 : await groupOwnsPort(port, group);
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
23
187
|
//#region src/services/process/listening.ts
|
|
24
188
|
async function firstAnswering(urls) {
|
|
25
189
|
return (await Promise.all(urls.map(async (url) => await answers(url) ? url : void 0))).find((url) => url !== void 0);
|
|
@@ -81,6 +245,13 @@ function lessonOf(reason, output, port) {
|
|
|
81
245
|
why: "A process is handed a free port unless `port` pins one, and a port another process holds is not free."
|
|
82
246
|
};
|
|
83
247
|
}
|
|
248
|
+
function foreignAnswer(port) {
|
|
249
|
+
return {
|
|
250
|
+
fix: "stop the process holding that port, or announce the port with a `ready` pattern if the server runs outside the command (a container, a daemon)",
|
|
251
|
+
found: `port ${String(port)} answered, and a process the command did not start listens on it`,
|
|
252
|
+
why: "A process is ready once its own server answers, and an answer from another process on its port is not one."
|
|
253
|
+
};
|
|
254
|
+
}
|
|
84
255
|
async function refuseHeldPort(port, specification, command) {
|
|
85
256
|
if (port === void 0) return;
|
|
86
257
|
const host = await holdingHost(port);
|
|
@@ -156,102 +327,6 @@ async function reservePort(pinned, pick = draw) {
|
|
|
156
327
|
};
|
|
157
328
|
}
|
|
158
329
|
//#endregion
|
|
159
|
-
//#region src/services/process/process-group.ts
|
|
160
|
-
const KILL_GRACE_MS = 2e3;
|
|
161
|
-
/** outside: Windows has no process groups to signal through a negative pid. */
|
|
162
|
-
const SIGNALS_GROUPS = process.platform !== "win32";
|
|
163
|
-
const GROUP_POLL_MS = 1e3;
|
|
164
|
-
const GROUP_SETTLE_MS = 50;
|
|
165
|
-
const STRAYS_SOURCE = `
|
|
166
|
-
const { execFileSync } = require('node:child_process');
|
|
167
|
-
const strays = JSON.parse(process.argv[1] ?? '[]');
|
|
168
|
-
const listed = (column) => {
|
|
169
|
-
let listing = '';
|
|
170
|
-
try { listing = execFileSync('ps', ['-A', '-o', 'pid=,' + column + '='], { encoding: 'utf8', maxBuffer: 67108864 }); } catch {}
|
|
171
|
-
return new Map(listing.split('\\n').flatMap((line) => {
|
|
172
|
-
const found = /^\\s*(\\d+)\\s(.*)$/u.exec(line);
|
|
173
|
-
return found === null ? [] : [[Number(found[1]), found[2]]];
|
|
174
|
-
}));
|
|
175
|
-
};
|
|
176
|
-
const names = (path, program) => path === program || path.endsWith('/' + program);
|
|
177
|
-
const strayPids = () => {
|
|
178
|
-
const commands = listed('command');
|
|
179
|
-
return [...listed('comm')]
|
|
180
|
-
.filter(([pid, executable]) => {
|
|
181
|
-
const command = commands.get(pid) ?? '';
|
|
182
|
-
return pid !== process.pid && strays.some((stray) =>
|
|
183
|
-
(names(executable, stray.program) || names(command.split(' ')[0], stray.program)) && stray.holds.every((word) => command.includes(word)));
|
|
184
|
-
})
|
|
185
|
-
.map(([pid]) => pid);
|
|
186
|
-
};
|
|
187
|
-
const signalStrays = (signal) => {
|
|
188
|
-
for (const pid of strays.length === 0 ? [] : strayPids()) {
|
|
189
|
-
try { process.kill(-pid, signal); } catch {}
|
|
190
|
-
try { process.kill(pid, signal); } catch {}
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
`;
|
|
194
|
-
/** Its arguments are the strays, then the group. With strays to reap, it outlives the group: a stray outlives the group that spawned it. */
|
|
195
|
-
const WATCHDOG = `${STRAYS_SOURCE}
|
|
196
|
-
const group = -Number(process.argv[2]);
|
|
197
|
-
const gone = () => { try { process.kill(group, 0); return false; } catch { return true; } };
|
|
198
|
-
const end = () => {
|
|
199
|
-
try { process.kill(group, 'SIGTERM'); } catch { if (strays.length === 0) process.exit(0); }
|
|
200
|
-
signalStrays('SIGTERM');
|
|
201
|
-
setTimeout(() => { try { process.kill(group, 'SIGKILL'); } catch {} signalStrays('SIGKILL'); process.exit(0); }, ${KILL_GRACE_MS});
|
|
202
|
-
};
|
|
203
|
-
process.stdin.on('end', end);
|
|
204
|
-
process.stdin.on('error', end);
|
|
205
|
-
process.stdin.resume();
|
|
206
|
-
setInterval(() => { if (strays.length === 0 && gone()) process.exit(0); }, ${GROUP_POLL_MS});
|
|
207
|
-
`;
|
|
208
|
-
/** Its argument is the strays: each gets SIGTERM, and SIGKILL when it outlives the grace. */
|
|
209
|
-
const REAPER = `${STRAYS_SOURCE}
|
|
210
|
-
signalStrays('SIGTERM');
|
|
211
|
-
const deadline = Date.now() + ${KILL_GRACE_MS};
|
|
212
|
-
const settle = () => {
|
|
213
|
-
if (strayPids().length === 0) return;
|
|
214
|
-
if (Date.now() >= deadline) { signalStrays('SIGKILL'); return; }
|
|
215
|
-
setTimeout(settle, ${GROUP_SETTLE_MS});
|
|
216
|
-
};
|
|
217
|
-
settle();
|
|
218
|
-
`;
|
|
219
|
-
function groupRunning(group) {
|
|
220
|
-
try {
|
|
221
|
-
process.kill(group, 0);
|
|
222
|
-
return true;
|
|
223
|
-
} catch {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async function groupEnded(group, withinMs) {
|
|
228
|
-
return await untilDeadline(() => !groupRunning(group) || void 0, {
|
|
229
|
-
interval: GROUP_SETTLE_MS,
|
|
230
|
-
timeout: withinMs
|
|
231
|
-
}) === true;
|
|
232
|
-
}
|
|
233
|
-
/** Resolves once the child is reaped, at once for one reaped already. */
|
|
234
|
-
async function reaped(child) {
|
|
235
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
236
|
-
await once(child, "exit");
|
|
237
|
-
}
|
|
238
|
-
/** The child's whole group where groups are signalled, the child alone where they are not or the group is gone; nothing for a child that never started. */
|
|
239
|
-
function signalGroup(child, signal = "SIGTERM") {
|
|
240
|
-
if (child.pid === void 0) return false;
|
|
241
|
-
if (SIGNALS_GROUPS) try {
|
|
242
|
-
process.kill(-child.pid, signal);
|
|
243
|
-
return true;
|
|
244
|
-
} catch {}
|
|
245
|
-
return child.kill(signal);
|
|
246
|
-
}
|
|
247
|
-
async function endGroup(child) {
|
|
248
|
-
if (child.pid === void 0) return;
|
|
249
|
-
const leaderReaped = reaped(child);
|
|
250
|
-
signalGroup(child, "SIGTERM");
|
|
251
|
-
if (!await groupEnded(-child.pid, 2e3)) signalGroup(child, "SIGKILL");
|
|
252
|
-
await leaderReaped;
|
|
253
|
-
}
|
|
254
|
-
//#endregion
|
|
255
330
|
//#region src/services/process/serve.ts
|
|
256
331
|
var serve_exports = /* @__PURE__ */ __exportAll({
|
|
257
332
|
ServedProcess: () => ServedProcess,
|
|
@@ -299,7 +374,7 @@ function runToCompletion(command, env) {
|
|
|
299
374
|
stdio: "pipe"
|
|
300
375
|
});
|
|
301
376
|
}
|
|
302
|
-
/** Any HTTP status counts as ready,
|
|
377
|
+
/** Any HTTP status counts as ready once a process of the child's own group listens on the port, and none outside it: the port it is handed is reserved for it until `stop()`, so no other served process is handed it, and an unrelated server that took it meanwhile is refused rather than read as ready. The child runs in its own process group so `stop()` kills a `next start` grandchild too, and a watchdog kills that group when the run dies before `stop()`; both reap the strays it declares, the processes it spawns outside its group. */
|
|
303
378
|
var ServedProcess = class {
|
|
304
379
|
port = null;
|
|
305
380
|
child = null;
|
|
@@ -411,11 +486,16 @@ var ServedProcess = class {
|
|
|
411
486
|
async awaitAnswer(port) {
|
|
412
487
|
const baseUrls = [`http://127.0.0.1:${port}`, `http://[::1]:${port}`];
|
|
413
488
|
const path = typeof this.options.ready === "string" ? this.options.ready : "/";
|
|
414
|
-
const seen = await untilDeadline(async () =>
|
|
489
|
+
const seen = await untilDeadline(async () => {
|
|
490
|
+
if (this.exited) return "exited";
|
|
491
|
+
const answered = await firstAnswering(baseUrls.map((baseUrl) => `${baseUrl}${path}`));
|
|
492
|
+
return (answered === void 0 || await childOwnsPort(port, this.child)) === false ? "foreign" : answered;
|
|
493
|
+
}, {
|
|
415
494
|
interval: READY_POLL_INTERVAL_MS,
|
|
416
495
|
timeout: this.timeout()
|
|
417
496
|
});
|
|
418
497
|
if (seen === "exited") return null;
|
|
498
|
+
if (seen === "foreign") return await this.refuse(foreignAnswer(port));
|
|
419
499
|
if (seen === void 0) return await this.refuse({
|
|
420
500
|
fix: "correct `ready` to a path the server answers, have it listen on `PORT`, or raise `timeout`",
|
|
421
501
|
found: `the server did not answer on ${baseUrls.map((baseUrl) => `${baseUrl}${path}`).join(" or ")} within ${this.timeout()}ms, whatever address it printed above`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as ServedProcess } from "./shared-serve.js";
|
|
2
2
|
import { t as isRecord } from "./shared-json-shape.js";
|
|
3
3
|
import { n as refusal } from "./shared-refusal.js";
|
|
4
|
-
import {
|
|
4
|
+
import { d as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { execFile } from "node:child_process";
|
package/dist/vitest.d.ts
CHANGED
|
@@ -98,7 +98,10 @@ export declare function job(options?: JobProjectOptions): TestProjectInlineConfi
|
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region src/specifications/lint/lint.project.d.ts
|
|
100
100
|
/** `lint()`: the rules a project ships, run by the real linters over fixture trees. */
|
|
101
|
-
type LintProjectOptions = SpecificationProjectOptions
|
|
101
|
+
type LintProjectOptions = SpecificationProjectOptions & {
|
|
102
|
+
/** `false` shares one module graph per worker (`isolate: false`); for specs that keep no state in a module. */
|
|
103
|
+
isolate?: boolean;
|
|
104
|
+
};
|
|
102
105
|
export declare function lint(options?: LintProjectOptions): TestProjectInlineConfiguration;
|
|
103
106
|
//#endregion
|
|
104
107
|
//#region src/specifications/mobile/mobile.project.d.ts
|
package/dist/vitest.js
CHANGED
|
@@ -3,7 +3,7 @@ import { t as isRecord } from "./shared-json-shape.js";
|
|
|
3
3
|
import { n as refusal$1, t as invariant } from "./shared-refusal.js";
|
|
4
4
|
import { i as renderText, n as literate, o as resolveSpecPath } from "./shared-literate-plugin.js";
|
|
5
5
|
import { u as shouldUpdateSnapshots } from "./shared-ledger.js";
|
|
6
|
-
import { c as
|
|
6
|
+
import { c as REPORTER_OUTPUT_FILES, d as VITEST_ARTIFACTS_DIR, i as COVERAGE_DIR, l as SCREENSHOTS_DIR, n as ATTACHMENTS_DIR, o as MODULE_CACHE_DIR, r as BLOB_DIR, s as NODE_COMPILE_CACHE_DIR, t as ARTIFACTS_DIR } from "./shared-artifacts.js";
|
|
7
7
|
import { t as derivedProjects } from "./shared-derived-projects.js";
|
|
8
8
|
import { t as TEST_SECTION } from "./shared-test-section.js";
|
|
9
9
|
import { n as servicesSetup } from "./shared-services.global-setup.js";
|
|
@@ -378,7 +378,14 @@ function job(options = {}) {
|
|
|
378
378
|
//#endregion
|
|
379
379
|
//#region src/specifications/lint/lint.project.ts
|
|
380
380
|
function lint(options = {}) {
|
|
381
|
-
|
|
381
|
+
const project = specificationProject("lint", options);
|
|
382
|
+
return options.isolate === void 0 ? project : {
|
|
383
|
+
...project,
|
|
384
|
+
test: {
|
|
385
|
+
...project.test,
|
|
386
|
+
isolate: options.isolate
|
|
387
|
+
}
|
|
388
|
+
};
|
|
382
389
|
}
|
|
383
390
|
//#endregion
|
|
384
391
|
//#region src/specifications/mobile/mobile.project.ts
|
|
@@ -442,6 +449,7 @@ function rootDefaults() {
|
|
|
442
449
|
return mergeConfig(projectDefaults(), { test: {
|
|
443
450
|
coverage: COVERAGE,
|
|
444
451
|
env: { NODE_COMPILE_CACHE: resolve(NODE_COMPILE_CACHE_DIR) },
|
|
452
|
+
fsModuleCachePath: MODULE_CACHE_DIR,
|
|
445
453
|
maxWorkers: slotsFor(resolve("."), "vitest"),
|
|
446
454
|
outputFile: reporterOutputFiles(process.argv)
|
|
447
455
|
} });
|
package/docs/guides/cli-specs.md
CHANGED
|
@@ -536,7 +536,7 @@ test('destroy removes the container created in an earlier run', async () => {
|
|
|
536
536
|
});
|
|
537
537
|
```
|
|
538
538
|
|
|
539
|
-
Container accessor surface: `exists`, `running`/`toBeRunning`, `status`, `id`, `file(path)` (content read inside the container), `exec(cmd)` (returns stdout/stderr), `stdout`/`stderr` (container logs). Looking up an absent container returns `exists: false` instead of throwing. `result.containerIds` lists every container captured for the run.
|
|
539
|
+
Container accessor surface: `exists`, `running`/`toBeRunning`, `status`, `id`, `file(path)` (content read inside the container), `exec(cmd)` (returns stdout/stderr), `stdout`/`stderr` (container logs). Looking up an absent container returns `exists: false` instead of throwing, and a name two containers of the run carry is refused ([Assertions § Container subjects](../reference/assertions.md#container-subjects-docker-aware-cli)). `result.containerIds` lists every container captured for the run.
|
|
540
540
|
|
|
541
541
|
The runner handle also destructures to `{ cli, cleanup, docker }`. The `docker(containerId)` reader is the escape hatch for reading an **arbitrary** container by raw id (e.g. one a follow-up command referenced): it lazily runs `docker inspect` and returns the same `ContainerAccessor` type, so `await expect(docker(id)).toBeRunning()` and the sync reads work identically. Unlike `result.container(name)` it does not need `nameLabel`: it looks up by id directly; an unknown id yields `exists: false`.
|
|
542
542
|
|
|
@@ -134,6 +134,10 @@ A preset's own `oxlint` (its plugin, its rulebook) is not run by `presets`: the
|
|
|
134
134
|
|
|
135
135
|
A linter that fails and reports no diagnostic is refused in four parts rather than read as a clean tree: a plugin that does not load, a config oxlint cannot build, a checker that cannot start.
|
|
136
136
|
|
|
137
|
+
### One module graph per worker
|
|
138
|
+
|
|
139
|
+
The lint project runs a worker's files in one module graph (`isolate: false`), as the standard hands it: each spec lays its own fixture tree and runs the linters as processes of their own, so a file leaves nothing in a module for the next one, and the worker, the framework and the kit load once per worker instead of once per file. A project whose lint specs keep state in a module (a cache, a counter, a stub set at load) states `isolate: true` for its `lint` project in `jterrazz.config.ts`, with its reason.
|
|
140
|
+
|
|
137
141
|
### A rule pack: a nested Vitest and a reporter fixture
|
|
138
142
|
|
|
139
143
|
A package that ships lint rules for other projects proves them by running the real binary on a tree: the spec starts a nested `vitest` over a fixture project (`.fixture(directory(...))`) with a reporter fixture that prints one line per finding, and compares that output whole. A rule alone is `ruleTester()`.
|
|
@@ -224,7 +224,7 @@ Same semantics as `result.directory(...)`, rooted at the cwd itself. `.files()`
|
|
|
224
224
|
|
|
225
225
|
## Container subjects (docker-aware cli)
|
|
226
226
|
|
|
227
|
-
Available when the runner declares `docker: { envVar, nameLabel, testRunLabel }`; results must be bound with `await using` (rule `docker-exec-bound-with-await-using`). `result.container(name)` looks a container up by its `nameLabel` value, lazily: no Docker call happens until you touch it. Absent containers return `exists: false` instead of throwing.
|
|
227
|
+
Available when the runner declares `docker: { envVar, nameLabel, testRunLabel }`; results must be bound with `await using` (rule `docker-exec-bound-with-await-using`). `result.container(name)` looks a container up by its `nameLabel` value, lazily: no Docker call happens until you touch it. Absent containers return `exists: false` instead of throwing; a name two containers of the run carry is refused, naming each candidate by id and docker name, since reading either would be a guess.
|
|
228
228
|
|
|
229
229
|
The runner handle itself also exposes a `docker(containerId)` reader (returned by `specification.api()` and `specification.cli()`, not `specification.job()`): given a raw container id, it lazily runs `docker inspect` and returns the **same** `ContainerAccessor` type, so it works with `await expect(spec.docker(id)).toBeRunning()` and every read accessor below. An unknown id yields an accessor with `exists: false` rather than throwing. The app of an api spec runs in the test process, but it can still start containers through the Docker daemon, as an API that deploys or provisions something does: the response hands back an id, and `docker(id)` reads the container the request left behind.
|
|
230
230
|
|
|
@@ -84,7 +84,7 @@ export const { cleanup, website } = await specification.website({
|
|
|
84
84
|
|
|
85
85
|
**A port is one process's at a time.** The port handed as `PORT` is drawn between 10000 and 32767, below the range every OS hands unasked ports from (a `listen(0)`, a stub's port, an outgoing connection), so no other socket is handed it by chance while the command starts. It is reserved from the draw until the process stops, against every served process of the machine whatever run started it, so files running in parallel never share one; a draw that finds no free port in 64 tries is refused. A `port` two processes pin at once is held by the first, and the second is refused before its command runs.
|
|
86
86
|
|
|
87
|
-
**Any status the `ready` path answers is the server's.**
|
|
87
|
+
**Any status the `ready` path answers is the server's, once the server is proved to be the one answering.** On Linux and macOS an answer counts only while every listener of the port this run can see is in the command's process group: a process outside it (one started by hand on that very number while the command starts, a server the command detached, a container's port forward) is refused at once, naming the port, never read as ready. A server that rightly runs outside the group announces its port instead, with a `ready` pattern. Elsewhere the answer is taken as the server's.
|
|
88
88
|
|
|
89
89
|
**Its URL is known once it is ready.** `url` read at declaration time is refused: a process started beside others reads theirs from the started services record: `env: (services) => ...`, `server: (services) => ...`.
|
|
90
90
|
|
|
@@ -108,7 +108,7 @@ There is one helper per kind (`unit`, `integration`, `component`, `website`, `mo
|
|
|
108
108
|
| `timeout` | Raise or lower the 30 s default for this project alone: each test's and each hook's, so the `afterAll` that stops what a slow test started has the same budget |
|
|
109
109
|
| `serial` | Run the project's files one at a time (`fileParallelism: false`), when they share one server, one file, one app |
|
|
110
110
|
|
|
111
|
-
`unit()` and `component()` are a pair: `unit()` collects `**/*.test.ts` outside `specs/` and leaves out `**/*.test.tsx`, which `component()` collects. So the suffix beside a file decides which project runs it. `unit()` also takes `roots` when the modules are not at the repository root, and `isolate: false` to run a worker's files in one module graph, only for a project whose tests leave no state behind in a module, and the default `vitestConfig` hands it. `component()` takes the options of the app it renders (`vite`, `wrap`, `viewport`, `clock`, `timezone`, `locale`, `root`), described in [Component specs](../guides/component-specs.md#the-constructor-there-is-none); `mobile()` takes the simulator the run shares ([Mobile specs](../guides/mobile-specs.md#one-session-per-run)).
|
|
111
|
+
`unit()` and `component()` are a pair: `unit()` collects `**/*.test.ts` outside `specs/` and leaves out `**/*.test.tsx`, which `component()` collects. So the suffix beside a file decides which project runs it. `unit()` also takes `roots` when the modules are not at the repository root, and `isolate: false` to run a worker's files in one module graph, only for a project whose tests leave no state behind in a module, and the default `vitestConfig` hands it. `lint()` takes `isolate` too, and the default `vitestConfig` hands it `false` as well, since a lint spec lays its own tree and runs the linters as processes of their own ([Lint specs § One module graph per worker](../guides/lint-specs.md#one-module-graph-per-worker)); `mobile()` keeps every file isolated. `component()` takes the options of the app it renders (`vite`, `wrap`, `viewport`, `clock`, `timezone`, `locale`, `root`), described in [Component specs](../guides/component-specs.md#the-constructor-there-is-none); `mobile()` takes the simulator the run shares ([Mobile specs](../guides/mobile-specs.md#one-session-per-run)).
|
|
112
112
|
|
|
113
113
|
Every project runs in one group, on the workers the preset sizes ([below](#what-the-preset-sets)): a `website` file takes a worker as a node file does, and the `component` pages run beside them in their one Chromium, as many at once as the run has workers. A project whose files run one at a time (`serial`, and `mobile()` unless it states `serial: false`) runs after that group, in a group of its own, so a simulator never shares its machine.
|
|
114
114
|
|
|
@@ -172,13 +172,14 @@ export default await oxlintConfig(import.meta.url);
|
|
|
172
172
|
|
|
173
173
|
**In the project, only under `.artifacts/`**, one folder per tool, one line in `.gitignore`, one `rm -rf .artifacts` for a clean slate:
|
|
174
174
|
|
|
175
|
-
| Path | Written by
|
|
176
|
-
| ------------------------------------------------ |
|
|
177
|
-
| `.artifacts/vitest/` | vite's transform cache, vitest's reports
|
|
178
|
-
| `.artifacts/vitest/coverage/` | the coverage provider
|
|
179
|
-
| `.artifacts/vitest/
|
|
180
|
-
| `.artifacts/vitest/
|
|
181
|
-
| `.artifacts/
|
|
175
|
+
| Path | Written by | Lifetime |
|
|
176
|
+
| ------------------------------------------------ | --------------------------------------------------------------- | ------------------------------- |
|
|
177
|
+
| `.artifacts/vitest/` | vite's transform cache, vitest's reports | Reused across runs |
|
|
178
|
+
| `.artifacts/vitest/coverage/` | the coverage provider | Rewritten per coverage run |
|
|
179
|
+
| `.artifacts/vitest/modules/` | vitest's transformed-module cache, one per member of a monorepo | Reused across runs |
|
|
180
|
+
| `.artifacts/vitest/sqlite/template-<key>.sqlite` | `sqlite()`'s schema template | Reused until the schema changes |
|
|
181
|
+
| `.artifacts/vitest/evidence/<pid>-.../` | the screenshot of a website or mobile scenario that failed | Kept until the next run starts |
|
|
182
|
+
| `.artifacts/node-compile-cache/` | Node, for the workers and every process they spawn | Reused across runs |
|
|
182
183
|
|
|
183
184
|
**Everything else is scratch, in one temp folder per run.** Before the first worker starts, the preset creates `jterrazz-test-runs-<uid>/<pid>-...` in the OS temp directory and points `TMPDIR`, `TMP` and `TEMP` at it, so every temp folder a worker, a spawned child or a tool it runs makes lands inside. That holds each CLI spec's working directory, the per-worker database copies and a browser's profile. The run removes the folder when it ends, and the next run removes the folder of any run that died first. A CLI spec's working directory goes sooner, once its test has finished, except under `test.concurrent`, where it waits for the run.
|
|
184
185
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/test",
|
|
3
|
-
"version": "18.0
|
|
3
|
+
"version": "18.1.0",
|
|
4
4
|
"description": "Declarative testing framework for HTTP APIs, CLIs, background jobs, websites, mobile apps, rendered components and the rules a project ships: one entry point, real infrastructure and a real browser, golden-file assertions, plus an oxlint convention plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-testing",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@hono/node-server": "^2.1.1",
|
|
95
|
-
"@jterrazz/typescript": "^11.
|
|
95
|
+
"@jterrazz/typescript": "^11.2.0",
|
|
96
96
|
"@types/better-sqlite3": "^9.6.0",
|
|
97
97
|
"@types/node": "^26.6.2",
|
|
98
98
|
"@types/pg": "^8.23.1",
|