@c9up/helix 0.1.3 → 0.1.5
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/cli/coverage/aggregate.d.ts +24 -0
- package/dist/cli/coverage/aggregate.d.ts.map +1 -0
- package/dist/cli/coverage/aggregate.js +215 -0
- package/dist/cli/coverage/aggregate.js.map +1 -0
- package/dist/cli/coverage/collect.d.ts +13 -0
- package/dist/cli/coverage/collect.d.ts.map +1 -0
- package/dist/cli/coverage/collect.js +66 -0
- package/dist/cli/coverage/collect.js.map +1 -0
- package/dist/cli/coverage/diff/base.d.ts +17 -0
- package/dist/cli/coverage/diff/base.d.ts.map +1 -0
- package/dist/cli/coverage/diff/base.js +44 -0
- package/dist/cli/coverage/diff/base.js.map +1 -0
- package/dist/cli/coverage/diff/index.d.ts +25 -0
- package/dist/cli/coverage/diff/index.d.ts.map +1 -0
- package/dist/cli/coverage/diff/index.js +119 -0
- package/dist/cli/coverage/diff/index.js.map +1 -0
- package/dist/cli/coverage/diff/overlay.d.ts +19 -0
- package/dist/cli/coverage/diff/overlay.d.ts.map +1 -0
- package/dist/cli/coverage/diff/overlay.js +58 -0
- package/dist/cli/coverage/diff/overlay.js.map +1 -0
- package/dist/cli/coverage/diff/parse.d.ts +27 -0
- package/dist/cli/coverage/diff/parse.d.ts.map +1 -0
- package/dist/cli/coverage/diff/parse.js +105 -0
- package/dist/cli/coverage/diff/parse.js.map +1 -0
- package/dist/cli/coverage/diff/reporters.d.ts +12 -0
- package/dist/cli/coverage/diff/reporters.d.ts.map +1 -0
- package/dist/cli/coverage/diff/reporters.js +59 -0
- package/dist/cli/coverage/diff/reporters.js.map +1 -0
- package/dist/cli/coverage/diff/types.d.ts +46 -0
- package/dist/cli/coverage/diff/types.d.ts.map +1 -0
- package/dist/cli/coverage/diff/types.js +15 -0
- package/dist/cli/coverage/diff/types.js.map +1 -0
- package/dist/cli/coverage/filter.d.ts +13 -0
- package/dist/cli/coverage/filter.d.ts.map +1 -0
- package/dist/cli/coverage/filter.js +60 -0
- package/dist/cli/coverage/filter.js.map +1 -0
- package/dist/cli/coverage/glob.d.ts +4 -0
- package/dist/cli/coverage/glob.d.ts.map +1 -0
- package/dist/cli/coverage/glob.js +0 -0
- package/dist/cli/coverage/glob.js.map +1 -0
- package/dist/cli/coverage/index.d.ts +40 -0
- package/dist/cli/coverage/index.d.ts.map +1 -0
- package/dist/cli/coverage/index.js +85 -0
- package/dist/cli/coverage/index.js.map +1 -0
- package/dist/cli/coverage/reporters/json.d.ts +13 -0
- package/dist/cli/coverage/reporters/json.d.ts.map +1 -0
- package/dist/cli/coverage/reporters/json.js +37 -0
- package/dist/cli/coverage/reporters/json.js.map +1 -0
- package/dist/cli/coverage/reporters/lcov.d.ts +13 -0
- package/dist/cli/coverage/reporters/lcov.d.ts.map +1 -0
- package/dist/cli/coverage/reporters/lcov.js +51 -0
- package/dist/cli/coverage/reporters/lcov.js.map +1 -0
- package/dist/cli/coverage/reporters/text.d.ts +8 -0
- package/dist/cli/coverage/reporters/text.d.ts.map +1 -0
- package/dist/cli/coverage/reporters/text.js +39 -0
- package/dist/cli/coverage/reporters/text.js.map +1 -0
- package/dist/cli/coverage/thresholds.d.ts +14 -0
- package/dist/cli/coverage/thresholds.d.ts.map +1 -0
- package/dist/cli/coverage/thresholds.js +58 -0
- package/dist/cli/coverage/thresholds.js.map +1 -0
- package/dist/cli/coverage/types.d.ts +118 -0
- package/dist/cli/coverage/types.d.ts.map +1 -0
- package/dist/cli/coverage/types.js +8 -0
- package/dist/cli/coverage/types.js.map +1 -0
- package/dist/cli/discover.d.ts +27 -0
- package/dist/cli/discover.d.ts.map +1 -0
- package/dist/cli/discover.js +142 -0
- package/dist/cli/discover.js.map +1 -0
- package/dist/cli/native.d.ts +43 -0
- package/dist/cli/native.d.ts.map +1 -0
- package/dist/cli/native.js +64 -0
- package/dist/cli/native.js.map +1 -0
- package/dist/cli/pool.d.ts +55 -0
- package/dist/cli/pool.d.ts.map +1 -0
- package/dist/cli/pool.js +404 -0
- package/dist/cli/pool.js.map +1 -0
- package/dist/cli/reporter.d.ts +51 -0
- package/dist/cli/reporter.d.ts.map +1 -0
- package/dist/cli/reporter.js +124 -0
- package/dist/cli/reporter.js.map +1 -0
- package/dist/cli/run.d.ts +61 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +302 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/summary.d.ts +18 -0
- package/dist/cli/summary.d.ts.map +1 -0
- package/dist/cli/summary.js +20 -0
- package/dist/cli/summary.js.map +1 -0
- package/dist/cli/watch/loop.d.ts +26 -0
- package/dist/cli/watch/loop.d.ts.map +1 -0
- package/dist/cli/watch/loop.js +133 -0
- package/dist/cli/watch/loop.js.map +1 -0
- package/dist/cli/watch/types.d.ts +22 -0
- package/dist/cli/watch/types.d.ts.map +1 -0
- package/dist/cli/watch/types.js +6 -0
- package/dist/cli/watch/types.js.map +1 -0
- package/dist/cli/watch/watcher.d.ts +25 -0
- package/dist/cli/watch/watcher.d.ts.map +1 -0
- package/dist/cli/watch/watcher.js +122 -0
- package/dist/cli/watch/watcher.js.map +1 -0
- package/dist/container/index.d.ts +7 -0
- package/dist/container/index.d.ts.map +1 -0
- package/dist/container/index.js +6 -0
- package/dist/container/index.js.map +1 -0
- package/{src/container/override.ts → dist/container/override.d.ts} +6 -46
- package/dist/container/override.d.ts.map +1 -0
- package/dist/container/override.js +67 -0
- package/dist/container/override.js.map +1 -0
- package/{src/container/spy.ts → dist/container/spy.d.ts} +2 -6
- package/dist/container/spy.d.ts.map +1 -0
- package/dist/container/spy.js +23 -0
- package/dist/container/spy.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +2 -25
- package/dist/index.js.map +1 -0
- package/dist/runtime/assertion-error.d.ts +20 -0
- package/dist/runtime/assertion-error.d.ts.map +1 -0
- package/dist/runtime/assertion-error.js +23 -0
- package/dist/runtime/assertion-error.js.map +1 -0
- package/dist/runtime/cli-worker.d.ts +25 -0
- package/dist/runtime/cli-worker.d.ts.map +1 -0
- package/dist/runtime/cli-worker.js +128 -0
- package/dist/runtime/cli-worker.js.map +1 -0
- package/dist/runtime/equals.d.ts +25 -0
- package/dist/runtime/equals.d.ts.map +1 -0
- package/dist/runtime/equals.js +343 -0
- package/dist/runtime/equals.js.map +1 -0
- package/dist/runtime/expect.d.ts +32 -0
- package/dist/runtime/expect.d.ts.map +1 -0
- package/dist/runtime/expect.js +121 -0
- package/dist/runtime/expect.js.map +1 -0
- package/{src/runtime/index.ts → dist/runtime/index.d.ts} +5 -27
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/index.js.map +1 -0
- package/{src/runtime/lifecycle.ts → dist/runtime/lifecycle.d.ts} +2 -8
- package/dist/runtime/lifecycle.d.ts.map +1 -0
- package/dist/runtime/lifecycle.js +10 -0
- package/dist/runtime/lifecycle.js.map +1 -0
- package/dist/runtime/matchers.d.ts +42 -0
- package/dist/runtime/matchers.d.ts.map +1 -0
- package/dist/runtime/matchers.js +375 -0
- package/dist/runtime/matchers.js.map +1 -0
- package/dist/runtime/run.d.ts +55 -0
- package/dist/runtime/run.d.ts.map +1 -0
- package/dist/runtime/run.js +456 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/suite.d.ts +91 -0
- package/dist/runtime/suite.d.ts.map +1 -0
- package/dist/runtime/suite.js +206 -0
- package/dist/runtime/suite.js.map +1 -0
- package/dist/runtime/test-context.d.ts +25 -0
- package/dist/runtime/test-context.d.ts.map +1 -0
- package/dist/runtime/test-context.js +52 -0
- package/dist/runtime/test-context.js.map +1 -0
- package/dist/runtime/vi/fake-timers.d.ts +48 -0
- package/dist/runtime/vi/fake-timers.d.ts.map +1 -0
- package/dist/runtime/vi/fake-timers.js +317 -0
- package/dist/runtime/vi/fake-timers.js.map +1 -0
- package/dist/runtime/vi/index.d.ts +70 -0
- package/dist/runtime/vi/index.d.ts.map +1 -0
- package/dist/runtime/vi/index.js +209 -0
- package/dist/runtime/vi/index.js.map +1 -0
- package/dist/runtime/vi/spy.d.ts +54 -0
- package/dist/runtime/vi/spy.d.ts.map +1 -0
- package/dist/runtime/vi/spy.js +159 -0
- package/dist/runtime/vi/spy.js.map +1 -0
- package/dist/runtime/vi/spyOn.d.ts +28 -0
- package/dist/runtime/vi/spyOn.d.ts.map +1 -0
- package/dist/runtime/vi/spyOn.js +121 -0
- package/dist/runtime/vi/spyOn.js.map +1 -0
- package/dist/runtime/vi/system-time.d.ts +30 -0
- package/dist/runtime/vi/system-time.d.ts.map +1 -0
- package/dist/runtime/vi/system-time.js +100 -0
- package/dist/runtime/vi/system-time.js.map +1 -0
- package/dist/runtime/worker.d.ts +26 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +195 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/time/freeze.d.ts +80 -0
- package/dist/time/freeze.d.ts.map +1 -0
- package/dist/time/freeze.js +184 -0
- package/dist/time/freeze.js.map +1 -0
- package/{src/time/index.ts → dist/time/index.d.ts} +1 -1
- package/dist/time/index.d.ts.map +1 -0
- package/dist/time/index.js +15 -0
- package/dist/time/index.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -2
- package/src/cli/coverage/aggregate.ts +0 -231
- package/src/cli/coverage/collect.ts +0 -63
- package/src/cli/coverage/diff/base.ts +0 -46
- package/src/cli/coverage/diff/index.ts +0 -160
- package/src/cli/coverage/diff/overlay.ts +0 -62
- package/src/cli/coverage/diff/parse.ts +0 -121
- package/src/cli/coverage/diff/reporters.ts +0 -82
- package/src/cli/coverage/diff/types.ts +0 -46
- package/src/cli/coverage/filter.ts +0 -71
- package/src/cli/coverage/glob.ts +0 -0
- package/src/cli/coverage/index.ts +0 -126
- package/src/cli/coverage/reporters/json.ts +0 -40
- package/src/cli/coverage/reporters/lcov.ts +0 -54
- package/src/cli/coverage/reporters/text.ts +0 -48
- package/src/cli/coverage/thresholds.ts +0 -73
- package/src/cli/coverage/types.ts +0 -93
- package/src/cli/discover.ts +0 -174
- package/src/cli/native.ts +0 -104
- package/src/cli/pool.ts +0 -486
- package/src/cli/reporter.ts +0 -155
- package/src/cli/run.ts +0 -440
- package/src/cli/summary.ts +0 -42
- package/src/cli/watch/loop.ts +0 -159
- package/src/cli/watch/types.ts +0 -22
- package/src/cli/watch/watcher.ts +0 -145
- package/src/container/index.ts +0 -16
- package/src/runtime/assertion-error.ts +0 -38
- package/src/runtime/cli-worker.ts +0 -140
- package/src/runtime/equals.ts +0 -400
- package/src/runtime/expect.ts +0 -173
- package/src/runtime/matchers.ts +0 -452
- package/src/runtime/run.ts +0 -573
- package/src/runtime/suite.ts +0 -310
- package/src/runtime/test-context.ts +0 -59
- package/src/runtime/vi/fake-timers.ts +0 -410
- package/src/runtime/vi/index.ts +0 -254
- package/src/runtime/vi/spy.ts +0 -224
- package/src/runtime/vi/spyOn.ts +0 -155
- package/src/runtime/vi/system-time.ts +0 -121
- package/src/runtime/worker.ts +0 -239
- package/src/time/freeze.ts +0 -229
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time freeze / travel — ergonomic surface over `vi.setSystemTime`
|
|
3
|
+
* with per-test auto-restore.
|
|
4
|
+
*
|
|
5
|
+
* **Helix is agnostic — no cross-package imports.** This module
|
|
6
|
+
* never imports `@c9up/chronos`. The clock pin happens entirely
|
|
7
|
+
* through `vi.setSystemTime` (which shims `globalThis.Date`); any
|
|
8
|
+
* code in user-land that calls `new Date()` — chronos `DateTime`
|
|
9
|
+
* included — picks up the frozen epoch automatically.
|
|
10
|
+
*
|
|
11
|
+
* Calendar arithmetic for `month` / `year` uses `Date.UTC` math
|
|
12
|
+
* locally so we don't take a runtime dependency on chronos.
|
|
13
|
+
*
|
|
14
|
+
* **Concurrency note.** `vi.setSystemTime` writes to the `vi`
|
|
15
|
+
* context that wraps the current test FILE (`withViContext`), not
|
|
16
|
+
* the per-test frame (`withTestContext`). Two `withTestContext`
|
|
17
|
+
* frames running in parallel inside the same file (e.g. via
|
|
18
|
+
* `Promise.all([withTestContext(A), withTestContext(B)])`) share
|
|
19
|
+
* one Date shim — A's freeze leaks into B. The helix runtime
|
|
20
|
+
* executes tests strictly sequentially (`runSuite`'s for-await
|
|
21
|
+
* loop), so this is not reachable through the documented test
|
|
22
|
+
* path. Callers who manually compose `withTestContext` in parallel
|
|
23
|
+
* inside a single file must serialise their freezes themselves.
|
|
24
|
+
*/
|
|
25
|
+
import { inTestContext, registerTestCleanup } from "../runtime/test-context.js";
|
|
26
|
+
import { vi } from "../runtime/vi/index.js";
|
|
27
|
+
/** JS `Date` represents instants from `-8.64e15` to `+8.64e15` ms.
|
|
28
|
+
* Anything beyond becomes "Invalid Date" — refuse early so callers
|
|
29
|
+
* see a clear error instead of `setSystemTime(NaN)` downstream. */
|
|
30
|
+
const JS_DATE_MAX = 8_640_000_000_000_000;
|
|
31
|
+
/**
|
|
32
|
+
* Pin `Date.now()` / `new Date()` (and anything that reads through
|
|
33
|
+
* them — e.g. `DateTime.now()`) to the given moment. Auto-restored
|
|
34
|
+
* at end-of-test via the helix test frame, so a forgotten
|
|
35
|
+
* `unfreeze()` does not leak into the next test.
|
|
36
|
+
*
|
|
37
|
+
* Throws when called outside a test frame — a queued cleanup would
|
|
38
|
+
* never fire there, so the freeze would leak across tests silently.
|
|
39
|
+
*/
|
|
40
|
+
export function freeze(input) {
|
|
41
|
+
requireTestContext("freeze");
|
|
42
|
+
const ms = inputToMs(input);
|
|
43
|
+
vi.setSystemTime(ms);
|
|
44
|
+
registerTestCleanup(() => {
|
|
45
|
+
vi.useRealSystemTime();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Advance the clock by `amount` × `unit`. If the clock isn't frozen
|
|
50
|
+
* yet, freezes it relative to the real `Date.now()` first, then
|
|
51
|
+
* applies the delta — so a one-shot `time.travel(7, 'day')` works
|
|
52
|
+
* without an explicit `freeze()`.
|
|
53
|
+
*
|
|
54
|
+
* Always queues a cleanup so the auto-restore path is uniform —
|
|
55
|
+
* cleanups are idempotent (each calls `vi.useRealSystemTime`),
|
|
56
|
+
* accumulating closures across many `travel` calls in one test is
|
|
57
|
+
* intentional and cheap.
|
|
58
|
+
*
|
|
59
|
+
* Throws when called outside a test frame, same reason as `freeze`.
|
|
60
|
+
*/
|
|
61
|
+
export function travel(amount, unit = "ms") {
|
|
62
|
+
requireTestContext("travel");
|
|
63
|
+
if (!Number.isFinite(amount)) {
|
|
64
|
+
throw new Error(`helix.time.travel: amount must be finite, got ${amount}.`);
|
|
65
|
+
}
|
|
66
|
+
if ((unit === "month" || unit === "year") && !Number.isInteger(amount)) {
|
|
67
|
+
throw new Error(`helix.time.travel: '${unit}' requires an integer amount, got ${amount}. Calendar units don't compose cleanly with fractions.`);
|
|
68
|
+
}
|
|
69
|
+
const base = vi.getMockedSystemTime() ?? Date.now();
|
|
70
|
+
const next = applyDelta(base, amount, unit);
|
|
71
|
+
requireInDateRange(next, "travel");
|
|
72
|
+
vi.setSystemTime(next);
|
|
73
|
+
// Always queue — `vi.useRealSystemTime` is idempotent so duplicate
|
|
74
|
+
// cleanups across `freeze + travel` are harmless. Always-queue
|
|
75
|
+
// guards against the "user pinned via raw `vi.setSystemTime`,
|
|
76
|
+
// then called `travel`" trap where the heuristic would skip.
|
|
77
|
+
registerTestCleanup(() => {
|
|
78
|
+
vi.useRealSystemTime();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Travel TO a specific moment. Equivalent to `freeze(target)` —
|
|
83
|
+
* exists as an explicit verb so "travel to 2026" reads better than
|
|
84
|
+
* "freeze at 2026" in tests.
|
|
85
|
+
*/
|
|
86
|
+
export function travelTo(target) {
|
|
87
|
+
freeze(target);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Restore the real system clock immediately. Idempotent — safe to
|
|
91
|
+
* call when not currently frozen. Auto-restore still fires at
|
|
92
|
+
* end-of-test, so calling `unfreeze` early is purely a
|
|
93
|
+
* test-readability choice.
|
|
94
|
+
*/
|
|
95
|
+
export function unfreeze() {
|
|
96
|
+
vi.useRealSystemTime();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read the currently frozen epoch, or `null` when running on real
|
|
100
|
+
* time. Useful for tests that compose `freeze` with their own clock
|
|
101
|
+
* arithmetic.
|
|
102
|
+
*/
|
|
103
|
+
export function frozenAt() {
|
|
104
|
+
return vi.getMockedSystemTime();
|
|
105
|
+
}
|
|
106
|
+
function requireTestContext(verb) {
|
|
107
|
+
if (!inTestContext()) {
|
|
108
|
+
throw new Error(`helix.time.${verb}: must be called inside a test (no active test frame). Calls from top-level setup leak across tests — use vi.setSystemTime directly with manual cleanup if that is what you want.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function inputToMs(input) {
|
|
112
|
+
let ms;
|
|
113
|
+
if (typeof input === "number") {
|
|
114
|
+
ms = input;
|
|
115
|
+
}
|
|
116
|
+
else if (input instanceof Date) {
|
|
117
|
+
ms = input.getTime();
|
|
118
|
+
}
|
|
119
|
+
else if (typeof input === "string") {
|
|
120
|
+
ms = Date.parse(input);
|
|
121
|
+
if (Number.isNaN(ms)) {
|
|
122
|
+
throw new Error(`helix.time: cannot parse '${input}' as a date — pass an ISO 8601 string, Date, number (epoch ms), or an object with toMillis().`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (typeof input === "object" &&
|
|
126
|
+
input !== null &&
|
|
127
|
+
typeof input.toMillis === "function") {
|
|
128
|
+
const result = input.toMillis();
|
|
129
|
+
if (typeof result !== "number") {
|
|
130
|
+
throw new Error(`helix.time: input.toMillis() must return a number, got ${typeof result}.`);
|
|
131
|
+
}
|
|
132
|
+
ms = result;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
throw new Error(`helix.time: unsupported input type ${typeof input}. Use string / Date / number / { toMillis() }.`);
|
|
136
|
+
}
|
|
137
|
+
requireInDateRange(ms, "freeze");
|
|
138
|
+
return ms;
|
|
139
|
+
}
|
|
140
|
+
function requireInDateRange(ms, verb) {
|
|
141
|
+
if (!Number.isFinite(ms)) {
|
|
142
|
+
throw new Error(`helix.time.${verb}: epoch must be finite, got ${ms}. (Did you pass an Invalid Date or a NaN?)`);
|
|
143
|
+
}
|
|
144
|
+
if (Math.abs(ms) > JS_DATE_MAX) {
|
|
145
|
+
throw new Error(`helix.time.${verb}: epoch ${ms} exceeds the JS Date range (±8.64e15 ms). The resulting clock would be 'Invalid Date'.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function applyDelta(base, amount, unit) {
|
|
149
|
+
switch (unit) {
|
|
150
|
+
case "ms":
|
|
151
|
+
return base + amount;
|
|
152
|
+
case "second":
|
|
153
|
+
return base + amount * 1_000;
|
|
154
|
+
case "minute":
|
|
155
|
+
return base + amount * 60_000;
|
|
156
|
+
case "hour":
|
|
157
|
+
return base + amount * 3_600_000;
|
|
158
|
+
case "day":
|
|
159
|
+
return base + amount * 86_400_000;
|
|
160
|
+
case "week":
|
|
161
|
+
return base + amount * 604_800_000;
|
|
162
|
+
case "month":
|
|
163
|
+
case "year": {
|
|
164
|
+
// UTC calendar math — JS `Date` mutators handle month
|
|
165
|
+
// overflow (e.g. month 13 → year+1 month 1) and clamp
|
|
166
|
+
// month-end days (e.g. Jan 31 + 1 month → Feb 28/29).
|
|
167
|
+
const d = new Date(base);
|
|
168
|
+
if (unit === "year") {
|
|
169
|
+
d.setUTCFullYear(d.getUTCFullYear() + amount);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
d.setUTCMonth(d.getUTCMonth() + amount);
|
|
173
|
+
}
|
|
174
|
+
return d.getTime();
|
|
175
|
+
}
|
|
176
|
+
default: {
|
|
177
|
+
// Defensive — TS narrows this branch to `never`, but a
|
|
178
|
+
// runtime cast (`unit as TimeUnit`) could land here.
|
|
179
|
+
const exhaustive = unit;
|
|
180
|
+
throw new Error(`helix.time.travel: unknown unit '${exhaustive}'.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=freeze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/time/freeze.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/time/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/helix/time` — time freeze / travel for tests.
|
|
3
|
+
*
|
|
4
|
+
* Pins `Date.now()`, `new Date()`, and `DateTime.now()` to a fake
|
|
5
|
+
* epoch. Auto-restored after each test via the per-test frame.
|
|
6
|
+
*
|
|
7
|
+
* import { time } from "@c9up/helix";
|
|
8
|
+
*
|
|
9
|
+
* time.freeze("2026-01-01");
|
|
10
|
+
* // ... test that depends on a fixed clock
|
|
11
|
+
* time.travel(7, "day");
|
|
12
|
+
* expect(DateTime.now().toISO()).toMatch(/^2026-01-08/);
|
|
13
|
+
*/
|
|
14
|
+
export { freeze, frozenAt, travel, travelTo, unfreeze } from "./freeze.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/time/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/helix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Helix — framework-agnostic test runtime for the Ream ecosystem (Vitest-compatible runner, spies, container overrides, time-travel). Per-package fakes live in each package's /testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"bin",
|
|
45
|
-
"src",
|
|
46
45
|
"dist",
|
|
46
|
+
"index.*.node",
|
|
47
47
|
"README.md",
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Aggregation: merge N raw coverage entries (possibly for the same file
|
|
3
|
-
* from different workers) into a single `CoverageSummary` with per-file
|
|
4
|
-
* and total line / function counts.
|
|
5
|
-
*
|
|
6
|
-
* Range merging strategy mirrors `v8-to-istanbul`:
|
|
7
|
-
* - Walk ranges OUTERMOST-first so inner ranges OVERWRITE the outer
|
|
8
|
-
* count for the lines they cover. A `count: 0` inner range carved
|
|
9
|
-
* out of a `count: 5` outer correctly marks those inner lines as
|
|
10
|
-
* uncovered, matching V8's block-coverage semantics. The previous
|
|
11
|
-
* `Math.max` strategy hid uncovered branches behind their outer
|
|
12
|
-
* function's hit count.
|
|
13
|
-
* - V8 emits BYTE offsets (UTF-8); we convert via Buffer.byteLength
|
|
14
|
-
* so non-ASCII source code maps lines correctly.
|
|
15
|
-
* - When two workers cover the same file:
|
|
16
|
-
* - if their `source` differs (cache-busting query loaded different
|
|
17
|
-
* content), we keep the longer source (best-effort) but emit a
|
|
18
|
-
* warning — offsets are only consistent within one source.
|
|
19
|
-
* - functions are deduplicated by `(name, startOffset)` so the
|
|
20
|
-
* lcov FNF count doesn't double.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type {
|
|
24
|
-
CoverageSummary,
|
|
25
|
-
FileSummary,
|
|
26
|
-
RawFileCoverage,
|
|
27
|
-
Totals,
|
|
28
|
-
V8Function,
|
|
29
|
-
} from "./types.js";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Build a function `byteOffset → 1-based line number` over `source`.
|
|
33
|
-
* V8 offsets are UTF-8 byte offsets, so we walk the string code-unit by
|
|
34
|
-
* code-unit and accumulate byte length to keep both indices in sync.
|
|
35
|
-
*
|
|
36
|
-
* Returns `[mapper, totalBytes]`. `totalBytes` is needed so callers can
|
|
37
|
-
* detect offsets pointing past EOF (corrupt input) and clamp.
|
|
38
|
-
*/
|
|
39
|
-
function buildOffsetToLine(
|
|
40
|
-
source: string,
|
|
41
|
-
): [(offset: number) => number, number] {
|
|
42
|
-
const lineStarts: number[] = [0];
|
|
43
|
-
let byteIndex = 0;
|
|
44
|
-
for (let i = 0; i < source.length; i += 1) {
|
|
45
|
-
const code = source.charCodeAt(i);
|
|
46
|
-
// UTF-16 surrogate pair → encodes a single 4-byte UTF-8 char.
|
|
47
|
-
if (code >= 0xd800 && code <= 0xdbff && i + 1 < source.length) {
|
|
48
|
-
const next = source.charCodeAt(i + 1);
|
|
49
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
50
|
-
byteIndex += 4;
|
|
51
|
-
i += 1;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
// Lone surrogate or BMP char — Buffer.byteLength of one code unit:
|
|
56
|
-
// < 0x80 → 1 byte
|
|
57
|
-
// < 0x800 → 2 bytes
|
|
58
|
-
// else → 3 bytes (BMP non-surrogate)
|
|
59
|
-
if (code < 0x80) {
|
|
60
|
-
byteIndex += 1;
|
|
61
|
-
if (code === 10 /* \n */) lineStarts.push(byteIndex);
|
|
62
|
-
} else if (code < 0x800) {
|
|
63
|
-
byteIndex += 2;
|
|
64
|
-
} else {
|
|
65
|
-
byteIndex += 3;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const totalBytes = byteIndex;
|
|
69
|
-
const mapper = (offset: number): number => {
|
|
70
|
-
const clamped = Math.max(0, Math.min(offset, totalBytes));
|
|
71
|
-
// Binary search for the largest lineStart <= clamped.
|
|
72
|
-
let lo = 0;
|
|
73
|
-
let hi = lineStarts.length - 1;
|
|
74
|
-
while (lo < hi) {
|
|
75
|
-
const mid = (lo + hi + 1) >>> 1;
|
|
76
|
-
if (lineStarts[mid] <= clamped) lo = mid;
|
|
77
|
-
else hi = mid - 1;
|
|
78
|
-
}
|
|
79
|
-
return lo + 1;
|
|
80
|
-
};
|
|
81
|
-
return [mapper, totalBytes];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function computeFileSummary(entry: RawFileCoverage): FileSummary {
|
|
85
|
-
const [offsetToLine] = buildOffsetToLine(entry.source);
|
|
86
|
-
const lineHitsMap = new Map<number, number>();
|
|
87
|
-
const functionHits: FileSummary["functionHits"] = [];
|
|
88
|
-
let fnCovered = 0;
|
|
89
|
-
// Apply ranges across ALL functions in one global pass, sorted by
|
|
90
|
-
// span DESC. The whole-file pseudo-function (largest span) lands
|
|
91
|
-
// first; inner functions and their inner ranges overwrite — so a
|
|
92
|
-
// never-called inner function correctly drops its lines back to
|
|
93
|
-
// count=0 even though the script itself executed at count=1.
|
|
94
|
-
const allRanges: Array<{
|
|
95
|
-
startOffset: number;
|
|
96
|
-
endOffset: number;
|
|
97
|
-
count: number;
|
|
98
|
-
}> = [];
|
|
99
|
-
for (const fn of entry.functions) {
|
|
100
|
-
for (const r of fn.ranges) {
|
|
101
|
-
if (r.endOffset > r.startOffset) allRanges.push(r);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
allRanges.sort((a, b) => {
|
|
105
|
-
const spanA = a.endOffset - a.startOffset;
|
|
106
|
-
const spanB = b.endOffset - b.startOffset;
|
|
107
|
-
if (spanA !== spanB) return spanB - spanA;
|
|
108
|
-
return a.startOffset - b.startOffset;
|
|
109
|
-
});
|
|
110
|
-
for (const r of allRanges) {
|
|
111
|
-
const startLine = offsetToLine(r.startOffset);
|
|
112
|
-
const endLine = offsetToLine(r.endOffset - 1);
|
|
113
|
-
for (let line = startLine; line <= endLine; line += 1) {
|
|
114
|
-
lineHitsMap.set(line, r.count);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for (const fn of entry.functions) {
|
|
118
|
-
const firstRange = fn.ranges[0];
|
|
119
|
-
// Skip pseudo-functions / synthetic entries with no usable range so
|
|
120
|
-
// reports don't carry `FN:0,name` (lcov rejects line < 1).
|
|
121
|
-
if (!firstRange) continue;
|
|
122
|
-
const fnCount = firstRange.count;
|
|
123
|
-
const fnLine = offsetToLine(firstRange.startOffset);
|
|
124
|
-
if (fnCount > 0) fnCovered += 1;
|
|
125
|
-
functionHits.push({
|
|
126
|
-
name: fn.functionName || "(anonymous)",
|
|
127
|
-
line: Math.max(1, fnLine),
|
|
128
|
-
count: fnCount,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
const lineHits = Array.from(lineHitsMap.entries())
|
|
132
|
-
.map(([line, count]) => ({ line, count }))
|
|
133
|
-
.sort((a, b) => a.line - b.line);
|
|
134
|
-
const linesCovered = lineHits.filter((h) => h.count > 0).length;
|
|
135
|
-
const linesTotal = lineHits.length;
|
|
136
|
-
const fnTotal = functionHits.length;
|
|
137
|
-
return {
|
|
138
|
-
file: entry.file,
|
|
139
|
-
lines: { covered: linesCovered, total: linesTotal },
|
|
140
|
-
// Statements ≈ lines (V8 doesn't expose AST statement boundaries —
|
|
141
|
-
// matches Vitest's v8 provider).
|
|
142
|
-
statements: { covered: linesCovered, total: linesTotal },
|
|
143
|
-
functions: { covered: fnCovered, total: fnTotal },
|
|
144
|
-
// Branches deferred; report 0/0 so reporters / thresholds can still
|
|
145
|
-
// format the field consistently.
|
|
146
|
-
branches: { covered: 0, total: 0 },
|
|
147
|
-
lineHits,
|
|
148
|
-
functionHits,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Merge two raw entries for the same file. Functions are deduplicated by
|
|
154
|
-
* `(name, startOffset)` so concatenation doesn't double FNF in lcov.
|
|
155
|
-
* If `source` differs, we keep the longer one (best effort) and emit a
|
|
156
|
-
* stderr advisory — offsets only line up within one source.
|
|
157
|
-
*/
|
|
158
|
-
function mergeRaw(a: RawFileCoverage, b: RawFileCoverage): RawFileCoverage {
|
|
159
|
-
if (a.source && b.source && a.source !== b.source) {
|
|
160
|
-
process.stderr.write(
|
|
161
|
-
`helix-coverage: ${a.file} has divergent source content across workers — line attribution may be off\n`,
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
const seen = new Set<string>();
|
|
165
|
-
const merged: V8Function[] = [];
|
|
166
|
-
for (const fn of [...a.functions, ...b.functions]) {
|
|
167
|
-
const startOffset = fn.ranges[0]?.startOffset ?? -1;
|
|
168
|
-
const key = `${fn.functionName}@${startOffset}`;
|
|
169
|
-
if (seen.has(key)) continue;
|
|
170
|
-
seen.add(key);
|
|
171
|
-
merged.push(fn);
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
file: a.file,
|
|
175
|
-
source: a.source.length >= b.source.length ? a.source : b.source,
|
|
176
|
-
functions: merged,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function pct(covered: number, total: number): number {
|
|
181
|
-
if (total === 0) return 100;
|
|
182
|
-
return Math.round((covered / total) * 10000) / 100;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function aggregate(raw: RawFileCoverage[]): CoverageSummary {
|
|
186
|
-
const byFile = new Map<string, RawFileCoverage>();
|
|
187
|
-
for (const entry of raw) {
|
|
188
|
-
// Skip files we couldn't read source for — emitting a 1-line ghost
|
|
189
|
-
// summary based on collapsed line=1 mapping is worse than omission.
|
|
190
|
-
if (!entry.source) {
|
|
191
|
-
process.stderr.write(
|
|
192
|
-
`helix-coverage: skipping ${entry.file} — source unreadable\n`,
|
|
193
|
-
);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const prev = byFile.get(entry.file);
|
|
197
|
-
byFile.set(entry.file, prev ? mergeRaw(prev, entry) : entry);
|
|
198
|
-
}
|
|
199
|
-
const files = Array.from(byFile.values())
|
|
200
|
-
.map(computeFileSummary)
|
|
201
|
-
// Files that ended up with 0 lines / 0 functions (no V8 data) are
|
|
202
|
-
// dropped — they distort per-file averages and clutter the table.
|
|
203
|
-
.filter((f) => f.lines.total > 0 || f.functions.total > 0)
|
|
204
|
-
.sort((a, b) => a.file.localeCompare(b.file));
|
|
205
|
-
|
|
206
|
-
const totals: Totals = {
|
|
207
|
-
lines: { covered: 0, total: 0, pct: 0 },
|
|
208
|
-
statements: { covered: 0, total: 0, pct: 0 },
|
|
209
|
-
functions: { covered: 0, total: 0, pct: 0 },
|
|
210
|
-
branches: { covered: 0, total: 0, pct: 0 },
|
|
211
|
-
};
|
|
212
|
-
for (const f of files) {
|
|
213
|
-
totals.lines.covered += f.lines.covered;
|
|
214
|
-
totals.lines.total += f.lines.total;
|
|
215
|
-
totals.statements.covered += f.statements.covered;
|
|
216
|
-
totals.statements.total += f.statements.total;
|
|
217
|
-
totals.functions.covered += f.functions.covered;
|
|
218
|
-
totals.functions.total += f.functions.total;
|
|
219
|
-
totals.branches.covered += f.branches.covered;
|
|
220
|
-
totals.branches.total += f.branches.total;
|
|
221
|
-
}
|
|
222
|
-
totals.lines.pct = pct(totals.lines.covered, totals.lines.total);
|
|
223
|
-
totals.statements.pct = pct(
|
|
224
|
-
totals.statements.covered,
|
|
225
|
-
totals.statements.total,
|
|
226
|
-
);
|
|
227
|
-
totals.functions.pct = pct(totals.functions.covered, totals.functions.total);
|
|
228
|
-
totals.branches.pct = pct(totals.branches.covered, totals.branches.total);
|
|
229
|
-
|
|
230
|
-
return { files, total: totals };
|
|
231
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reads every `coverage-*.json` produced by the V8 profiler in a given
|
|
3
|
-
* directory, decodes `file://` URLs, and returns a normalised list of
|
|
4
|
-
* `RawFileCoverage` entries (one per unique script URL).
|
|
5
|
-
*
|
|
6
|
-
* Workers write coverage via `NODE_V8_COVERAGE=<dir>` — the orchestrator
|
|
7
|
-
* sets this env var on every spawn and reads the directory after all
|
|
8
|
-
* workers exit. Multiple workers may record the same file; we return ALL
|
|
9
|
-
* entries here and let `aggregate.ts` merge.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import type { RawFileCoverage, V8CoverageFile } from "./types.js";
|
|
16
|
-
|
|
17
|
-
export async function collect(dir: string): Promise<RawFileCoverage[]> {
|
|
18
|
-
let entries: string[];
|
|
19
|
-
try {
|
|
20
|
-
entries = await readdir(dir);
|
|
21
|
-
} catch {
|
|
22
|
-
return [];
|
|
23
|
-
}
|
|
24
|
-
const jsonFiles = entries.filter((e) => e.endsWith(".json"));
|
|
25
|
-
const all: RawFileCoverage[] = [];
|
|
26
|
-
for (const name of jsonFiles) {
|
|
27
|
-
const p = path.join(dir, name);
|
|
28
|
-
let parsed: V8CoverageFile;
|
|
29
|
-
try {
|
|
30
|
-
const raw = await readFile(p, "utf8");
|
|
31
|
-
parsed = JSON.parse(raw) as V8CoverageFile;
|
|
32
|
-
} catch {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
for (const script of parsed.result ?? []) {
|
|
36
|
-
if (!script.url?.startsWith("file://")) continue;
|
|
37
|
-
// V8 sometimes appends `?query` params (our worker uses them as
|
|
38
|
-
// cache-busters) — strip them so the same file from two workers
|
|
39
|
-
// merges correctly.
|
|
40
|
-
const cleanUrl = script.url.split("?")[0];
|
|
41
|
-
let file: string;
|
|
42
|
-
try {
|
|
43
|
-
file = fileURLToPath(cleanUrl);
|
|
44
|
-
} catch {
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
let source = "";
|
|
48
|
-
try {
|
|
49
|
-
source = await readFile(file, "utf8");
|
|
50
|
-
} catch {
|
|
51
|
-
// Source read may fail for transient temp files or modules that
|
|
52
|
-
// have been GC'd — leave empty; downstream stages fall back to
|
|
53
|
-
// offset-only computation.
|
|
54
|
-
}
|
|
55
|
-
all.push({
|
|
56
|
-
file,
|
|
57
|
-
source,
|
|
58
|
-
functions: script.functions ?? [],
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return all;
|
|
63
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve the default base ref to diff against. Tries, in order:
|
|
3
|
-
* 1. `origin/main`
|
|
4
|
-
* 2. `origin/master`
|
|
5
|
-
* 3. local `main`
|
|
6
|
-
* 4. local `master`
|
|
7
|
-
*
|
|
8
|
-
* Returns `undefined` if none of those exist (so the caller can warn
|
|
9
|
-
* instead of failing the run). Throws when the `git` binary itself is
|
|
10
|
-
* missing — the caller distinguishes "no repo / no ref" (skip
|
|
11
|
-
* gracefully) from "git not installed" (configuration error).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
15
|
-
|
|
16
|
-
const CANDIDATES = ["origin/main", "origin/master", "main", "master"];
|
|
17
|
-
|
|
18
|
-
export class GitMissingError extends Error {
|
|
19
|
-
constructor() {
|
|
20
|
-
super("git binary not found on PATH");
|
|
21
|
-
this.name = "GitMissingError";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function runGit(cwd: string, args: string[]): { ok: boolean } {
|
|
26
|
-
const result = spawnSync("git", args, {
|
|
27
|
-
cwd,
|
|
28
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
29
|
-
});
|
|
30
|
-
const err = result.error as NodeJS.ErrnoException | undefined;
|
|
31
|
-
if (err?.code === "ENOENT") throw new GitMissingError();
|
|
32
|
-
return { ok: result.status === 0 };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function refExists(cwd: string, ref: string): boolean {
|
|
36
|
-
return runGit(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`])
|
|
37
|
-
.ok;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function resolveBaseRef(cwd: string): string | undefined {
|
|
41
|
-
if (!runGit(cwd, ["rev-parse", "--git-dir"]).ok) return undefined;
|
|
42
|
-
for (const cand of CANDIDATES) {
|
|
43
|
-
if (refExists(cwd, cand)) return cand;
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|