@zerotal/arch 1.7.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,153 @@
1
+ ---
2
+ title: Browser Tests
3
+ description: Drive a real browser against a running Zerotal app with Playwright to test Flow and Inertia UIs end-to-end.
4
+ ---
5
+
6
+ # Browser Tests
7
+
8
+ Browser tests exercise the parts of your app that only exist in a real browser, by
9
+ driving a live server with Playwright and asserting on what the user sees.
10
+
11
+ Some behavior only exists in a real browser — [Flow](/docs/flow)'s WebSocket
12
+ bridge and Alpine runtime, [Inertia](/docs/inertia) client navigation, focus and
13
+ keyboard handling. This page covers **[Playwright](https://playwright.dev)** pointed
14
+ at a running server, which is what to use for cross-browser end-to-end coverage.
15
+
16
+ > **Testing Flow's bridge?** [`FlowBrowser`](/docs/testing/flow-browser) ships in
17
+ > `@zerotal/testing`, needs no extra dependency, and runs inside `bun test` alongside
18
+ > the rest of your suite. It drives a headless page against a real server and can
19
+ > assert on the WebSocket transport itself — whether the socket upgraded, what frames
20
+ > crossed the wire. Reach for Playwright when you need several browsers, or coverage
21
+ > beyond Flow.
22
+
23
+ > **Note** — Unit and [HTTP tests](/docs/testing/http) run on Bun's test runner. Browser tests
24
+ > run under Playwright's own runner (`*.e2e.ts` / `*.spec.ts`), separately from
25
+ > `bun test`.
26
+
27
+ ## Setup
28
+
29
+ Install Playwright in your app:
30
+
31
+ ```bash
32
+ # in your project root
33
+ bun add -d @playwright/test
34
+ bunx playwright install --with-deps
35
+ ```
36
+
37
+ Add a `playwright.config.ts` that boots your app with the `webServer` option, so the
38
+ server starts once for the whole run and is torn down after:
39
+
40
+ ```typescript
41
+ // playwright.config.ts
42
+ import { defineConfig, devices } from "@playwright/test";
43
+
44
+ const PORT = 3000;
45
+ const BASE_URL = `http://localhost:${PORT}`;
46
+
47
+ export default defineConfig({
48
+ testDir: "./e2e",
49
+ testMatch: "**/*.e2e.ts",
50
+ timeout: 30_000,
51
+ use: { baseURL: BASE_URL, trace: "on-first-retry" },
52
+ projects: [
53
+ { name: "chromium", use: { ...devices["Desktop Chrome"] } },
54
+ { name: "firefox", use: { ...devices["Desktop Firefox"] } },
55
+ { name: "webkit", use: { ...devices["Desktop Safari"] } },
56
+ ],
57
+ // Boot the server once for the run (no --hot, to avoid reload flakiness)
58
+ webServer: {
59
+ command: "bun run start",
60
+ url: BASE_URL,
61
+ reuseExistingServer: !process.env.CI,
62
+ timeout: 120_000,
63
+ },
64
+ });
65
+ ```
66
+
67
+ Wire up scripts in `package.json`:
68
+
69
+ ```json
70
+ // package.json
71
+ {
72
+ "scripts": {
73
+ "test:e2e": "playwright test",
74
+ "test:e2e:ui": "playwright test --ui"
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Writing a test
80
+
81
+ Specs live in `e2e/` and use Playwright's `test`/`expect`. Drive the page through
82
+ roles and assert on what the user sees:
83
+
84
+ ```typescript
85
+ // e2e/navigate.e2e.ts
86
+ import { test, expect } from "@playwright/test";
87
+
88
+ test.describe("SPA navigation", () => {
89
+ test("swaps content without a full page reload", async ({ page }) => {
90
+ await page.goto("/pulse/counter");
91
+ // A full reload would clear this marker; an SPA swap keeps it.
92
+ await page.evaluate(() => ((window as any).__spa = true));
93
+
94
+ await page.getByRole("link", { name: "Components" }).click();
95
+ await expect(page).toHaveURL(/\/pulse\/components/);
96
+ await expect(page.getByRole("heading", { name: "Native Components" })).toBeVisible();
97
+
98
+ const kept = await page.evaluate(() => (window as any).__spa);
99
+ expect(kept).toBe(true); // no full reload happened
100
+ });
101
+ });
102
+ ```
103
+
104
+ ```bash
105
+ # in your project root
106
+ bun run test:e2e
107
+ ```
108
+
109
+ ## Waiting for the client to boot
110
+
111
+ Flow pages finish wiring once Alpine has walked the DOM and fired
112
+ `alpine:initialized`. A fast click can land before that and silently no-op (a flaky,
113
+ browser-dependent failure). Wait for readiness before interacting:
114
+
115
+ ```typescript
116
+ // e2e/support/gotoReady.ts
117
+ import type { Page } from "@playwright/test";
118
+
119
+ export async function gotoReady(page: Page, url: string): Promise<void> {
120
+ await page.addInitScript(() => {
121
+ (window as any).__alpineReady = false;
122
+ document.addEventListener("alpine:initialized", () => ((window as any).__alpineReady = true));
123
+ });
124
+ await page.goto(url);
125
+ await page.waitForFunction(() => (window as any).__alpineReady === true, null, {
126
+ timeout: 10_000,
127
+ });
128
+ }
129
+ ```
130
+
131
+ Then `await gotoReady(page, "/pulse/counter")` instead of `page.goto(...)` for
132
+ interactive Flow pages.
133
+
134
+ > **Warning** — Clicking before `alpine:initialized` fires is the most common source
135
+ > of browser-test flakiness. Use `gotoReady` (or an equivalent readiness wait) on any
136
+ > page driven by Flow's Alpine runtime.
137
+
138
+ ## Tips
139
+
140
+ - **Seed deterministic state.** If a flow needs data, seed it before the run (or use
141
+ pages backed by in-memory state). Browser tests don't share the transactional
142
+ rollback that [database tests](/docs/testing/database) use.
143
+ - **Prefer role/label selectors** (`getByRole`, `getByLabel`) over CSS — they're
144
+ resilient to markup changes and assert accessibility.
145
+ - **Run cross-browser in CI.** The three projects above cover Chromium, Firefox, and
146
+ WebKit; enable retries on CI to absorb transient flakiness.
147
+
148
+ ## Next steps
149
+
150
+ - [HTTP Tests](/docs/testing/http) — faster tests for anything not needing a browser.
151
+ - [Database Tests](/docs/testing/database) — seed and roll back data for backed flows.
152
+ - [Flow](/docs/flow) / [Inertia](/docs/inertia) — the client runtimes these tests exercise.
153
+ - [Deployment](/docs/deployment) — `bun run start`, the command the `webServer` boots.
@@ -0,0 +1,130 @@
1
+ ---
2
+ title: Console Tests
3
+ description: Run CLI commands in-process from a test and assert their output, exit code, and side effects.
4
+ ---
5
+
6
+ # Console Tests
7
+
8
+ Console tests exercise your [CLI commands](/docs/commands) — generators, migrations,
9
+ maintenance tasks — without spawning a subprocess. `Artisan.call()` runs a registered
10
+ command **in-process**, captures everything it printed, and returns its exit code.
11
+
12
+ ```typescript
13
+ // in a test file
14
+ import { Artisan } from "zerotal";
15
+ ```
16
+
17
+ > **Note** — `Artisan.call()` needs a `CommandRunner` bound under `commands` in the
18
+ > container — present in `web` and `console` runtime modes. When you boot the app with
19
+ > [`createTestApp()`](/docs/testing/http), commands are registered for you.
20
+
21
+ ## Running a command
22
+
23
+ `Artisan.call(name, parameters?)` returns `{ code, output }` — `code` is `0` on
24
+ success, and `output` is everything the command wrote via `info`/`warn`/`error`/etc.:
25
+
26
+ ```typescript
27
+ // src/tests/CacheClearTest.ts
28
+ import { describe, it, expect } from "bun:test";
29
+ import { Artisan } from "zerotal";
30
+
31
+ describe("cache:clear", () => {
32
+ it("clears the cache and reports success", async () => {
33
+ const { code, output } = await Artisan.call("cache:clear");
34
+
35
+ expect(code).toBe(0);
36
+ expect(output).toContain("Cache cleared");
37
+ });
38
+ });
39
+ ```
40
+
41
+ Pass arguments and flags as a parameter map. Booleans become bare flags when `true`;
42
+ everything else becomes a `key=value` token, mirroring how the CLI parses argv:
43
+
44
+ ```typescript
45
+ // in a test file
46
+ await Artisan.call("migrate", { "--fresh": true });
47
+ await Artisan.call("make:model", { name: "Post", "--migration": true });
48
+ ```
49
+
50
+ Output is captured, so nothing leaks to the test runner's console.
51
+
52
+ ## Asserting side effects
53
+
54
+ Commands usually do work — touch the database, queue a job, write a file. Assert the
55
+ _effect_, not just the output, using the [database](/docs/testing/database) and
56
+ [mocking](/docs/testing/mocking) helpers:
57
+
58
+ ```typescript
59
+ // src/tests/PruneUsersTest.ts
60
+ import { Artisan } from "zerotal";
61
+ import { assertDatabaseCount, QueueFake } from "@zerotal/testing";
62
+
63
+ it("prune:users deletes stale accounts and queues a report", async () => {
64
+ const queue = QueueFake.install();
65
+
66
+ const { code } = await Artisan.call("prune:users", { "--days": 30 });
67
+
68
+ expect(code).toBe(0);
69
+ await assertDatabaseCount("users", 5);
70
+ queue.assertDispatched(PruneReportJob);
71
+
72
+ queue.restore();
73
+ });
74
+ ```
75
+
76
+ ## Unit-testing a command class
77
+
78
+ For a focused test of a single command's logic, instantiate it directly and inject a
79
+ `BufferWriter` to capture output. The `_writer`, `args`, and `flags` fields are
80
+ internal — the runner normally sets them — so a unit test reaches in with a cast:
81
+
82
+ ```typescript
83
+ // src/tests/GreetCommandTest.ts
84
+ import { expect } from "bun:test";
85
+ import { BufferWriter } from "zerotal";
86
+ import { GreetCommand } from "../app/commands/GreetCommand.ts";
87
+
88
+ const cmd = new GreetCommand();
89
+ const buffer = new BufferWriter();
90
+
91
+ // These are internal fields the CommandRunner usually populates:
92
+ (cmd as any)._writer = buffer; // capture output instead of printing
93
+ (cmd as any).args = { name: "Alice" };
94
+ (cmd as any).flags = {};
95
+
96
+ await cmd.run();
97
+
98
+ expect(buffer.flush()).toContain("Hello, Alice");
99
+ ```
100
+
101
+ > **Tip** — Prefer `Artisan.call()` for end-to-end command behavior — it wires the
102
+ > runner for you. Reach for direct instantiation only when you deliberately want to
103
+ > bypass the runner.
104
+
105
+ ## Which approach should I use?
106
+
107
+ - **`Artisan.call(name, params?)`** — the default. Use it whenever you want the real
108
+ command behavior: argument parsing, flag defaults, container resolution, and the
109
+ command's full `run()`. Assert on `code` and `output`, plus any side effects.
110
+ - **Direct instantiation + `BufferWriter`** — only for unit-testing one command's
111
+ logic in isolation. You bypass the runner, so you must set `_writer`, `args`, and
112
+ `flags` yourself.
113
+
114
+ ## References
115
+
116
+ The console-testing surface lives in `@zerotal/core`:
117
+
118
+ | Member | Signature | Description |
119
+ | -------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
120
+ | `Artisan.call` | `call(name: string, parameters?: Record<string, string \| boolean \| number>): Promise<ArtisanResult>` | Run a registered command in-process and capture its output. |
121
+ | `ArtisanResult` | `{ code: number; output: string }` | Exit code (`0` = success) and the captured output string. |
122
+ | `BufferWriter` | `class BufferWriter implements OutputWriter` | Capturing writer; collects output in memory instead of printing. |
123
+ | `BufferWriter.flush` | `flush(): string` | Return all captured output as one string and clear the buffer. |
124
+
125
+ ## Next steps
126
+
127
+ - [Commands](/docs/commands) — writing and registering commands.
128
+ - [Database](/docs/testing/database) — asserting a command's DB effects.
129
+ - [Mocking](/docs/testing/mocking) — asserting queued jobs and sent mail.
130
+ - [HTTP Tests](/docs/testing/http) — boot the app and exercise it over real requests.
@@ -0,0 +1,238 @@
1
+ ---
2
+ title: Database Testing
3
+ description: Keep database tests isolated with per-test transactional rollback and assert on rows and stored files.
4
+ ---
5
+
6
+ # Database Testing
7
+
8
+ Tests that touch the database must stay isolated — one test's writes can't leak into
9
+ the next. Zerotal wraps each test in a transaction that **rolls back** when it finishes,
10
+ and ships assertions for checking rows and stored files.
11
+
12
+ ```typescript
13
+ // in a test file
14
+ import {
15
+ migrateDatabase,
16
+ refreshDatabase,
17
+ withDatabase,
18
+ assertDatabaseHas,
19
+ assertDatabaseMissing,
20
+ assertDatabaseCount,
21
+ } from "@zerotal/testing";
22
+ ```
23
+
24
+ > **Note** — These helpers are part of the [testing toolkit](/docs/testing/index) and
25
+ > only run inside `bun test`. Install the package as a dev dependency if it isn't
26
+ > already: `bun add -d @zerotal/testing`.
27
+
28
+ ## Building the schema
29
+
30
+ A test needs tables before it needs rows. Build them by running the project's own
31
+ [migrations](/docs/migrations) rather than by writing the schema a second time:
32
+
33
+ ```typescript
34
+ // tests/Feature/PostTest.ts
35
+ import { migrateDatabase } from "@zerotal/testing";
36
+
37
+ beforeAll(async () => {
38
+ app = await createApp();
39
+ await migrateDatabase();
40
+ });
41
+ ```
42
+
43
+ A hand-written `CREATE TABLE` in a test is a second definition of the same tables,
44
+ and the two drift. A column added in a migration is missing from the test schema
45
+ until something fails for a reason that has nothing to do with the change you
46
+ made. Running the migrations themselves means the schema under test is the schema
47
+ that ships.
48
+
49
+ ```typescript
50
+ // signature
51
+ function migrateDatabase(options?: MigrateDatabaseOptions): Promise<string[]>;
52
+ ```
53
+
54
+ | Option | Type | Default | Purpose |
55
+ | ------------ | ------------- | ----------------------- | ------------------------------- |
56
+ | `connection` | `SQLInstance` | the active connection | Which database to migrate. |
57
+ | `path` | `string` | `"database/migrations"` | Where the migration files live. |
58
+ | `table` | `string` | `"migrations"` | Tracking-table name. |
59
+
60
+ It returns the names of the migrations it applied, and is idempotent — a second
61
+ call applies nothing. `refreshDatabase({ migrate: true })` runs it for you, which
62
+ is usually what you want:
63
+
64
+ ```typescript
65
+ // in a test file
66
+ describe("Post", () => {
67
+ refreshDatabase({ connection: db, migrate: true });
68
+ // …the real schema, and every test rolled back after itself
69
+ });
70
+ ```
71
+
72
+ For a file-backed or server test database — where migrating once up front beats
73
+ migrating per test file — `bun zt test --migrate` applies them before the suite
74
+ starts. An `:memory:` database belongs to the process that opened it, so there is
75
+ nothing for a parent process to migrate; use `refreshDatabase({ migrate: true })`
76
+ there.
77
+
78
+ ## Which rollback helper do I use?
79
+
80
+ Both run your test inside a transaction that rolls back, leaving the database
81
+ untouched. Choose by scope:
82
+
83
+ - **`refreshDatabase`** — call once in a `describe` block to roll back _every_ test in
84
+ it, and (optionally) install a connection and run schema setup once. Reach for this
85
+ in feature suites where many tests share the same tables.
86
+ - **`withDatabase`** — wrap a _single_ `it()` body when only that one test needs
87
+ rollback. No `describe`-level setup.
88
+
89
+ ## refreshDatabase — suite-level rollback
90
+
91
+ Call it once inside a `describe` block. Every `it()` in that block runs inside a
92
+ transaction that rolls back when the test ends, so each test starts from the same
93
+ clean baseline:
94
+
95
+ ```typescript
96
+ // tests/Feature/UserTest.ts
97
+ import { describe, it } from "bun:test";
98
+ import { refreshDatabase, assertDatabaseHas, assertDatabaseMissing } from "@zerotal/testing";
99
+ import { SQL } from "bun";
100
+ import { User } from "../app/models/User.ts";
101
+
102
+ const db = new SQL(":memory:");
103
+
104
+ describe("User", () => {
105
+ refreshDatabase({ connection: db, migrate: true });
106
+
107
+ it("creates a user", async () => {
108
+ await User.create({ email: "a@b.com" });
109
+ await assertDatabaseHas("users", { email: "a@b.com" });
110
+ }); // ← rolled back; next test starts clean
111
+
112
+ it("starts clean", async () => {
113
+ await assertDatabaseMissing("users", { email: "a@b.com" });
114
+ });
115
+ });
116
+ ```
117
+
118
+ ```typescript
119
+ // signature
120
+ function refreshDatabase(options?: RefreshDatabaseOptions): void;
121
+ ```
122
+
123
+ | Option | Type | Required | Purpose |
124
+ | ------------ | ------------------------------ | -------- | ------------------------------------------------------------------------------------ |
125
+ | `connection` | `SQLInstance` | no | Install as the model connection for this suite. Omit to use the existing connection. |
126
+ | `migrate` | `boolean \| string` | no | Run the project's migrations first. A string names a different directory. |
127
+ | `setup` | `(db: SQLInstance) => unknown` | no | Runs once before the suite and is **committed**, so every test sees it. |
128
+ | `teardown` | `(db: SQLInstance) => unknown` | no | Runs once after all tests complete. |
129
+
130
+ Both `migrate` and `setup` are committed (they are the schema and any shared
131
+ fixtures), while each test's own writes happen inside the rolled-back transaction.
132
+
133
+ > **Warning** — `refreshDatabase()` only works inside a `bun test` run; it pulls
134
+ > `beforeAll`/`afterEach` from `bun:test`. Calling it outside the test runner throws.
135
+
136
+ ## withDatabase — per-test wrapper
137
+
138
+ When you want rollback for a single test rather than a whole block, wrap the test body:
139
+
140
+ ```typescript
141
+ // in a test file
142
+ import { withDatabase } from "@zerotal/testing";
143
+
144
+ it(
145
+ "creates a user",
146
+ withDatabase(async () => {
147
+ await User.create({ email: "a@b.com" });
148
+ await assertDatabaseHas("users", { email: "a@b.com" });
149
+ }),
150
+ ); // ← transaction rolled back here
151
+ ```
152
+
153
+ ```typescript
154
+ // signature
155
+ function withDatabase(fn: () => Promise<void>): () => Promise<void>;
156
+ ```
157
+
158
+ A test failure inside the callback still rolls the transaction back, then re-throws so
159
+ the assertion is reported.
160
+
161
+ ## Database assertions
162
+
163
+ Each assertion runs a `COUNT(*)` against the current connection and throws a
164
+ descriptive error when the expectation fails:
165
+
166
+ ```typescript
167
+ // in a test file
168
+ import { assertDatabaseHas, assertDatabaseMissing, assertDatabaseCount } from "@zerotal/testing";
169
+
170
+ await assertDatabaseHas("users", { email: "alice@example.com", role: "admin" });
171
+ await assertDatabaseMissing("users", { email: "deleted@example.com" });
172
+ await assertDatabaseCount("posts", 5);
173
+ await assertDatabaseCount("posts", 2, { published: 1 }); // optional where filter
174
+ ```
175
+
176
+ | Function | Throws when |
177
+ | --------------------------------------- | --------------------------------- |
178
+ | `assertDatabaseHas(table, where)` | No row matches `where`. |
179
+ | `assertDatabaseMissing(table, where)` | A matching row exists. |
180
+ | `assertDatabaseCount(table, n, where?)` | The matching row count isn't `n`. |
181
+
182
+ ## Storage assertions
183
+
184
+ For tests that write to a [storage](/docs/storage) disk, assert file presence without
185
+ reading bytes. Pass a disk name or a `StorageDriver` instance:
186
+
187
+ ```typescript
188
+ // in a test file
189
+ import { assertStoredFile, assertMissingFile } from "@zerotal/testing";
190
+
191
+ await assertStoredFile("local", "uploads/avatar.jpg");
192
+ await assertMissingFile("local", "uploads/old.jpg");
193
+ ```
194
+
195
+ > **Note** — A disk name routes through the `Storage` facade (needs
196
+ > `StorageProvider` registered); a `StorageDriver` instance lets isolated unit tests
197
+ > skip the facade entirely.
198
+
199
+ ## Arranging rows
200
+
201
+ Use [factories](/docs/orm/factories) to build the records a test acts on, and
202
+ [seeders](/docs/seeding) for shared fixtures:
203
+
204
+ ```typescript
205
+ // in a test file
206
+ import { UserFactory } from "../database/factories/UserFactory.ts";
207
+ import { PostFactory } from "../database/factories/PostFactory.ts";
208
+
209
+ const editor = await UserFactory.state("editor").create();
210
+ const posts = await PostFactory.for(editor).count(3).create();
211
+ ```
212
+
213
+ Factories suppress model observers by default, so seeding rows doesn't fire emails or
214
+ jobs — call `.dispatchEvents()` when a test needs the full lifecycle.
215
+
216
+ > **Tip** — `.for(editor)` injects the parent's primary key as the `<Model>Id`
217
+ > foreign key, so child rows line up with the record you created.
218
+
219
+ ## References
220
+
221
+ | Function | Signature | Description |
222
+ | ----------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
223
+ | `migrateDatabase` | `(options?: MigrateDatabaseOptions) => Promise<string[]>` | Build the schema by running the project's migrations. |
224
+ | `refreshDatabase` | `(options?: RefreshDatabaseOptions) => void` | Roll back every test in a `describe` block; optional connection + setup. |
225
+ | `withDatabase` | `(fn: () => Promise<void>) => () => Promise<void>` | Wrap one `it()` body in a transaction that always rolls back. |
226
+ | `assertDatabaseHas` | `(table: string, where: Record<string, unknown>) => Promise<void>` | Pass when at least one row matches `where`. |
227
+ | `assertDatabaseMissing` | `(table: string, where: Record<string, unknown>) => Promise<void>` | Pass when no row matches `where`. |
228
+ | `assertDatabaseCount` | `(table: string, expected: number, where?: Record<string, unknown>) => Promise<void>` | Pass when the matching row count equals `expected`. |
229
+ | `assertStoredFile` | `(diskOrDriver: string \| StorageDriver, path: string) => Promise<void>` | Pass when the file exists on the disk or driver. |
230
+ | `assertMissingFile` | `(diskOrDriver: string \| StorageDriver, path: string) => Promise<void>` | Pass when the file is absent from the disk or driver. |
231
+
232
+ ## Next steps
233
+
234
+ - [Testing overview](/docs/testing/index) — the full toolkit and a first test.
235
+ - [Factories](/docs/orm/factories) — generating model records for tests.
236
+ - [Seeding](/docs/seeding) — reusable fixtures across dev and tests.
237
+ - [Database](/docs/database) — transactions and the raw `DB` layer being rolled back.
238
+ - [HTTP Tests](/docs/testing/http) — drive these rolled-back rows through real requests.