@alchemy.run/test 0.0.0 → 2.0.0-beta.85
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/LICENSE +201 -0
- package/bin/alchemy-test.ts +2 -0
- package/lib/Alchemy.d.ts +78 -0
- package/lib/Alchemy.d.ts.map +1 -0
- package/lib/Alchemy.js +194 -0
- package/lib/Alchemy.js.map +1 -0
- package/lib/Api.d.ts +134 -0
- package/lib/Api.d.ts.map +1 -0
- package/lib/Api.js +245 -0
- package/lib/Api.js.map +1 -0
- package/lib/Bun.d.ts +67 -0
- package/lib/Bun.d.ts.map +1 -0
- package/lib/Bun.js +143 -0
- package/lib/Bun.js.map +1 -0
- package/lib/Capture.d.ts +10 -0
- package/lib/Capture.d.ts.map +1 -0
- package/lib/Capture.js +68 -0
- package/lib/Capture.js.map +1 -0
- package/lib/Core.d.ts +287 -0
- package/lib/Core.d.ts.map +1 -0
- package/lib/Core.js +429 -0
- package/lib/Core.js.map +1 -0
- package/lib/DevCli.d.ts +47 -0
- package/lib/DevCli.d.ts.map +1 -0
- package/lib/DevCli.js +181 -0
- package/lib/DevCli.js.map +1 -0
- package/lib/Expect.d.ts +84 -0
- package/lib/Expect.d.ts.map +1 -0
- package/lib/Expect.js +518 -0
- package/lib/Expect.js.map +1 -0
- package/lib/FileLog.d.ts +27 -0
- package/lib/FileLog.d.ts.map +1 -0
- package/lib/FileLog.js +136 -0
- package/lib/FileLog.js.map +1 -0
- package/lib/Http.d.ts +109 -0
- package/lib/Http.d.ts.map +1 -0
- package/lib/Http.js +123 -0
- package/lib/Http.js.map +1 -0
- package/lib/Model.d.ts +98 -0
- package/lib/Model.d.ts.map +1 -0
- package/lib/Model.js +59 -0
- package/lib/Model.js.map +1 -0
- package/lib/PlainReporter.d.ts +19 -0
- package/lib/PlainReporter.d.ts.map +1 -0
- package/lib/PlainReporter.js +348 -0
- package/lib/PlainReporter.js.map +1 -0
- package/lib/Plan.d.ts +19 -0
- package/lib/Plan.d.ts.map +1 -0
- package/lib/Plan.js +32 -0
- package/lib/Plan.js.map +1 -0
- package/lib/Registry.d.ts +34 -0
- package/lib/Registry.d.ts.map +1 -0
- package/lib/Registry.js +80 -0
- package/lib/Registry.js.map +1 -0
- package/lib/Reporter.d.ts +160 -0
- package/lib/Reporter.d.ts.map +1 -0
- package/lib/Reporter.js +9 -0
- package/lib/Reporter.js.map +1 -0
- package/lib/Runner.d.ts +51 -0
- package/lib/Runner.d.ts.map +1 -0
- package/lib/Runner.js +1052 -0
- package/lib/Runner.js.map +1 -0
- package/lib/StrayOutput.d.ts +8 -0
- package/lib/StrayOutput.d.ts.map +1 -0
- package/lib/StrayOutput.js +259 -0
- package/lib/StrayOutput.js.map +1 -0
- package/lib/Tags.d.ts +17 -0
- package/lib/Tags.d.ts.map +1 -0
- package/lib/Tags.js +84 -0
- package/lib/Tags.js.map +1 -0
- package/lib/TestState.d.ts +12 -0
- package/lib/TestState.d.ts.map +1 -0
- package/lib/TestState.js +15 -0
- package/lib/TestState.js.map +1 -0
- package/lib/Tui.d.ts +4 -0
- package/lib/Tui.d.ts.map +1 -0
- package/lib/Tui.js +1137 -0
- package/lib/Tui.js.map +1 -0
- package/lib/Vitest.d.ts +58 -0
- package/lib/Vitest.d.ts.map +1 -0
- package/lib/Vitest.js +132 -0
- package/lib/Vitest.js.map +1 -0
- package/lib/WorktreeStage.d.ts +17 -0
- package/lib/WorktreeStage.d.ts.map +1 -0
- package/lib/WorktreeStage.js +48 -0
- package/lib/WorktreeStage.js.map +1 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +222 -0
- package/lib/cli.js.map +1 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -0
- package/package.json +73 -3
- package/src/Alchemy.ts +335 -0
- package/src/Api.ts +521 -0
- package/src/Bun.ts +256 -0
- package/src/Capture.ts +72 -0
- package/src/Core.ts +872 -0
- package/src/DevCli.ts +229 -0
- package/src/Expect.ts +763 -0
- package/src/FileLog.ts +158 -0
- package/src/Http.ts +201 -0
- package/src/Model.ts +160 -0
- package/src/PlainReporter.ts +426 -0
- package/src/Plan.ts +45 -0
- package/src/Registry.ts +101 -0
- package/src/Reporter.ts +171 -0
- package/src/Runner.ts +1341 -0
- package/src/StrayOutput.ts +283 -0
- package/src/Tags.ts +100 -0
- package/src/TestState.ts +35 -0
- package/src/Tui.ts +1272 -0
- package/src/Vitest.ts +239 -0
- package/src/WorktreeStage.ts +45 -0
- package/src/cli.ts +330 -0
- package/src/index.ts +57 -0
- package/README.md +0 -9
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stray-output hardening: while a run is active, every patchable JS-level
|
|
3
|
+
* path that could write to the terminal is diverted into the run log so it
|
|
4
|
+
* can never interleave with reporter output (plain mode) or corrupt the
|
|
5
|
+
* alternate screen (TUI mode). Identified-and-fixed-at-the-source is still
|
|
6
|
+
* preferred (per-test capture attributes output to its test); this is the
|
|
7
|
+
* safety net that makes an unknown writer harmless instead of a corruption
|
|
8
|
+
* hunt.
|
|
9
|
+
*
|
|
10
|
+
* Diverted globally (prefixed in the log; all verified under bun):
|
|
11
|
+
* 1. `process.stdout.write` / `process.stderr.write` [stray stdout|stderr]
|
|
12
|
+
* 2. global `console.*` — bun's console does NOT go through
|
|
13
|
+
* `process.stdout.write`, it writes natively [stray console]
|
|
14
|
+
* 3. `Bun.spawn` with `stdout/stderr: "inherit"` — downgraded to "pipe"
|
|
15
|
+
* and pumped into the log [stray child]
|
|
16
|
+
* 4. best-effort only: `node:child_process.spawn` inherit and
|
|
17
|
+
* `fs.writeSync/write` to fds 1/2. Under bun, ESM namespace imports
|
|
18
|
+
* bind straight to the native functions and IGNORE module-object
|
|
19
|
+
* mutation, so these patches only cover CJS `require` callers. ESM
|
|
20
|
+
* users of these APIs must be fixed at the source (e.g.
|
|
21
|
+
* LanguageModel.test.ts's fs.writeSync(2) was).
|
|
22
|
+
*
|
|
23
|
+
* NOT interceptable from JS at all: native code writing to the fd directly
|
|
24
|
+
* (e.g. rolldown's Rust progress spinner — held off via CI=true / logLevel).
|
|
25
|
+
*
|
|
26
|
+
* The reporter itself writes through {@link writeDirect}, which always uses
|
|
27
|
+
* the REAL stream saved before patching.
|
|
28
|
+
*/
|
|
29
|
+
// The sinks run inside synchronous patches — they cannot be effectful, so
|
|
30
|
+
// this module uses node:fs append directly (append mode interleaves safely
|
|
31
|
+
// with the Effect-based FileLog appends).
|
|
32
|
+
import * as childProcess from "node:child_process";
|
|
33
|
+
import * as fs from "node:fs";
|
|
34
|
+
|
|
35
|
+
type StreamWrite = typeof process.stdout.write;
|
|
36
|
+
|
|
37
|
+
let realStdoutWrite: StreamWrite | undefined;
|
|
38
|
+
|
|
39
|
+
/** Write to the real terminal, bypassing any active stray-output capture. */
|
|
40
|
+
export const writeDirect = (text: string): void => {
|
|
41
|
+
const write = realStdoutWrite ?? process.stdout.write.bind(process.stdout);
|
|
42
|
+
write(text);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
|
|
47
|
+
// Strip ANSI so diverted output is readable in the log.
|
|
48
|
+
const ANSI_RE =
|
|
49
|
+
// eslint-disable-next-line no-control-regex
|
|
50
|
+
/\u001B(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001B]*(?:\u0007|\u001B\\)|[@-Z\\-_])/g;
|
|
51
|
+
|
|
52
|
+
const toText = (chunk: unknown): string =>
|
|
53
|
+
(typeof chunk === "string"
|
|
54
|
+
? chunk
|
|
55
|
+
: chunk instanceof Uint8Array
|
|
56
|
+
? decoder.decode(chunk)
|
|
57
|
+
: String(chunk)
|
|
58
|
+
).replace(ANSI_RE, "");
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Divert every JS-level terminal writer into `logFile`. Returns a restore
|
|
62
|
+
* function; safe to call multiple times (only the first install wins).
|
|
63
|
+
*/
|
|
64
|
+
export const captureStrayOutput = (logFile: string): (() => void) => {
|
|
65
|
+
if (realStdoutWrite !== undefined) return () => {};
|
|
66
|
+
|
|
67
|
+
const sink = (prefix: string, chunk: unknown): void => {
|
|
68
|
+
try {
|
|
69
|
+
const text = toText(chunk);
|
|
70
|
+
if (text.trim() === "") return;
|
|
71
|
+
const prefixed = text
|
|
72
|
+
.split("\n")
|
|
73
|
+
.map((line) => (line === "" ? line : `[${prefix}] ${line}`))
|
|
74
|
+
.join("\n");
|
|
75
|
+
fs.appendFileSync(logFile, prefixed.endsWith("\n") ? prefixed : `${prefixed}\n`);
|
|
76
|
+
} catch {
|
|
77
|
+
// Never let log diversion break the writer.
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const restores: Array<() => void> = [];
|
|
82
|
+
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
// 1. process.stdout / process.stderr stream writes
|
|
85
|
+
// -------------------------------------------------------------------------
|
|
86
|
+
realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
87
|
+
const realStderrWrite = process.stderr.write.bind(process.stderr);
|
|
88
|
+
const divertStream =
|
|
89
|
+
(prefix: string): StreamWrite =>
|
|
90
|
+
(
|
|
91
|
+
chunk: string | Uint8Array,
|
|
92
|
+
encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
|
|
93
|
+
cb?: (err?: Error | null) => void,
|
|
94
|
+
): boolean => {
|
|
95
|
+
const callback = typeof encodingOrCb === "function" ? encodingOrCb : cb;
|
|
96
|
+
sink(prefix, chunk);
|
|
97
|
+
callback?.(null);
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
process.stdout.write = divertStream("stray stdout");
|
|
101
|
+
process.stderr.write = divertStream("stray stderr");
|
|
102
|
+
restores.push(() => {
|
|
103
|
+
process.stdout.write = realStdoutWrite!;
|
|
104
|
+
process.stderr.write = realStderrWrite;
|
|
105
|
+
realStdoutWrite = undefined;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// -------------------------------------------------------------------------
|
|
109
|
+
// 2. global console — bun's console writes natively, NOT via stdout.write
|
|
110
|
+
// -------------------------------------------------------------------------
|
|
111
|
+
try {
|
|
112
|
+
const methods = [
|
|
113
|
+
"log",
|
|
114
|
+
"info",
|
|
115
|
+
"warn",
|
|
116
|
+
"error",
|
|
117
|
+
"debug",
|
|
118
|
+
"trace",
|
|
119
|
+
"dir",
|
|
120
|
+
"table",
|
|
121
|
+
"group",
|
|
122
|
+
"groupCollapsed",
|
|
123
|
+
"groupEnd",
|
|
124
|
+
] as const;
|
|
125
|
+
const original = new Map<string, unknown>();
|
|
126
|
+
for (const method of methods) {
|
|
127
|
+
original.set(method, console[method]);
|
|
128
|
+
(console as unknown as Record<string, unknown>)[method] = (...args: Array<unknown>) => {
|
|
129
|
+
sink(
|
|
130
|
+
"stray console",
|
|
131
|
+
args
|
|
132
|
+
.map((arg) => (typeof arg === "string" ? arg : Bun.inspect(arg, { depth: 4 })))
|
|
133
|
+
.join(" "),
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
restores.push(() => {
|
|
138
|
+
for (const [method, fn] of original) {
|
|
139
|
+
(console as unknown as Record<string, unknown>)[method] = fn;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
// best-effort
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// -------------------------------------------------------------------------
|
|
147
|
+
// 3. fs.writeSync / fs.write to fds 1 and 2 (bypasses the stream objects)
|
|
148
|
+
// -------------------------------------------------------------------------
|
|
149
|
+
try {
|
|
150
|
+
const realWriteSync = fs.writeSync;
|
|
151
|
+
const realWrite = fs.write;
|
|
152
|
+
const fsModule = fs as unknown as Record<string, unknown>;
|
|
153
|
+
fsModule.writeSync = ((fd: unknown, data: unknown, ...rest: Array<unknown>) => {
|
|
154
|
+
if (fd === 1 || fd === 2) {
|
|
155
|
+
sink(`stray fd${fd}`, data);
|
|
156
|
+
return typeof data === "string"
|
|
157
|
+
? Buffer.byteLength(data)
|
|
158
|
+
: ((data as Uint8Array).byteLength ?? 0);
|
|
159
|
+
}
|
|
160
|
+
return (realWriteSync as (...a: Array<unknown>) => number)(fd, data, ...rest);
|
|
161
|
+
}) as typeof fs.writeSync;
|
|
162
|
+
fsModule.write = ((fd: unknown, data: unknown, ...rest: Array<unknown>) => {
|
|
163
|
+
if (fd === 1 || fd === 2) {
|
|
164
|
+
sink(`stray fd${fd}`, data);
|
|
165
|
+
const callback = rest.findLast((arg) => typeof arg === "function") as
|
|
166
|
+
| ((err: Error | null, written: number, data: unknown) => void)
|
|
167
|
+
| undefined;
|
|
168
|
+
callback?.(null, 0, data);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
return (realWrite as (...a: Array<unknown>) => unknown)(fd, data, ...rest);
|
|
172
|
+
}) as typeof fs.write;
|
|
173
|
+
restores.push(() => {
|
|
174
|
+
fsModule.writeSync = realWriteSync;
|
|
175
|
+
fsModule.write = realWrite;
|
|
176
|
+
});
|
|
177
|
+
} catch {
|
|
178
|
+
// best-effort — the module namespace may not be writable
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
// 4. child processes spawned with stdio: "inherit" (fd-level, otherwise
|
|
183
|
+
// unintersectable) — downgrade stdout/stderr to "pipe" and pump.
|
|
184
|
+
// -------------------------------------------------------------------------
|
|
185
|
+
try {
|
|
186
|
+
const realSpawn = childProcess.spawn;
|
|
187
|
+
const cpModule = childProcess as unknown as Record<string, unknown>;
|
|
188
|
+
cpModule.spawn = ((...args: Array<unknown>) => {
|
|
189
|
+
const options = args.find(
|
|
190
|
+
(arg): arg is Record<string, unknown> =>
|
|
191
|
+
typeof arg === "object" && arg !== null && !Array.isArray(arg),
|
|
192
|
+
);
|
|
193
|
+
let pumpStdout = false;
|
|
194
|
+
let pumpStderr = false;
|
|
195
|
+
if (options !== undefined) {
|
|
196
|
+
const stdio = options.stdio;
|
|
197
|
+
if (stdio === "inherit") {
|
|
198
|
+
options.stdio = ["inherit", "pipe", "pipe"];
|
|
199
|
+
pumpStdout = pumpStderr = true;
|
|
200
|
+
} else if (Array.isArray(stdio)) {
|
|
201
|
+
if (stdio[1] === "inherit") {
|
|
202
|
+
stdio[1] = "pipe";
|
|
203
|
+
pumpStdout = true;
|
|
204
|
+
}
|
|
205
|
+
if (stdio[2] === "inherit") {
|
|
206
|
+
stdio[2] = "pipe";
|
|
207
|
+
pumpStderr = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const child = (realSpawn as (...a: Array<unknown>) => childProcess.ChildProcess)(...args);
|
|
212
|
+
if (pumpStdout) {
|
|
213
|
+
child.stdout?.on("data", (chunk) => sink("stray child", chunk));
|
|
214
|
+
}
|
|
215
|
+
if (pumpStderr) {
|
|
216
|
+
child.stderr?.on("data", (chunk) => sink("stray child", chunk));
|
|
217
|
+
}
|
|
218
|
+
return child;
|
|
219
|
+
}) as typeof childProcess.spawn;
|
|
220
|
+
restores.push(() => {
|
|
221
|
+
cpModule.spawn = realSpawn;
|
|
222
|
+
});
|
|
223
|
+
} catch {
|
|
224
|
+
// best-effort
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const realBunSpawn = Bun.spawn;
|
|
229
|
+
Bun.spawn = ((...args: Array<unknown>) => {
|
|
230
|
+
// Both signatures: Bun.spawn(cmds, options?) and Bun.spawn({ cmd, ... }).
|
|
231
|
+
const options = args.find(
|
|
232
|
+
(arg): arg is Record<string, unknown> =>
|
|
233
|
+
typeof arg === "object" && arg !== null && !Array.isArray(arg),
|
|
234
|
+
);
|
|
235
|
+
let pumpStdout = false;
|
|
236
|
+
let pumpStderr = false;
|
|
237
|
+
if (options !== undefined) {
|
|
238
|
+
if (options.stdout === "inherit") {
|
|
239
|
+
options.stdout = "pipe";
|
|
240
|
+
pumpStdout = true;
|
|
241
|
+
}
|
|
242
|
+
if (options.stderr === "inherit") {
|
|
243
|
+
options.stderr = "pipe";
|
|
244
|
+
pumpStderr = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const proc = (realBunSpawn as (...a: Array<unknown>) => ReturnType<typeof Bun.spawn>)(
|
|
248
|
+
...args,
|
|
249
|
+
);
|
|
250
|
+
const pump = (stream: unknown): void => {
|
|
251
|
+
if (stream instanceof ReadableStream) {
|
|
252
|
+
void (async () => {
|
|
253
|
+
const reader = stream.getReader();
|
|
254
|
+
for (;;) {
|
|
255
|
+
const { done, value } = await reader.read();
|
|
256
|
+
if (done) break;
|
|
257
|
+
sink("stray child", value);
|
|
258
|
+
}
|
|
259
|
+
})().catch(() => {});
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
if (pumpStdout) pump(proc.stdout);
|
|
263
|
+
if (pumpStderr) pump(proc.stderr);
|
|
264
|
+
return proc;
|
|
265
|
+
}) as typeof Bun.spawn;
|
|
266
|
+
restores.push(() => {
|
|
267
|
+
Bun.spawn = realBunSpawn;
|
|
268
|
+
});
|
|
269
|
+
} catch {
|
|
270
|
+
// best-effort
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return () => {
|
|
274
|
+
// Restore in reverse installation order.
|
|
275
|
+
for (const restore of restores.reverse()) {
|
|
276
|
+
try {
|
|
277
|
+
restore();
|
|
278
|
+
} catch {
|
|
279
|
+
// ignore
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
};
|
package/src/Tags.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** Augment this interface with `tags: "unit" | "e2e" | ...` to constrain tags. */
|
|
2
|
+
export interface TestTags {}
|
|
3
|
+
|
|
4
|
+
export type TestTag = TestTags extends { tags: infer T extends string } ? T : string;
|
|
5
|
+
|
|
6
|
+
export type Tags = TestTag | ReadonlyArray<TestTag>;
|
|
7
|
+
|
|
8
|
+
/** Merge inherited labels without changing runner options or execution mode. */
|
|
9
|
+
export const mergeTags = (inherited: ReadonlyArray<string>, tags?: Tags): ReadonlyArray<string> => {
|
|
10
|
+
const own = typeof tags === "string" ? [tags] : (tags ?? []);
|
|
11
|
+
for (const tag of own) {
|
|
12
|
+
if (!tag || /[\s()&|!*]/.test(tag) || /^(and|or|not)$/i.test(tag)) {
|
|
13
|
+
throw new Error(`Invalid test tag: ${JSON.stringify(tag)}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...new Set([...inherited, ...own])];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type TagsFilter = (
|
|
20
|
+
tags: ReadonlyArray<string>,
|
|
21
|
+
optInTags?: ReadonlyArray<string>,
|
|
22
|
+
) => boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compile a tag expression: `!` / `not`, `&&` / `and`, `||` / `or`,
|
|
26
|
+
* parentheses, and `*` wildcards. Repeated expressions are ANDed together.
|
|
27
|
+
* Parse eagerly so malformed filters fail before any test file is imported.
|
|
28
|
+
*/
|
|
29
|
+
export const compileTagsFilter = (expressions: ReadonlyArray<string>): TagsFilter => {
|
|
30
|
+
const explicitlySelected = new Set<string>();
|
|
31
|
+
const filters = expressions.map((expression): TagsFilter => {
|
|
32
|
+
const tokens = expression.match(/&&|\|\||[()!&|]|[^\s()!&|]+/g) ?? [];
|
|
33
|
+
let position = 0;
|
|
34
|
+
let negationDepth = 0;
|
|
35
|
+
const fail = (): never => {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid --tags ${JSON.stringify(expression)}: unexpected ${
|
|
38
|
+
tokens[position] === undefined ? "end of expression" : JSON.stringify(tokens[position])
|
|
39
|
+
}`,
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
const take = (...values: string[]): boolean => {
|
|
43
|
+
if (!values.includes(tokens[position]?.toLowerCase() ?? "")) return false;
|
|
44
|
+
position++;
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
const primary = (): TagsFilter => {
|
|
48
|
+
if (take("!", "not")) {
|
|
49
|
+
negationDepth++;
|
|
50
|
+
const operand = primary();
|
|
51
|
+
negationDepth--;
|
|
52
|
+
return (tags) => !operand(tags);
|
|
53
|
+
}
|
|
54
|
+
if (take("(")) {
|
|
55
|
+
const inner = or();
|
|
56
|
+
if (!take(")")) fail();
|
|
57
|
+
return inner;
|
|
58
|
+
}
|
|
59
|
+
const token = tokens[position];
|
|
60
|
+
if (token === undefined || /^[()&|!]$/.test(token) || /^(and|or|&&|\|\|)$/i.test(token))
|
|
61
|
+
return fail();
|
|
62
|
+
position++;
|
|
63
|
+
// A wildcard or a reference anywhere inside negation is not an opt-in.
|
|
64
|
+
if (negationDepth === 0 && !token.includes("*")) {
|
|
65
|
+
explicitlySelected.add(token);
|
|
66
|
+
}
|
|
67
|
+
const pattern = new RegExp(
|
|
68
|
+
`^${token
|
|
69
|
+
.split("*")
|
|
70
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
71
|
+
.join(".*")}$`,
|
|
72
|
+
);
|
|
73
|
+
return (tags) => tags.some((tag) => pattern.test(tag));
|
|
74
|
+
};
|
|
75
|
+
const and = (): TagsFilter => {
|
|
76
|
+
let left = primary();
|
|
77
|
+
while (take("&&", "and")) {
|
|
78
|
+
const previous = left;
|
|
79
|
+
const right = primary();
|
|
80
|
+
left = (tags) => previous(tags) && right(tags);
|
|
81
|
+
}
|
|
82
|
+
return left;
|
|
83
|
+
};
|
|
84
|
+
const or = (): TagsFilter => {
|
|
85
|
+
let left = and();
|
|
86
|
+
while (take("||", "or")) {
|
|
87
|
+
const previous = left;
|
|
88
|
+
const right = and();
|
|
89
|
+
left = (tags) => previous(tags) || right(tags);
|
|
90
|
+
}
|
|
91
|
+
return left;
|
|
92
|
+
};
|
|
93
|
+
const result = or();
|
|
94
|
+
if (position !== tokens.length) fail();
|
|
95
|
+
return result;
|
|
96
|
+
});
|
|
97
|
+
return (tags, optInTags = []) =>
|
|
98
|
+
optInTags.every((tag) => explicitlySelected.has(tag)) &&
|
|
99
|
+
filters.every((filter) => filter([...tags, ...optInTags]));
|
|
100
|
+
};
|
package/src/TestState.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Stack } from "alchemy/Stack";
|
|
2
|
+
import * as State from "alchemy/State";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
|
|
5
|
+
export const state = (resources: Record<string, State.ResourceState> = {}) =>
|
|
6
|
+
Layer.effect(
|
|
7
|
+
State.State,
|
|
8
|
+
Stack.useSync((stack) =>
|
|
9
|
+
State.InMemoryService({
|
|
10
|
+
[stack.name]: {
|
|
11
|
+
[stack.stage]: resources,
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const defaultState = (
|
|
18
|
+
resources: Record<string, State.ResourceState> = {},
|
|
19
|
+
other?: {
|
|
20
|
+
[stack: string]: {
|
|
21
|
+
[stage: string]: {
|
|
22
|
+
[resourceId: string]: State.ResourceState;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
) =>
|
|
27
|
+
Layer.succeed(
|
|
28
|
+
State.State,
|
|
29
|
+
State.InMemoryService({
|
|
30
|
+
["test-app"]: {
|
|
31
|
+
["test-stage"]: resources,
|
|
32
|
+
},
|
|
33
|
+
...other,
|
|
34
|
+
}),
|
|
35
|
+
);
|