@systemfsoftware/vitest 0.0.0 → 0.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.
@@ -0,0 +1,246 @@
1
+
2
+ import { chai } from "vitest";
3
+ //#region src/internal/refusals.ts
4
+ /**
5
+ * Every refusal the fork prints, as the one text it uses on both channels: the `this` type (or parameter union)
6
+ * the compiler prints at the call site, and the message thrown at run time.
7
+ *
8
+ * Surface: `it(name, function* ({ expect }) { ... })`. Ported verbatim from the prototype's `checks.ts`; the
9
+ * prototype's `test(` surfaces are rewritten to `it(`, and `refusedText` gained the `expect` statics
10
+ * (`.resolves`, `.rejects`, `expect.anything`, `expect.poll`, `expect.soft`).
11
+ *
12
+ * @since 4.0.0
13
+ */
14
+ /**
15
+ * The brand every run-time refusal carries. A test is run a second time on a fresh build to catch leaked state,
16
+ * so when that second run fails with a refusal the fork itself made, the runner re-throws the refusal rather than
17
+ * relabelling it as a leak (see `throwSecondRun` in `runner.ts`). The brand is a `Symbol.for` value because a
18
+ * worker holds two copies of these modules at once — the setup file's `dist/guard.mjs` beside the test graph's
19
+ * `src/mod.ts` — and a refusal thrown by either copy has to be recognisable to the other, exactly as the guard's
20
+ * window is shared.
21
+ */
22
+ const refusalBrand = Symbol.for("@systemfsoftware/vitest/refusal");
23
+ /** The error a refusal throws: the refusal text, branded so {@link isRefusal} recognises it. */
24
+ /** @internal */
25
+ const refusalOf = (text) => {
26
+ const error = new Error(text);
27
+ Reflect.set(error, refusalBrand, true);
28
+ return error;
29
+ };
30
+ /** Whether an error is one of the fork's own refusals, rather than a failure of the test under it. */
31
+ /** @internal */
32
+ const isRefusal = (error) => error instanceof Error && Reflect.get(error, refusalBrand) === true;
33
+ /** @internal */
34
+ const refusedText = {
35
+ toBeDefined: "✗ toBeDefined passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).",
36
+ toBeTruthy: "✗ toBeTruthy passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).",
37
+ toBeFalsy: "✗ toBeFalsy passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).",
38
+ toHaveLength: "✗ toHaveLength checks how many, not which. Assert the contents: toEqual([...]); only some of them: toEqual(expect.arrayContaining([...])).",
39
+ toHaveProperty: "✗ toHaveProperty checks one key at a time. Assert the shape once: toMatchObject({ key: value, ... }).",
40
+ toBeInstanceOf: "✗ toBeInstanceOf checks the kind of value, not the value. Assert the value: toEqual(expected); only its shape: toEqual(expect.schemaMatching(Schema)).",
41
+ toBeTypeOf: "✗ toBeTypeOf checks the kind of value, not the value. Assert the value: toEqual(expected); only its shape: toEqual(expect.schemaMatching(Schema)).",
42
+ toHaveBeenCalled: "✗ toHaveBeenCalled passes for any call with any arguments. Assert what the call carried: toHaveBeenCalledExactlyOnceWith(...args).",
43
+ toHaveBeenCalledTimes: "✗ toHaveBeenCalledTimes counts calls, not what they carried. Assert the calls: toHaveBeenNthCalledWith(n, ...args), or all of them: expect(spy.mock.calls).toEqual([[...], [...]]).",
44
+ toMatchSnapshot: "✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).",
45
+ toMatchInlineSnapshot: "✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).",
46
+ toMatchFileSnapshot: "✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).",
47
+ toThrowErrorMatchingSnapshot: "✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. Name the error: toThrow(MyError).",
48
+ toThrowErrorMatchingInlineSnapshot: "✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. Name the error: toThrow(MyError).",
49
+ resolves: "✗ .resolves/.rejects assert on a Promise outside the test runtime. Yield the Effect: const value = yield* program; its failure: const error = yield* Effect.flip(program); then yield* expect(error).toEqual(new MyError(...)).",
50
+ rejects: "✗ .resolves/.rejects assert on a Promise outside the test runtime. Yield the Effect: const value = yield* program; its failure: const error = yield* Effect.flip(program); then yield* expect(error).toEqual(new MyError(...)).",
51
+ anything: "✗ expect.anything() matches every value. Say what the value must be: expect.any(Money), expect.objectContaining({ ... }), expect.schemaMatching(Schema).",
52
+ poll: "✗ expect.poll waits on the real clock, outside the runner's virtual time. Let time pass inside the test: yield* TestClock.adjust(\"3 seconds\"); then yield* expect(value).toEqual(expected).",
53
+ soft: "✗ expect.soft reports several failures on one state; the fork reports the state once. Assert the state once: yield* expect(actual).toEqual(expected)."
54
+ };
55
+ /** @internal */
56
+ const negatedText = {
57
+ ...refusedText,
58
+ toBeNull: "✗ not.toBeNull passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).",
59
+ toBeUndefined: "✗ not.toBeUndefined passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected)."
60
+ };
61
+ /** @internal */
62
+ const neededText = {
63
+ toThrow: "✗ toThrow() passes for any error. Name the one expected: toThrow(MyError) or toThrow(\"message\"). An Effect failure is a value: const error = yield* Effect.flip(program); yield* expect(error).toEqual(new MyError(...)).",
64
+ toThrowError: "✗ toThrowError() passes for any error. Name the one expected: toThrow(MyError) or toThrow(\"message\"). An Effect failure is a value: const error = yield* Effect.flip(program); yield* expect(error).toEqual(new MyError(...)).",
65
+ toSatisfy: "✗ toSatisfy(predicate) fails with 'expected x to satisfy [Function]'. Say what must hold: toSatisfy(predicate, \"every line has a positive quantity\").",
66
+ toMatchObject: "✗ toMatchObject({}) matches every object. Name the fields that must hold: toMatchObject({ status: \"Pending\" })."
67
+ };
68
+ /** @internal */
69
+ const booleanRefusal = "✗ expect(<boolean>) can only report 'expected false to be true'. Pass the two values instead: yield* expect(a).toEqual(b), which uses Effect Equal. A predicate: yield* expect(value).toSatisfy(predicate, \"what must hold\").";
70
+ /** @internal */
71
+ const refusePiecewise = "✗ a second check on the same state. Checking a state piece by piece reports one field at a time and misses the ones never checked. Assert the state once: yield* expect(order).toMatchObject({ id: 1, status: \"Pending\" }), or gather what you observed: yield* expect({ total, status }).toEqual({ total: Money.of(4.4), status: \"Pending\" }). A check inside a loop is the same: assert the whole array once. Several inputs: it.each(rows)(name, function* (row, { expect }) { ... }).";
72
+ /** @internal */
73
+ const refuseUnyielded = "✗ a check was written but never yielded, so it never ran. Yield it: yield* expect(actual).toEqual(expected).";
74
+ /** @internal */
75
+ const noCheckRefusal = "✗ this test yields no check, so it cannot fail. Yield one from the test's own expect: it(name, function* ({ expect }) { yield* expect(actual).toEqual(expected) }). An expect imported from vitest does not count.";
76
+ /** @internal */
77
+ const unprovidedText = "✗ the test needs a service that nothing provides. Write the test inside layer(Service.layer)((it) => { it(name, ...) }); every test gets its own fresh build.";
78
+ /** @internal */
79
+ const refuseAsyncBody = "✗ an async body runs outside the test runtime. Pass a generator: it(name, function* ({ expect }) { yield* expect(actual).toEqual(expected) }).";
80
+ /** @internal */
81
+ const refuseSyncBody = "✗ the body must be a generator that yields its checks: it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
82
+ /** @internal */
83
+ const refuseEffectLane = "✗ it.effect is removed: the body is the generator itself, so the runner sees every step. it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
84
+ /** @internal */
85
+ const refuseScopedLane = "✗ it.scoped is removed: the body is the generator itself, so the runner sees every step. it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
86
+ /** @internal */
87
+ const refuseScopedLiveLane = "✗ it.scopedLive is removed: the body is the generator itself, so the runner sees every step. it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
88
+ /** @internal */
89
+ const refuseHook = "✗ hooks share state between tests. Build what a test needs inside it; services come fresh per test from layer(Service.layer)((it) => { ... }).";
90
+ /** @internal */
91
+ const refuseRawExpect = "✗ an expect imported from vitest ran; take it from the test callback: it(name, function* ({ expect }) { ... })";
92
+ /** @internal */
93
+ const refuseRawIt = "✗ this test was registered with vitest's it; import it from @systemfsoftware/vitest";
94
+ //#endregion
95
+ //#region src/internal/guard.ts
96
+ /**
97
+ * The library brands both its checks and its tests, and this module closes the way around both brands.
98
+ *
99
+ * The `Asserted` brand only proves a test yielded a fork check. It cannot see a raw `vitest` `expect` beside
100
+ * that check, and it cannot see a test the fork never registered. Both travel on the one shared
101
+ * `chai.Assertion` prototype, measured on Vitest 5.0.1 and Chai 6.2.2:
102
+ *
103
+ * - raw `expect`, `expect.soft` and `expect.poll` all enter chai through `chai.expect`
104
+ * (`vitest/dist/chunks/index.m3L2HgmY.js:8005`, `:8035`, `:7514`);
105
+ * - the task's own `ctx.expect` is `createExpect(context.task)` (`vitest/dist/chunks/index.m3L2HgmY.js:8513`)
106
+ * and enters through that same `chai.expect` (`:8005`);
107
+ * - chai's `expect` is `new Assertion(val, message)` (`chai/index.js:3272`), and every `assert.*` method is
108
+ * `new Assertion(...)` on the same class (`chai/index.js:3367`, `:3394`).
109
+ *
110
+ * So this module wraps that prototype once, and every method, chainable method and property on it refuses
111
+ * unless a check opened the window with {@link authorize} for the duration of its matcher call. Nothing
112
+ * asynchronous may run inside that window, so a plain synchronous depth counter is enough; the counter is
113
+ * nested-safe because chai's own matchers build further assertions inside the one call.
114
+ *
115
+ * The window, the install flag and the wrapper marks live on `globalThis` under `Symbol.for` keys rather than
116
+ * in module scope. A worker can hold two copies of this file at once — the setup file resolves through Node's
117
+ * conditions to `dist/guard.mjs` while the test graph resolves the fork's entry through the source condition
118
+ * to `src/mod.ts` — and a window opened by one copy has to be the window the other copy reads. Module-scope
119
+ * state made every legitimate check fail with the raw-expect refusal.
120
+ *
121
+ * The other half is {@link markTask}: the fork's `it` marks each task it registers, and the setup-file entry
122
+ * (`@systemfsoftware/vitest/guard`) refuses a task that carries no mark. This setup-file hook is internal plumbing; the
123
+ * user-facing hook refusal is the fork's own `beforeEach`.
124
+ *
125
+ * @since 4.0.0
126
+ */
127
+ const isCallable = (value) => typeof value === "function";
128
+ const isObject = (value) => typeof value === "object" && value !== null;
129
+ const hasOwn = (value, key) => Object.hasOwn(value, key);
130
+ const hasGuardFields = (value) => hasOwn(value, "depth") && hasOwn(value, "installed");
131
+ const isGuardState = (value) => isObject(value) && hasGuardFields(value);
132
+ const stateKey = Symbol.for("@systemfsoftware/vitest/guard");
133
+ const wrappedKey = Symbol.for("@systemfsoftware/vitest/guard/wrapped");
134
+ const createState = () => {
135
+ const created = {
136
+ depth: 0,
137
+ installed: false,
138
+ extendPatched: false
139
+ };
140
+ Reflect.set(globalThis, stateKey, created);
141
+ return created;
142
+ };
143
+ const stateOf = () => {
144
+ const existing = Reflect.get(globalThis, stateKey);
145
+ return isGuardState(existing) ? existing : createState();
146
+ };
147
+ const state = stateOf();
148
+ const isWrapped = (value) => Reflect.get(value, wrappedKey) === true;
149
+ const isUnwrapped = (value) => isCallable(value) && !isWrapped(value);
150
+ const authorized = () => state.depth > 0;
151
+ const refuse = () => {
152
+ throw refusalOf(refuseRawExpect);
153
+ };
154
+ /** @internal */
155
+ const authorize = (run) => {
156
+ state.depth += 1;
157
+ try {
158
+ return run();
159
+ } finally {
160
+ state.depth -= 1;
161
+ }
162
+ };
163
+ /** @internal */
164
+ const markTask = (task) => {
165
+ task.meta.effectVitestMarked = true;
166
+ };
167
+ /** @internal */
168
+ const isMarked = (task) => task.meta.effectVitestMarked === true;
169
+ const guardedCall = (run) => {
170
+ const guarded = function(...args) {
171
+ if (!authorized()) refuse();
172
+ return Reflect.apply(run, this, args);
173
+ };
174
+ Reflect.defineProperty(guarded, wrappedKey, { value: true });
175
+ return guarded;
176
+ };
177
+ const fieldOf = (descriptor, field) => Reflect.get(descriptor, field);
178
+ const withValue = (descriptor) => {
179
+ const value = fieldOf(descriptor, "value");
180
+ return isUnwrapped(value) ? {
181
+ ...descriptor,
182
+ value: guardedCall(value)
183
+ } : descriptor;
184
+ };
185
+ const withRead = (descriptor) => {
186
+ const read = fieldOf(descriptor, "get");
187
+ return isUnwrapped(read) ? {
188
+ ...descriptor,
189
+ get: guardedCall(read)
190
+ } : descriptor;
191
+ };
192
+ const withWrite = (descriptor) => {
193
+ const write = fieldOf(descriptor, "set");
194
+ return isUnwrapped(write) ? {
195
+ ...descriptor,
196
+ set: guardedCall(write)
197
+ } : descriptor;
198
+ };
199
+ const guardedDescriptor = (descriptor) => withWrite(withRead(withValue(descriptor)));
200
+ const isWrappable = (descriptor) => descriptor?.configurable === true;
201
+ const guardMember = (proto, name) => {
202
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
203
+ if (!isWrappable(descriptor)) return;
204
+ Object.defineProperty(proto, name, guardedDescriptor(descriptor));
205
+ };
206
+ const bookkeeping = [
207
+ "constructor",
208
+ "__methods",
209
+ "__flags",
210
+ "_obj"
211
+ ];
212
+ /**
213
+ * Every own member of the prototype, string- and symbol-keyed, except chai's bookkeeping fields. `Reflect.ownKeys`
214
+ * rather than `Object.getOwnPropertyNames`, so a member chai hangs off a symbol is guarded too: measured on chai
215
+ * 6.2.2, `Assertion.prototype` carries 104 string-keyed own members and no symbol-keyed ones, so the enumeration
216
+ * is unchanged today and stays closed if chai adds one.
217
+ */
218
+ const assertionMembers = (proto) => Reflect.ownKeys(proto).filter((name) => bookkeeping.includes(name) === false);
219
+ const guardPrototype = () => {
220
+ const proto = chai.Assertion.prototype;
221
+ for (const name of assertionMembers(proto)) guardMember(proto, name);
222
+ };
223
+ const extendOf = () => Reflect.get(chai.expect, "extend");
224
+ const wrapExtend = () => {
225
+ const extend = extendOf();
226
+ if (!isCallable(extend)) return;
227
+ state.extendPatched = true;
228
+ chai.util.addMethod(chai.expect, "extend", function(...args) {
229
+ const result = Reflect.apply(extend, this, args);
230
+ guardPrototype();
231
+ return result;
232
+ });
233
+ };
234
+ const patchExtend = () => {
235
+ if (state.extendPatched) return;
236
+ wrapExtend();
237
+ };
238
+ /** @internal */
239
+ const installGuard = () => {
240
+ if (state.installed) return;
241
+ state.installed = true;
242
+ guardPrototype();
243
+ patchExtend();
244
+ };
245
+ //#endregion
246
+ export { refuseScopedLiveLane as _, booleanRefusal as a, refusedText as b, negatedText as c, refuseAsyncBody as d, refuseEffectLane as f, refuseScopedLane as g, refuseRawIt as h, markTask as i, noCheckRefusal as l, refusePiecewise as m, installGuard as n, isRefusal as o, refuseHook as p, isMarked as r, neededText as s, authorize as t, refusalOf as u, refuseSyncBody as v, unprovidedText as x, refuseUnyielded as y };
@@ -0,0 +1,2 @@
1
+
2
+ export {}
package/dist/guard.mjs ADDED
@@ -0,0 +1,19 @@
1
+
2
+ import { h as refuseRawIt, n as installGuard, r as isMarked, u as refusalOf } from "./guard-BocOhXt2.mjs";
3
+ import { beforeEach } from "vitest";
4
+ //#region src/guard.ts
5
+ /**
6
+ * The guard setup file, published as `@systemfsoftware/vitest/guard` and listed in the shared Vitest config's
7
+ * `setupFiles`. It installs the prototype guard once per worker and refuses any task the fork's `it` did not
8
+ * register, so neither a raw `vitest` `expect` nor a raw `vitest` `it` can sit beside a fork check (KTD8).
9
+ *
10
+ * Both halves are internal plumbing; the fork's own `beforeEach` refusal is the user-facing one.
11
+ *
12
+ * @since 4.0.0
13
+ */
14
+ installGuard();
15
+ beforeEach((context) => {
16
+ if (!isMarked(context.task)) throw refusalOf(refuseRawIt);
17
+ });
18
+ //#endregion
19
+ export {};