| 1 | import { expect, it } from "alchemy-test"; |
| 2 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; |
| 3 | import { tmpdir } from "node:os"; |
| 4 | import { dirname, resolve } from "node:path"; |
| 5 | import { fileURLToPath, pathToFileURL } from "node:url"; |
| 6 | |
| 7 | const here = dirname(fileURLToPath(import.meta.url)); |
| 8 | const cli = resolve(here, "../bin/alchemy-test.ts"); |
| 9 | const apiUrl = pathToFileURL(resolve(here, "../src/index.ts")).href; |
| 10 | const effectUrl = pathToFileURL( |
| 11 | resolve(here, "../../../node_modules/effect/dist/Effect.js"), |
| 12 | ).href; |
| 13 | |
| 14 | it( |
| 15 | "excludes exclusive-lock queue time from test durations", |
| 16 | async () => { |
| 17 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-timing-")); |
| 18 | try { |
| 19 | await writeFile( |
| 20 | resolve(root, "timing.test.ts"), |
| 21 | ` |
| 22 | import { describe, it, expect } from ${JSON.stringify(apiUrl)}; |
| 23 | describe.concurrent("timing", () => { |
| 24 | let finished = false; |
| 25 | it("holder", async () => { |
| 26 | await new Promise(resolve => setTimeout(resolve, 500)); |
| 27 | finished = true; |
| 28 | }); |
| 29 | it("queued exclusive", () => { |
| 30 | expect(finished).toBe(true); |
| 31 | }, { exclusive: true }); |
| 32 | }); |
| 33 | `, |
| 34 | ); |
| 35 | const child = Bun.spawn([process.execPath, cli, root, "--retry", "0"], { |
| 36 | cwd: root, |
| 37 | stdout: "pipe", |
| 38 | stderr: "pipe", |
| 39 | env: { ...process.env, NO_COLOR: "1" }, |
| 40 | }); |
| 41 | const [exitCode, stdout, stderr] = await Promise.all([ |
| 42 | child.exited, |
| 43 | new Response(child.stdout).text(), |
| 44 | new Response(child.stderr).text(), |
| 45 | ]); |
| 46 | expect(exitCode).toBe(0); |
| 47 | const duration = `${stdout}\n${stderr}`.match( |
| 48 | /queued exclusive \((\d+)ms\)/, |
| 49 | ); |
| 50 | expect(duration).not.toBeNull(); |
| 51 | expect(Number(duration![1])).toBeLessThan(250); |
| 52 | } finally { |
| 53 | await rm(root, { recursive: true, force: true }); |
| 54 | } |
| 55 | }, |
| 56 | { tags: ["unit", "local"] }, |
| 57 | ); |
| 58 | |
| 59 | it( |
| 60 | "tag filters compose with names and skip excluded setup and teardown", |
| 61 | async () => { |
| 62 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-tags-")); |
| 63 | try { |
| 64 | await writeFile( |
| 65 | resolve(root, "tags.test.ts"), |
| 66 | ` |
| 67 | import { describe, it, beforeAll, afterAll, expect } from ${JSON.stringify(apiUrl)}; |
| 68 | let ready = false; |
| 69 | let ran = false; |
| 70 | describe("selected", { tags: ["e2e", "provider:aws"] }, () => { |
| 71 | beforeAll(() => { ready = true; }); |
| 72 | afterAll(() => { expect(ran).toBe(true); }); |
| 73 | describe("nested", { tags: ["live"] }, () => { |
| 74 | it("chosen", () => { expect(ready).toBe(true); ran = true; }); |
| 75 | it("slow", () => { throw new Error("slow body ran"); }, { tags: "slow" }); |
| 76 | it("other-name", () => { throw new Error("name filter ignored"); }); |
| 77 | }); |
| 78 | }); |
| 79 | describe("excluded", { tags: ["e2e", "dev", "provider:cloudflare"] }, () => { |
| 80 | beforeAll(() => { throw new Error("excluded setup ran"); }); |
| 81 | afterAll(() => { throw new Error("excluded teardown ran"); }); |
| 82 | it("chosen", () => { throw new Error("excluded body ran"); }); |
| 83 | }); |
| 84 | it("untagged", () => { throw new Error("untagged body ran"); }); |
| 85 | `, |
| 86 | ); |
| 87 | const run = async (args: string[]) => { |
| 88 | const child = Bun.spawn( |
| 89 | [process.execPath, cli, root, "--retry", "0", ...args], |
| 90 | { |
| 91 | cwd: root, |
| 92 | stdout: "pipe", |
| 93 | stderr: "pipe", |
| 94 | env: { ...process.env, NO_COLOR: "1" }, |
| 95 | }, |
| 96 | ); |
| 97 | const [exitCode, stdout, stderr] = await Promise.all([ |
| 98 | child.exited, |
| 99 | new Response(child.stdout).text(), |
| 100 | new Response(child.stderr).text(), |
| 101 | ]); |
| 102 | return { exitCode, output: `${stdout}\n${stderr}` }; |
| 103 | }; |
| 104 | await writeFile( |
| 105 | resolve(root, "excluded.test.ts"), |
| 106 | ` |
| 107 | import { it, beforeAll, afterAll } from ${JSON.stringify(apiUrl)}; |
| 108 | beforeAll(() => { throw new Error("excluded file setup ran"); }); |
| 109 | afterAll(() => { throw new Error("excluded file teardown ran"); }); |
| 110 | it("excluded", () => { throw new Error("excluded file body ran"); }, { tags: "unit" }); |
| 111 | `, |
| 112 | ); |
| 113 | const selected = await run([ |
| 114 | "--tags", |
| 115 | "e2e && live && provider:*", |
| 116 | "--tags", |
| 117 | "!slow", |
| 118 | "-t", |
| 119 | "chosen", |
| 120 | ]); |
| 121 | expect(selected.exitCode).toBe(0); |
| 122 | expect(selected.output).toContain("1 passed"); |
| 123 | expect(selected.output).toContain("selected > nested > chosen"); |
| 124 | const empty = await run(["--tags", "missing"]); |
| 125 | expect(empty.exitCode).toBe(0); |
| 126 | expect(empty.output).toContain("0 passed"); |
| 127 | |
| 128 | await writeFile( |
| 129 | resolve(root, "only.test.ts"), |
| 130 | ` |
| 131 | import { it } from ${JSON.stringify(apiUrl)}; |
| 132 | it.only("only", () => { throw new Error("only bypassed tags"); }, { tags: "other" }); |
| 133 | `, |
| 134 | ); |
| 135 | const only = await run(["--tags", "e2e"]); |
| 136 | expect(only.exitCode).toBe(0); |
| 137 | expect(only.output).toContain("0 passed"); |
| 138 | |
| 139 | // Even an import that throws must not be reached with malformed syntax. |
| 140 | await writeFile( |
| 141 | resolve(root, "bad.test.ts"), |
| 142 | 'throw new Error("IMPORTED_SENTINEL");', |
| 143 | ); |
| 144 | const invalid = await run(["--tags", "unit &&"]); |
| 145 | expect(invalid.exitCode).not.toBe(0); |
| 146 | expect(invalid.output).toContain("Invalid --tags"); |
| 147 | expect(invalid.output).not.toContain("IMPORTED_SENTINEL"); |
| 148 | } finally { |
| 149 | await rm(root, { recursive: true, force: true }); |
| 150 | } |
| 151 | }, |
| 152 | { tags: ["unit", "local"] }, |
| 153 | ); |
| 154 | |
| 155 | const fixture = (hook: string, body: string): string => ` |
| 156 | import { it, registerHook } from ${JSON.stringify(apiUrl)}; |
| 157 | import * as Effect from ${JSON.stringify(effectUrl)}; |
| 158 | registerHook(${JSON.stringify(hook)}, { body: () => Effect.gen(function* () { |
| 159 | yield* Effect.log(${JSON.stringify(`${hook}-captured-output`)}); |
| 160 | return yield* Effect.fail(new Error(${JSON.stringify(`${hook}-sentinel`)})); |
| 161 | }) |
| 162 | }); |
| 163 | ${body} |
| 164 | `; |
| 165 | |
| 166 | it( |
| 167 | "streams file-hook output to the run log while the hook is still running", |
| 168 | async () => { |
| 169 | // Regression: file-level hook output (deploy/destroy) used to be buffered |
| 170 | // until FileEnd, so a long-running beforeAll produced a run log that |
| 171 | // stopped growing entirely — a multi-minute cloud deploy read as a |
| 172 | // deadlocked run (0% CPU, silent log). Hook log entries must reach the |
| 173 | // per-run log file WHILE the hook is still executing. |
| 174 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-livehook-")); |
| 175 | try { |
| 176 | await writeFile( |
| 177 | resolve(root, "live-hook.test.ts"), |
| 178 | ` |
| 179 | import { it, registerHook } from ${JSON.stringify(apiUrl)}; |
| 180 | import * as Effect from ${JSON.stringify(effectUrl)}; |
| 181 | registerHook("beforeAll", { body: () => Effect.gen(function* () { |
| 182 | yield* Effect.log("hook-live-sentinel"); |
| 183 | yield* Effect.sleep("8 seconds"); |
| 184 | }) }); |
| 185 | it("body", () => {}); |
| 186 | `, |
| 187 | ); |
| 188 | |
| 189 | const child = Bun.spawn( |
| 190 | [process.execPath, cli, root, "--retry", "0", "--concurrency", "1"], |
| 191 | { |
| 192 | cwd: root, |
| 193 | stdout: "pipe", |
| 194 | stderr: "pipe", |
| 195 | env: { ...process.env, NO_COLOR: "1" }, |
| 196 | }, |
| 197 | ); |
| 198 | try { |
| 199 | // Poll the run log (created under the child's cwd) for the sentinel. |
| 200 | // The hook sleeps 8s after logging; seeing the sentinel within ~6s |
| 201 | // proves it was streamed mid-hook, not flushed at FileEnd. |
| 202 | const logDir = resolve(root, ".alchemy", "log", "test"); |
| 203 | const deadline = Date.now() + 6_000; |
| 204 | let streamed = false; |
| 205 | while (Date.now() < deadline) { |
| 206 | const { readdir, readFile } = await import("node:fs/promises"); |
| 207 | const entries = await readdir(logDir).catch(() => [] as string[]); |
| 208 | for (const entry of entries) { |
| 209 | const content = await readFile( |
| 210 | resolve(logDir, entry), |
| 211 | "utf8", |
| 212 | ).catch(() => ""); |
| 213 | if (content.includes("hook-live-sentinel")) { |
| 214 | streamed = true; |
| 215 | break; |
| 216 | } |
| 217 | } |
| 218 | if (streamed) break; |
| 219 | await new Promise((r) => setTimeout(r, 200)); |
| 220 | } |
| 221 | expect(streamed).toBe(true); |
| 222 | |
| 223 | const exitCode = await child.exited; |
| 224 | expect(exitCode).toBe(0); |
| 225 | } finally { |
| 226 | child.kill(); |
| 227 | } |
| 228 | } finally { |
| 229 | await rm(root, { recursive: true, force: true }); |
| 230 | } |
| 231 | }, |
| 232 | { tags: ["unit", "local"] }, |
| 233 | ); |
| 234 | |
| 235 | it( |
| 236 | "--exclude skips folders unless they are passed explicitly", |
| 237 | async () => { |
| 238 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-exclude-")); |
| 239 | try { |
| 240 | const { mkdir } = await import("node:fs/promises"); |
| 241 | await mkdir(resolve(root, "test", "Railway"), { recursive: true }); |
| 242 | await mkdir(resolve(root, "test", "Other"), { recursive: true }); |
| 243 | const testFile = (name: string) => ` |
| 244 | import { it } from ${JSON.stringify(apiUrl)}; |
| 245 | it(${JSON.stringify(name)}, () => {}); |
| 246 | `; |
| 247 | await Promise.all([ |
| 248 | writeFile( |
| 249 | resolve(root, "test", "Railway", "Excluded.test.ts"), |
| 250 | testFile("excluded-test"), |
| 251 | ), |
| 252 | writeFile( |
| 253 | resolve(root, "test", "Other", "Included.test.ts"), |
| 254 | testFile("included-test"), |
| 255 | ), |
| 256 | ]); |
| 257 | |
| 258 | const runCli = async (args: ReadonlyArray<string>) => { |
| 259 | const child = Bun.spawn( |
| 260 | [ |
| 261 | process.execPath, |
| 262 | cli, |
| 263 | ...args, |
| 264 | "--retry", |
| 265 | "0", |
| 266 | "--concurrency", |
| 267 | "1", |
| 268 | ], |
| 269 | { |
| 270 | cwd: root, |
| 271 | stdout: "pipe", |
| 272 | stderr: "pipe", |
| 273 | env: { ...process.env, NO_COLOR: "1" }, |
| 274 | }, |
| 275 | ); |
| 276 | const [exitCode, stdout, stderr] = await Promise.all([ |
| 277 | child.exited, |
| 278 | new Response(child.stdout).text(), |
| 279 | new Response(child.stderr).text(), |
| 280 | ]); |
| 281 | return { exitCode, output: `${stdout}\n${stderr}` }; |
| 282 | }; |
| 283 | |
| 284 | // Default discovery with the exclusion: only the non-excluded file runs. |
| 285 | const excluded = await runCli(["--exclude", "test/Railway"]); |
| 286 | expect(excluded.exitCode).toBe(0); |
| 287 | expect(excluded.output).toContain("included-test"); |
| 288 | expect(excluded.output).not.toContain("excluded-test"); |
| 289 | |
| 290 | // An explicit positional root inside the excluded path overrides it. |
| 291 | const explicit = await runCli([ |
| 292 | "test/Railway/Excluded.test.ts", |
| 293 | "--exclude", |
| 294 | "test/Railway", |
| 295 | ]); |
| 296 | expect(explicit.exitCode).toBe(0); |
| 297 | expect(explicit.output).toContain("excluded-test"); |
| 298 | |
| 299 | // Non-path excludes degrade to case-insensitive substring filters. |
| 300 | const substring = await runCli(["--exclude", "railway"]); |
| 301 | expect(substring.exitCode).toBe(0); |
| 302 | expect(substring.output).toContain("included-test"); |
| 303 | expect(substring.output).not.toContain("excluded-test"); |
| 304 | } finally { |
| 305 | await rm(root, { recursive: true, force: true }); |
| 306 | } |
| 307 | }, |
| 308 | { tags: ["unit", "local"] }, |
| 309 | ); |
| 310 | |
| 311 | it( |
| 312 | "fails the process for every hook kind and preserves hook output", |
| 313 | async () => { |
| 314 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-hooks-")); |
| 315 | try { |
| 316 | await Promise.all([ |
| 317 | writeFile( |
| 318 | resolve(root, "before-all.test.ts"), |
| 319 | fixture("beforeAll", 'it("body", () => {});'), |
| 320 | ), |
| 321 | writeFile( |
| 322 | resolve(root, "before-each.test.ts"), |
| 323 | fixture("beforeEach", 'it("body", () => {});'), |
| 324 | ), |
| 325 | writeFile( |
| 326 | resolve(root, "after-each.test.ts"), |
| 327 | fixture( |
| 328 | "afterEach", |
| 329 | 'it.fails("expected body failure", () => { throw new Error("expected-body-failure"); });', |
| 330 | ), |
| 331 | ), |
| 332 | writeFile( |
| 333 | resolve(root, "after-all.test.ts"), |
| 334 | fixture("afterAll", 'it("body", () => {});'), |
| 335 | ), |
| 336 | ]); |
| 337 | |
| 338 | const child = Bun.spawn( |
| 339 | [process.execPath, cli, root, "--retry", "0", "--concurrency", "1"], |
| 340 | { |
| 341 | cwd: root, |
| 342 | stdout: "pipe", |
| 343 | stderr: "pipe", |
| 344 | env: { ...process.env, NO_COLOR: "1" }, |
| 345 | }, |
| 346 | ); |
| 347 | const [exitCode, stdout, stderr] = await Promise.all([ |
| 348 | child.exited, |
| 349 | new Response(child.stdout).text(), |
| 350 | new Response(child.stderr).text(), |
| 351 | ]); |
| 352 | const output = `${stdout}\n${stderr}`; |
| 353 | |
| 354 | expect(exitCode).toBe(1); |
| 355 | expect(output).toContain("beforeAll hook failed:"); |
| 356 | expect(output).toContain("beforeEach hook failed:"); |
| 357 | expect(output).toContain("afterEach hook failed:"); |
| 358 | expect(output).toContain("afterAll hook failed:"); |
| 359 | expect(output).toContain("Tests: 4 failed | 1 passed"); |
| 360 | for (const hook of ["beforeAll", "beforeEach", "afterEach", "afterAll"]) { |
| 361 | expect(output).toContain(`${hook}-captured-output`); |
| 362 | expect(output).toContain(`${hook}-sentinel`); |
| 363 | } |
| 364 | } finally { |
| 365 | await rm(root, { recursive: true, force: true }); |
| 366 | } |
| 367 | }, |
| 368 | { tags: ["unit", "local"] }, |
| 369 | ); |
| 370 | |
| 371 | it( |
| 372 | "runs every afterAll hook even when an earlier one fails", |
| 373 | async () => { |
| 374 | // Regression: `runAfterAll` used to short-circuit on the first failing |
| 375 | // hook, so a failing teardown assertion silently dropped every later |
| 376 | // afterAll — in particular Test.make's fallback hook that closes the |
| 377 | // shared scope and local provider sidecar, leaking the sidecar for the |
| 378 | // rest of the process. All teardown hooks must run; failures aggregate. |
| 379 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-afterall-")); |
| 380 | try { |
| 381 | await writeFile( |
| 382 | resolve(root, "teardown-chain.test.ts"), |
| 383 | ` |
| 384 | import { it, registerHook } from ${JSON.stringify(apiUrl)}; |
| 385 | import * as Effect from ${JSON.stringify(effectUrl)}; |
| 386 | registerHook("afterAll", { body: () => Effect.gen(function* () { |
| 387 | return yield* Effect.fail(new Error("first-teardown-failed")); |
| 388 | }) }); |
| 389 | registerHook("afterAll", { body: () => Effect.gen(function* () { |
| 390 | yield* Effect.log("second-teardown-ran"); |
| 391 | }) }); |
| 392 | it("body", () => {}); |
| 393 | `, |
| 394 | ); |
| 395 | |
| 396 | const child = Bun.spawn( |
| 397 | [process.execPath, cli, root, "--retry", "0", "--concurrency", "1"], |
| 398 | { |
| 399 | cwd: root, |
| 400 | stdout: "pipe", |
| 401 | stderr: "pipe", |
| 402 | env: { ...process.env, NO_COLOR: "1" }, |
| 403 | }, |
| 404 | ); |
| 405 | const [exitCode, stdout, stderr] = await Promise.all([ |
| 406 | child.exited, |
| 407 | new Response(child.stdout).text(), |
| 408 | new Response(child.stderr).text(), |
| 409 | ]); |
| 410 | const output = `${stdout}\n${stderr}`; |
| 411 | |
| 412 | // The failure is reported and fails the run… |
| 413 | expect(exitCode).toBe(1); |
| 414 | expect(output).toContain("afterAll hook failed:"); |
| 415 | expect(output).toContain("first-teardown-failed"); |
| 416 | // …and the later teardown hook still ran. |
| 417 | expect(output).toContain("second-teardown-ran"); |
| 418 | } finally { |
| 419 | await rm(root, { recursive: true, force: true }); |
| 420 | } |
| 421 | }, |
| 422 | { tags: ["unit", "local"] }, |
| 423 | ); |
| 424 | |
| 425 | it( |
| 426 | "attributes imported tests and deferred hooks to their own files", |
| 427 | async () => { |
| 428 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-collection-")); |
| 429 | try { |
| 430 | for (const name of ["first", "second"]) { |
| 431 | const file = `${name}.test.ts`; |
| 432 | await writeFile( |
| 433 | resolve(root, file), |
| 434 | ` |
| 435 | import { beforeAll, afterAll, currentFile, describe, expect, it } from ${JSON.stringify(apiUrl)}; |
| 436 | expect(currentFile()?.split("/").pop()).toBe(${JSON.stringify(file)}); |
| 437 | await new Promise(resolve => setTimeout(resolve, 10)); |
| 438 | expect(currentFile()?.split("/").pop()).toBe(${JSON.stringify(file)}); |
| 439 | let ready = false; |
| 440 | let ran = false; |
| 441 | queueMicrotask(() => { |
| 442 | expect(currentFile()?.split("/").pop()).toBe(${JSON.stringify(file)}); |
| 443 | beforeAll(() => { ready = true; }); |
| 444 | afterAll(() => { expect(ran).toBe(true); }); |
| 445 | }); |
| 446 | describe("nested", () => { |
| 447 | it(${JSON.stringify(name)}, () => { |
| 448 | expect(currentFile()).toBeUndefined(); |
| 449 | expect(ready).toBe(true); |
| 450 | ran = true; |
| 451 | }); |
| 452 | }); |
| 453 | `, |
| 454 | ); |
| 455 | } |
| 456 | const child = Bun.spawn( |
| 457 | [process.execPath, cli, root, "--retry", "0", "--concurrency", "2"], |
| 458 | { |
| 459 | cwd: root, |
| 460 | stdout: "pipe", |
| 461 | stderr: "pipe", |
| 462 | env: { ...process.env, NO_COLOR: "1" }, |
| 463 | }, |
| 464 | ); |
| 465 | const [exitCode, stdout, stderr] = await Promise.all([ |
| 466 | child.exited, |
| 467 | new Response(child.stdout).text(), |
| 468 | new Response(child.stderr).text(), |
| 469 | ]); |
| 470 | expect(`${stdout}\n${stderr}`).toContain( |
| 471 | "first.test.ts > nested > first", |
| 472 | ); |
| 473 | expect(`${stdout}\n${stderr}`).toContain( |
| 474 | "second.test.ts > nested > second", |
| 475 | ); |
| 476 | expect(exitCode).toBe(0); |
| 477 | } finally { |
| 478 | await rm(root, { recursive: true, force: true }); |
| 479 | } |
| 480 | }, |
| 481 | { tags: ["unit", "local"] }, |
| 482 | ); |
| 483 | |
| 484 | it( |
| 485 | "opt-in tags gate execution and hooks until explicitly selected", |
| 486 | async () => { |
| 487 | const root = await mkdtemp(resolve(tmpdir(), "alchemy-test-opt-in-")); |
| 488 | const file = resolve(root, "opt-in.test.ts"); |
| 489 | try { |
| 490 | await writeFile( |
| 491 | file, |
| 492 | ` |
| 493 | import { describe, it, beforeAll, afterAll, expect } from ${JSON.stringify(apiUrl)}; |
| 494 | let ran = false; |
| 495 | describe("gated", { tags: ["e2e", "provider:aws"], optInTags: ["slow"] }, () => { |
| 496 | beforeAll(() => { if (!process.env.EXPECT_ENABLED) throw new Error("excluded setup ran"); }); |
| 497 | afterAll(() => { expect(ran).toBe(true); }); |
| 498 | it("single", () => { ran = true; }); |
| 499 | it("double", () => { ran = true; }, { optInTags: ["enterprise"] }); |
| 500 | }); |
| 501 | it("ordinary", () => {}, { tags: ["unit", "slow"] }); |
| 502 | `, |
| 503 | ); |
| 504 | const run = async (args: string[], enabled = false) => { |
| 505 | const child = Bun.spawn( |
| 506 | [process.execPath, cli, file, "--retry", "0", ...args], |
| 507 | { |
| 508 | cwd: root, |
| 509 | stdout: "pipe", |
| 510 | stderr: "pipe", |
| 511 | env: { |
| 512 | ...process.env, |
| 513 | NO_COLOR: "1", |
| 514 | EXPECT_ENABLED: enabled ? "1" : "", |
| 515 | }, |
| 516 | }, |
| 517 | ); |
| 518 | const [code, stdout, stderr] = await Promise.all([ |
| 519 | child.exited, |
| 520 | new Response(child.stdout).text(), |
| 521 | new Response(child.stderr).text(), |
| 522 | ]); |
| 523 | expect(code).toBe(0); |
| 524 | return `${stdout}\n${stderr}`; |
| 525 | }; |
| 526 | expect(await run([])).toContain("1 passed"); |
| 527 | expect(await run(["--tags", "*"])).toContain("1 passed"); |
| 528 | for (const filter of ["provider:aws", "!unit", "sl*", "!!slow"]) { |
| 529 | expect(await run(["--tags", filter, "-t", "gated"])).toContain( |
| 530 | "0 passed", |
| 531 | ); |
| 532 | } |
| 533 | expect(await run(["-t", "gated"])).toContain("0 passed"); |
| 534 | expect(await run(["--tags", "slow && provider:aws"], true)).toContain( |
| 535 | "1 passed", |
| 536 | ); |
| 537 | expect( |
| 538 | await run(["--tags", "slow", "--tags", "enterprise"], true), |
| 539 | ).toContain("1 passed"); |
| 540 | await writeFile( |
| 541 | file, |
| 542 | ` |
| 543 | import { it } from ${JSON.stringify(apiUrl)}; |
| 544 | it.only("gated", () => { throw new Error("only bypassed opt-in"); }, { optInTags: ["slow"] }); |
| 545 | `, |
| 546 | ); |
| 547 | expect(await run([])).toContain("0 passed"); |
| 548 | } finally { |
| 549 | await rm(root, { recursive: true, force: true }); |
| 550 | } |
| 551 | }, |
| 552 | { tags: ["unit", "local"], timeout: 90_000 }, |
| 553 | ); |