@jskit-ai/agent-docs 0.1.280 → 0.1.282
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/guide/agent/app-extras/assistant-conversation.md +5 -1
- package/guide/agent/app-extras/realtime.md +20 -10
- package/guide/agent/app-setup/authentication.md +20 -1
- package/guide/agent/app-setup/database-layer.md +75 -31
- package/guide/agent/app-setup/existing-application-migration.md +7 -1
- package/guide/agent/app-setup/multi-homing.md +6 -2
- package/guide/agent/app-setup/upgrading-jskit.md +78 -0
- package/guide/agent/framework/client-features.md +35 -1
- package/guide/agent/framework/sanity-checks.md +4 -3
- package/guide/agent/framework/typed-features.md +79 -30
- package/package.json +1 -1
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/minimal-foundation/example/AGENTS.md +14 -7
- package/patterns/minimal-foundation/example/package.json +3 -3
- package/patterns/minimal-foundation/example/playwright.config.mjs +3 -33
- package/patterns/minimal-foundation/example/server.js +7 -1
- package/patterns/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/patterns/shell-foundation/example/AGENTS.md +14 -7
- package/patterns/shell-foundation/example/package.json +4 -4
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/playwright.config.mjs +3 -33
- package/patterns/shell-foundation/example/server.js +7 -1
- package/patterns/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/reference/autogen/KERNEL_MAP.md +2 -0
- package/reference/autogen/PATTERN_INDEX.md +37 -37
- package/reference/autogen/packages/architecture-inspector.md +1 -0
- package/reference/autogen/packages/assistant-runtime.md +1 -1
- package/reference/autogen/packages/auth-provider-local-core.md +2 -1
- package/reference/autogen/packages/connectors-catalog.md +8 -0
- package/reference/autogen/packages/database-runtime.md +33 -0
- package/reference/autogen/packages/http-runtime.md +1 -1
- package/reference/autogen/packages/kernel.md +29 -1
- package/reference/autogen/packages/oidc-issuer-core.md +0 -1
- package/reference/autogen/packages/realtime.md +8 -5
- package/reference/autogen/packages/shell-web.md +4 -3
- package/reference/autogen/packages/workspaces-core.md +1 -1
- package/reference/autogen/tooling/jskit-catalog.md +71 -1
- package/skills/jskit/SKILL.md +8 -6
- package/skills/jskit/references/client-features.md +35 -1
- package/skills/jskit/references/existing-application-migration.md +7 -1
- package/skills/jskit/references/heuristics/client-requests.md +1 -0
- package/skills/jskit/references/heuristics/typed-features.md +2 -2
- package/skills/jskit/references/heuristics/ui-testing.md +21 -16
- package/skills/jskit/references/multi-homing.md +6 -2
- package/skills/jskit/references/pattern-index.md +37 -37
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +14 -7
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +3 -3
- package/skills/jskit/references/patterns/app/minimal-foundation/example/playwright.config.mjs +3 -33
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +7 -1
- package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +14 -7
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +4 -4
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/playwright.config.mjs +3 -33
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +7 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
- package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +34 -24
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +3 -2
- package/skills/jskit/references/patterns/database/mysql-application/example/tests/e2e/databases.js +25 -0
- package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +12 -0
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +3 -2
- package/skills/jskit/references/patterns/database/postgres-application/example/tests/e2e/databases.js +25 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
- package/skills/jskit/references/sanity-checks.md +4 -3
- package/skills/jskit/references/typed-features.md +79 -30
- package/skills/jskit/references/patterns/database/mysql-application/example/tests/browser/database.js +0 -68
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@jskit-ai/connectors-core": "0.2.
|
|
11
|
-
"@jskit-ai/connector-google-calendar": "0.2.
|
|
12
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/database-runtime-mysql": "0.1.
|
|
10
|
+
"@jskit-ai/connectors-core": "0.2.14",
|
|
11
|
+
"@jskit-ai/connector-google-calendar": "0.2.14",
|
|
12
|
+
"@jskit-ai/database-runtime": "0.1.262",
|
|
13
|
+
"@jskit-ai/database-runtime-mysql": "0.1.260",
|
|
14
14
|
"knex": "^3.1.0"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"./shared": "./src/shared/index.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@jskit-ai/crud-core": "0.2.
|
|
13
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
12
|
+
"@jskit-ai/crud-core": "0.2.14",
|
|
13
|
+
"@jskit-ai/resource-crud-core": "0.1.204"
|
|
14
14
|
},
|
|
15
15
|
"jskit": {
|
|
16
16
|
"kind": "runtime",
|
|
@@ -61,27 +61,38 @@ metadata. Do not combine it with the PostgreSQL pattern's workflow: a MySQL
|
|
|
61
61
|
application's workflow has no `postgres` service, `POSTGRES_*` variables, port
|
|
62
62
|
`5432`, or `pg_isready` option.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
and
|
|
79
|
-
|
|
64
|
+
Tests get their own databases from
|
|
65
|
+
`@jskit-ai/database-runtime/server/testDatabases`. The `test` script runs
|
|
66
|
+
`jskit test --databases`: each Node test file running at once gets its own
|
|
67
|
+
copy of `TEST_DB_NAME` (`<name>`, `<name>_1`, ...), restored from a stored
|
|
68
|
+
snapshot of the current migrations' schema and the rows they write, in
|
|
69
|
+
`node_modules/.cache/jskit-test-databases/`. The snapshot is replayed once, and
|
|
70
|
+
again only when a migration's name or contents or `package-lock.json` change;
|
|
71
|
+
a file that migrates or drops tables leaves its copy to be restored before the
|
|
72
|
+
next file takes it.
|
|
73
|
+
|
|
74
|
+
`example/tests/e2e/databases.js` is the browser run's `prepare` module for
|
|
75
|
+
`browserTestConfig({ prepare: "tests/e2e/databases.js" })` from
|
|
76
|
+
`@jskit-ai/jskit-catalog/browser-tests`. It gives each Playwright worker's
|
|
77
|
+
server its own copy of `BROWSER_TEST_DB_NAME`, restored from the same kind of
|
|
78
|
+
snapshot and seeded in one transaction by the app's fixture `seed`, which
|
|
79
|
+
restores any migration-owned baseline rows the app needs. Every server serves
|
|
80
|
+
the production build. Both refuse the application's database (`DB_NAME`,
|
|
81
|
+
`DATABASE_URL`'s, the Knex connection's), `PROD_DB_NAME` and the other test
|
|
82
|
+
database and its copies. Provision `TEST_DB_NAME`, `BROWSER_TEST_DB_NAME` and
|
|
83
|
+
their numbered copies with grants for the application account (a `<app>_%`
|
|
84
|
+
grant covers them); a user who may create no copies runs one file or worker at
|
|
85
|
+
a time. Two test runs at once need different database names. Test-only server
|
|
86
|
+
settings that disable external effects (email, payments) go in
|
|
87
|
+
`browserTestConfig({ env })`; a suite that writes data proves the server's
|
|
88
|
+
identity first.
|
|
89
|
+
|
|
90
|
+
Each preparation reports how many databases it made, whether it replayed the
|
|
91
|
+
migrations for a new snapshot, and the restore and seed durations. Use those
|
|
92
|
+
measurements to diagnose a slow start before rerunning a browser case.
|
|
80
93
|
|
|
81
94
|
Keep fixtures small for the selected test scope. Run related cases in one suite
|
|
82
|
-
invocation.
|
|
83
|
-
production build separately when needed. Fresh-schema migration proofs are
|
|
84
|
-
separate from browser startup.
|
|
95
|
+
invocation. Fresh-schema migration proofs are separate from browser startup.
|
|
85
96
|
|
|
86
97
|
## Variation points
|
|
87
98
|
|
|
@@ -99,11 +110,10 @@ it as `seed` to `prepareDatabaseFromApp()`.
|
|
|
99
110
|
- Run `npm run db:migrate:status` and the application verification command.
|
|
100
111
|
- Exercise one transaction and one negative connection case.
|
|
101
112
|
- When a seed exists, run `db:prepare` twice and require the second run to be safe.
|
|
102
|
-
- For
|
|
103
|
-
|
|
104
|
-
foreign
|
|
105
|
-
|
|
106
|
-
test when adapting the example; reject a harness that recreates the schema.
|
|
113
|
+
- For the test databases, run the suite twice: the second run restores the
|
|
114
|
+
stored snapshot instead of replaying the migrations, stale rows are gone,
|
|
115
|
+
baseline rows are restored, foreign keys are enforced, and the normal
|
|
116
|
+
database is unchanged. A new migration takes a new snapshot.
|
|
107
117
|
- Confirm `.github/workflows/verify.yml` parses as YAML and declares only the
|
|
108
118
|
`mariadb` service. GitHub reports an unparseable workflow only as a failed
|
|
109
119
|
run with "no jobs were run".
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
"db:prepare": "node ./scripts/prepare-database.js",
|
|
6
6
|
"db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
|
|
7
7
|
"db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
|
|
8
|
-
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
|
|
8
|
+
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list",
|
|
9
|
+
"test": "jskit test --databases"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/database-runtime-mysql": "0.1.
|
|
12
|
+
"@jskit-ai/database-runtime-mysql": "0.1.260"
|
|
12
13
|
}
|
|
13
14
|
}
|
package/skills/jskit/references/patterns/database/mysql-application/example/tests/e2e/databases.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { prepareTestDatabases } from "@jskit-ai/database-runtime/server/testDatabases";
|
|
2
|
+
import knexConfig from "../../knexfile.js";
|
|
3
|
+
|
|
4
|
+
// The browser run's `prepare` module: `browserTestConfig({ prepare: "tests/e2e/databases.js" })` in
|
|
5
|
+
// playwright.config.mjs runs it once, before the servers start. Every Playwright worker gets its own copy of
|
|
6
|
+
// BROWSER_TEST_DB_NAME (`<name>`, `<name>_1`, ...), restored from the schema snapshot of the current migrations, then
|
|
7
|
+
// seeded; the normal and disposable TEST_DB_NAME databases are refused. It is also given each worker's `baseUrls`,
|
|
8
|
+
// for a server setting that names its own address.
|
|
9
|
+
|
|
10
|
+
/** The fixtures the specs rely on, including any migration-owned baseline rows the app needs. */
|
|
11
|
+
async function seed(transaction) {
|
|
12
|
+
await transaction("fixtures").insert({ id: 1, label: "Browser fixture" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function prepareBrowserDatabases({ workers }) {
|
|
16
|
+
const { databases, summary } = await prepareTestDatabases({
|
|
17
|
+
knexConfig,
|
|
18
|
+
environment: process.env,
|
|
19
|
+
variable: "BROWSER_TEST_DB_NAME",
|
|
20
|
+
count: workers,
|
|
21
|
+
seed
|
|
22
|
+
});
|
|
23
|
+
console.log(`[browser-tests] ${summary}.`);
|
|
24
|
+
return databases.map((database) => ({ DB_NAME: database }));
|
|
25
|
+
}
|
|
@@ -53,6 +53,18 @@ dialect. `example/scripts/prepare-database.js` is the portable
|
|
|
53
53
|
migrate-then-seed entrypoint for managed sessions and deployments.
|
|
54
54
|
`example/.env.example` names the five connection values without a secret.
|
|
55
55
|
|
|
56
|
+
Tests get their own databases from
|
|
57
|
+
`@jskit-ai/database-runtime/server/testDatabases`. The `test` script runs
|
|
58
|
+
`jskit test --databases`: each Node test file running at once gets a copy of
|
|
59
|
+
`TEST_DB_NAME`, created from `<TEST_DB_NAME>_snapshot`, a database the
|
|
60
|
+
migrations are replayed into once and again only when a migration changes.
|
|
61
|
+
`example/tests/e2e/databases.js` is the browser run's `prepare` module for
|
|
62
|
+
`browserTestConfig({ prepare: "tests/e2e/databases.js" })` from
|
|
63
|
+
`@jskit-ai/jskit-catalog/browser-tests`: each Playwright worker's server gets
|
|
64
|
+
its own seeded copy of `BROWSER_TEST_DB_NAME`. The database user needs
|
|
65
|
+
`CREATEDB`; without it, Node test files run one at a time on the connected
|
|
66
|
+
database.
|
|
67
|
+
|
|
56
68
|
`example/.github/workflows/verify.yml` is a normal app-owned CI workflow with
|
|
57
69
|
exactly one database service: `postgres` (`postgres:17`, `POSTGRES_*`
|
|
58
70
|
variables, port `5432`, a `pg_isready` health check). Copy it whole from this
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
"db:prepare": "node ./scripts/prepare-database.js",
|
|
6
6
|
"db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
|
|
7
7
|
"db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
|
|
8
|
-
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
|
|
8
|
+
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list",
|
|
9
|
+
"test": "jskit test --databases"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/database-runtime-postgres": "0.1.
|
|
12
|
+
"@jskit-ai/database-runtime-postgres": "0.1.259"
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { prepareTestDatabases } from "@jskit-ai/database-runtime/server/testDatabases";
|
|
2
|
+
import knexConfig from "../../knexfile.js";
|
|
3
|
+
|
|
4
|
+
// The browser run's `prepare` module: `browserTestConfig({ prepare: "tests/e2e/databases.js" })` in
|
|
5
|
+
// playwright.config.mjs runs it once, before the servers start. Every Playwright worker gets its own copy of
|
|
6
|
+
// BROWSER_TEST_DB_NAME (`<name>`, `<name>_1`, ...), created from the snapshot database of the current migrations with
|
|
7
|
+
// CREATE DATABASE ... TEMPLATE, then seeded; the normal and disposable TEST_DB_NAME databases are refused. The database
|
|
8
|
+
// user needs CREATEDB.
|
|
9
|
+
|
|
10
|
+
/** The fixtures the specs rely on, including any migration-owned baseline rows the app needs. */
|
|
11
|
+
async function seed(transaction) {
|
|
12
|
+
await transaction("fixtures").insert({ id: 1, label: "Browser fixture" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function prepareBrowserDatabases({ workers }) {
|
|
16
|
+
const { databases, summary } = await prepareTestDatabases({
|
|
17
|
+
knexConfig,
|
|
18
|
+
environment: process.env,
|
|
19
|
+
variable: "BROWSER_TEST_DB_NAME",
|
|
20
|
+
count: workers,
|
|
21
|
+
seed
|
|
22
|
+
});
|
|
23
|
+
console.log(`[browser-tests] ${summary}.`);
|
|
24
|
+
return databases.map((database) => ({ DB_NAME: database }));
|
|
25
|
+
}
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"./shared": "./src/shared/index.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@jskit-ai/crud-core": "0.2.
|
|
14
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
13
|
+
"@jskit-ai/crud-core": "0.2.14",
|
|
14
|
+
"@jskit-ai/resource-crud-core": "0.1.204"
|
|
15
15
|
},
|
|
16
16
|
"jskit": {
|
|
17
17
|
"kind": "runtime",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"./shared": "./src/shared/index.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@jskit-ai/crud-core": "0.2.
|
|
14
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
15
|
-
"@jskit-ai/workspaces-core": "0.2.
|
|
13
|
+
"@jskit-ai/crud-core": "0.2.14",
|
|
14
|
+
"@jskit-ai/resource-crud-core": "0.1.204",
|
|
15
|
+
"@jskit-ai/workspaces-core": "0.2.15"
|
|
16
16
|
},
|
|
17
17
|
"jskit": {
|
|
18
18
|
"kind": "runtime",
|
|
@@ -78,9 +78,10 @@ expects the relationship id or an unselected field never gets it.
|
|
|
78
78
|
|
|
79
79
|
### Query counts and batches
|
|
80
80
|
|
|
81
|
-
- Count a path's queries with
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
- Count a path's queries with `countQueries()` from
|
|
82
|
+
`@jskit-ai/database-runtime/server/testDatabases` at a few rows and at a
|
|
83
|
+
realistic number; the count should follow tables and pages, not rows
|
|
84
|
+
([Query counts at realistic
|
|
84
85
|
sizes](typed-features.md#query-counts-at-realistic-sizes)).
|
|
85
86
|
- A JSON:API read adds a few lookup and visibility queries per page and per
|
|
86
87
|
relationship field. That follows pages, so it is expected.
|
|
@@ -1025,12 +1025,27 @@ export function resetInvoiceCacheForTests() {
|
|
|
1025
1025
|
|
|
1026
1026
|
### Query counts at realistic sizes
|
|
1027
1027
|
|
|
1028
|
-
**A list, batch or write path's database test counts its queries with
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1028
|
+
**A list, batch or write path's database test counts its queries with
|
|
1029
|
+
`countQueries()` at a few rows and at a realistic number, and fails when the
|
|
1030
|
+
count grows with the rows instead of the pages.**
|
|
1031
|
+
|
|
1032
|
+
- `countQueries(knex, work)`, from
|
|
1033
|
+
`@jskit-ai/database-runtime/server/testDatabases`, runs `work` and returns
|
|
1034
|
+
its `result`, the `count` of statements the Knex instance or transaction
|
|
1035
|
+
sent meanwhile (a transaction's `BEGIN` and `COMMIT` included) and their
|
|
1036
|
+
SQL in `queries`. Pass the Knex the path runs on: the one the test built its
|
|
1037
|
+
repository or service with.
|
|
1038
|
+
- A route is counted through the server the test boots in its own process
|
|
1039
|
+
(`createServer()`, then `app.inject()`): `countRuntimeQueries(work)`, from
|
|
1040
|
+
the same module, records what the app's own `runtime.database` sends while
|
|
1041
|
+
`work` runs, and leaves out the test's own Knex. A server in another process
|
|
1042
|
+
is out of its reach, so the route's guard boots it in the test.
|
|
1032
1043
|
- Pages add a few queries; rows should add none. A path that pages nothing
|
|
1033
1044
|
(one read, one write) compares a short and a long history.
|
|
1045
|
+
- When the counts differ more than the pages explain, read `queries`: the
|
|
1046
|
+
statement that repeats once per row is the one to batch. Judge the
|
|
1047
|
+
comparison by what the path has to read; never tune a fixed ceiling until
|
|
1048
|
+
it passes.
|
|
1034
1049
|
- When it fails, fix the reads ([Lists read related rows in
|
|
1035
1050
|
batches](#lists-read-related-rows-in-batches), [Bounded access
|
|
1036
1051
|
checks](#bounded-access-checks)).
|
|
@@ -1041,17 +1056,7 @@ cannot.
|
|
|
1041
1056
|
|
|
1042
1057
|
```js
|
|
1043
1058
|
// good
|
|
1044
|
-
|
|
1045
|
-
let queries = 0;
|
|
1046
|
-
const count = () => { queries += 1; };
|
|
1047
|
-
knex.on("query", count);
|
|
1048
|
-
try {
|
|
1049
|
-
await work();
|
|
1050
|
-
} finally {
|
|
1051
|
-
knex.off("query", count);
|
|
1052
|
-
}
|
|
1053
|
-
return queries;
|
|
1054
|
-
}
|
|
1059
|
+
import { countQueries, countRuntimeQueries } from "@jskit-ai/database-runtime/server/testDatabases";
|
|
1055
1060
|
|
|
1056
1061
|
test("the invoice list's queries follow its pages, not its invoices", async () => {
|
|
1057
1062
|
const listAt = async (count) => {
|
|
@@ -1062,7 +1067,22 @@ test("the invoice list's queries follow its pages, not its invoices", async () =
|
|
|
1062
1067
|
const few = await listAt(10);
|
|
1063
1068
|
const many = await listAt(300);
|
|
1064
1069
|
// Thirty times the invoices may add pages; a read per invoice would add about 290 queries.
|
|
1065
|
-
assert.ok(many <= few * 2,
|
|
1070
|
+
assert.ok(many.count <= few.count * 2,
|
|
1071
|
+
`${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// good: the same comparison for the route, through the server this test booted
|
|
1075
|
+
test("GET /api/invoices reads per page, not per invoice", async () => {
|
|
1076
|
+
const app = await createServer({ runtimeEnv });
|
|
1077
|
+
const listAt = async (count) => {
|
|
1078
|
+
const workspace = await createWorkspace(knex);
|
|
1079
|
+
await seedInvoices(knex, { workspace, count, linesEach: 5 });
|
|
1080
|
+
return countRuntimeQueries(() => app.inject({ url: invoicesUrl(workspace), headers: signedIn }));
|
|
1081
|
+
};
|
|
1082
|
+
const few = await listAt(10);
|
|
1083
|
+
const many = await listAt(300);
|
|
1084
|
+
assert.ok(many.count <= few.count * 2,
|
|
1085
|
+
`${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
|
|
1066
1086
|
});
|
|
1067
1087
|
|
|
1068
1088
|
// bad: three invoices, nothing counted, so a read per invoice passes
|
|
@@ -1096,37 +1116,63 @@ and Vitest's `maxWorkers` come from `testConcurrency()`.**
|
|
|
1096
1116
|
file uses, its own Vite `cacheDir` for each dev server it starts (Vite
|
|
1097
1117
|
replaces a shared dependency cache under servers still reading it), and no
|
|
1098
1118
|
writes into the source tree. Files that collide are isolated, never
|
|
1099
|
-
serialised. A
|
|
1100
|
-
|
|
1119
|
+
serialised. A database test file gets its own copy of `TEST_DB_NAME` from
|
|
1120
|
+
`jskit test --databases`
|
|
1121
|
+
([integration test databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)).
|
|
1122
|
+
- An application's `playwright.config.mjs` is
|
|
1123
|
+
`defineConfig(browserTestConfig({ testDir: "./tests/e2e" }))` from
|
|
1124
|
+
`@jskit-ai/jskit-catalog/browser-tests`. It builds the production frontend
|
|
1125
|
+
once (reused while its sources are unchanged) and gives each worker its own
|
|
1126
|
+
server, port and, through its `prepare` module, its own seeded database; a
|
|
1127
|
+
spec file's tests run in order in one worker. With `PLAYWRIGHT_BASE_URL`, the
|
|
1128
|
+
tests use that managed target as its host serves it. Every run is bounded by
|
|
1129
|
+
a `globalTimeout` (20 minutes, or the suite's own measured bound) and ends
|
|
1130
|
+
with its 10 slowest tests. With a `signIn` module, each worker's server is
|
|
1131
|
+
signed in once and every test of that worker starts signed in, instead of
|
|
1132
|
+
each test signing in. `jskit check` fails a webServer that runs the development server
|
|
1133
|
+
(`npm run develop`, `vite`), one webServer shared by every worker, and a
|
|
1134
|
+
config without a `globalTimeout`.
|
|
1101
1135
|
- A file that truly cannot run beside others, because it holds a resource of
|
|
1102
1136
|
the whole machine, says why on its first line,
|
|
1103
1137
|
`// jskit-test: alone - <reason>`. `jskit test` runs it after the others,
|
|
1104
1138
|
one such file at a time, and fails on a marker without a reason or below the
|
|
1105
1139
|
first line.
|
|
1140
|
+
- `jskit test` ends with its 10 slowest test files and tests, records each
|
|
1141
|
+
file's time in `node_modules/.cache/jskit-test/file-times.json`, and fails
|
|
1142
|
+
when a file takes more than 10 minutes or the whole command more than 45:
|
|
1143
|
+
bounds only a clearly broken suite reaches, such as a file waiting out its
|
|
1144
|
+
timeouts. A suite whose measured work needs more sets `runMs`, `fileMs` or a
|
|
1145
|
+
file's own bound in `test-time-bounds.json`, with the measurement in the
|
|
1146
|
+
commit that raises it; a file that got slow is fixed instead. A file still
|
|
1147
|
+
running at twice its bound, or sooner at ten times the time it last finished
|
|
1148
|
+
in (at least two minutes), is stopped and fails the run by name, so the other
|
|
1149
|
+
files still finish and report: look for what it leaves open (a server, child
|
|
1150
|
+
process, timer or socket) or a test without a timeout. A file with its own
|
|
1151
|
+
bound is stopped at twice that bound.
|
|
1106
1152
|
|
|
1107
1153
|
Why: `jskit test` and `testConcurrency()` read one budget, so every runner
|
|
1108
1154
|
uses the machine without starving the other runs on it.
|
|
1109
1155
|
|
|
1110
1156
|
```js
|
|
1111
1157
|
// good: package.json "test": "jskit test", and playwright.config.mjs
|
|
1112
|
-
import {
|
|
1158
|
+
import { browserTestConfig } from "@jskit-ai/jskit-catalog/browser-tests";
|
|
1113
1159
|
|
|
1114
|
-
export default defineConfig({
|
|
1160
|
+
export default defineConfig(browserTestConfig({ testDir: "./tests/e2e" }));
|
|
1115
1161
|
|
|
1116
1162
|
// bad: "test": "node --test --test-concurrency=1", and
|
|
1117
|
-
export default defineConfig({ workers: 1 });
|
|
1163
|
+
export default defineConfig({ workers: 1, webServer: { command: "npm run develop" } });
|
|
1118
1164
|
```
|
|
1119
1165
|
|
|
1120
1166
|
### Test databases are prepared once
|
|
1121
1167
|
|
|
1122
1168
|
**A test run prepares each database once and restores it from a stored
|
|
1123
|
-
snapshot of the schema
|
|
1124
|
-
|
|
1169
|
+
snapshot of the schema and the rows the migrations write, migration ledger
|
|
1170
|
+
included, checked by a digest of the migrations.**
|
|
1125
1171
|
|
|
1126
|
-
- The snapshot is rebuilt only when the digest of the
|
|
1127
|
-
|
|
1172
|
+
- The snapshot is rebuilt only when the digest of the migrations' names and
|
|
1173
|
+
contents changes, once and outside the timed tests; every disposable
|
|
1128
1174
|
database, and every database a test migrated up or down, is restored from
|
|
1129
|
-
it.
|
|
1175
|
+
it. A browser run's fixture seed runs after each restore.
|
|
1130
1176
|
- The full migration history runs once, in its own fresh-schema check
|
|
1131
1177
|
([database verification](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#verification)).
|
|
1132
1178
|
- The harness reports each preparation phase and its duration, and those
|
|
@@ -1140,8 +1186,8 @@ Why: an application's migrations include those of every installed package, so
|
|
|
1140
1186
|
replaying them costs more with each package as well as each migration.
|
|
1141
1187
|
|
|
1142
1188
|
```js
|
|
1143
|
-
// good:
|
|
1144
|
-
|
|
1189
|
+
// good: package.json "test": "jskit test --databases"; each file gets a restored copy of TEST_DB_NAME
|
|
1190
|
+
const { databases } = await prepareTestDatabases({ knexConfig, environment: process.env, count: workers, seed });
|
|
1145
1191
|
|
|
1146
1192
|
// bad: every file replays every migration
|
|
1147
1193
|
before(() => knex.migrate.latest());
|
|
@@ -1341,8 +1387,11 @@ that must run alone says so on its own first line instead.
|
|
|
1341
1387
|
| Rule | Reports | Section |
|
|
1342
1388
|
| --- | --- | --- |
|
|
1343
1389
|
| `test/node-test` | a script that runs `node --test` directly instead of `jskit test` | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1344
|
-
| `test/fixed-concurrency` | `--test-concurrency` in a script | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1345
|
-
| `test/playwright-workers` | `--workers` or `-j` passed to `playwright test`,
|
|
1390
|
+
| `test/fixed-concurrency` | `--test-concurrency` in a script, or a script that pins `V64_TEST_CONCURRENCY` or `PLAYWRIGHT_WORKERS` | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1391
|
+
| `test/playwright-workers` | `--workers` or `-j` passed to `playwright test`, its config without `workers: testConcurrency()` or `browserTestConfig()`, or with `workers` set over `browserTestConfig()`'s | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1392
|
+
| `test/playwright-dev-server` | a Playwright webServer command that runs the development server (`npm run develop`, `npm run dev`, `bin/develop.js`, `vite`, `vite dev`) | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1393
|
+
| `test/playwright-isolation` | a Playwright webServer every worker shares, without `browserTestConfig()` | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1394
|
+
| `test/playwright-global-timeout` | a Playwright config without a `globalTimeout` (or `browserTestConfig()`), or with `globalTimeout: 0` | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1346
1395
|
| `test/vitest-workers` | `--maxWorkers`, `--minWorkers` or `--no-file-parallelism` passed to Vitest, or its config without `maxWorkers: testConcurrency()` or with `fileParallelism: false` | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1347
1396
|
|
|
1348
1397
|
The rules see shapes, not intent. Feature files, owned state beyond
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import knex from "knex";
|
|
2
|
-
|
|
3
|
-
// Application-owned test setup. Schema migration tests use a separate fresh database.
|
|
4
|
-
async function prepareBrowserTestDatabase({ knexConfig, environment = process.env, seed }) {
|
|
5
|
-
const started = performance.now();
|
|
6
|
-
if (environment.NODE_ENV === "production") throw new Error("Browser tests cannot prepare a production runtime.");
|
|
7
|
-
if (typeof seed !== "function") throw new TypeError("An explicit browser fixture seed is required.");
|
|
8
|
-
if (knexConfig?.client !== "mysql2" || !knexConfig.connection || typeof knexConfig.connection !== "object") {
|
|
9
|
-
throw new Error("Browser preparation requires a resolved MySQL Knex configuration.");
|
|
10
|
-
}
|
|
11
|
-
const database = String(environment.BROWSER_TEST_DB_NAME || "").trim();
|
|
12
|
-
if (!/^[a-zA-Z0-9_]{1,64}$/.test(database)) throw new Error("BROWSER_TEST_DB_NAME must name the exact browser database.");
|
|
13
|
-
const protectedNames = [knexConfig.connection.database, environment.DB_NAME, environment.TEST_DB_NAME];
|
|
14
|
-
if (environment.DATABASE_URL) {
|
|
15
|
-
try { protectedNames.push(decodeURIComponent(new URL(environment.DATABASE_URL).pathname.slice(1))); }
|
|
16
|
-
catch { throw new Error("DATABASE_URL must be a valid database URL."); }
|
|
17
|
-
}
|
|
18
|
-
if (!knexConfig.connection.database || protectedNames.some(name => String(name || "").trim().toLowerCase() === database.toLowerCase())) {
|
|
19
|
-
throw new Error("BROWSER_TEST_DB_NAME must differ from the application and disposable TEST_DB_NAME databases.");
|
|
20
|
-
}
|
|
21
|
-
const adminConnection = { ...knexConfig.connection };
|
|
22
|
-
delete adminConnection.database;
|
|
23
|
-
const admin = knex({ ...knexConfig, connection: adminConnection, pool: { min: 0, max: 1 } });
|
|
24
|
-
let schemaExisted;
|
|
25
|
-
try {
|
|
26
|
-
const [[row]] = await admin.raw("SELECT COUNT(*) AS count FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", [database]);
|
|
27
|
-
schemaExisted = Number(row.count) === 1;
|
|
28
|
-
await admin.raw("CREATE DATABASE IF NOT EXISTS ??", [database]);
|
|
29
|
-
}
|
|
30
|
-
finally { await admin.destroy(); }
|
|
31
|
-
console.info(`Browser database ${database}: ${schemaExisted ? "existing schema" : "new schema"}.`);
|
|
32
|
-
const db = knex({ ...knexConfig, connection: { ...knexConfig.connection, database } });
|
|
33
|
-
try {
|
|
34
|
-
const migrationStarted = performance.now();
|
|
35
|
-
const [, migrations] = await db.migrate.latest();
|
|
36
|
-
const migrationMs = Math.round(performance.now() - migrationStarted);
|
|
37
|
-
console.info(`Browser migrations: ${migrations.length} applied in ${migrationMs}ms.`);
|
|
38
|
-
const resetStarted = performance.now();
|
|
39
|
-
let resetTables = 0;
|
|
40
|
-
const connection = await db.client.acquireConnection();
|
|
41
|
-
try {
|
|
42
|
-
const [[actual]] = await db.raw("SELECT DATABASE() AS databaseName").connection(connection);
|
|
43
|
-
if (actual.databaseName !== database) throw new Error("Browser reset requires the exact BROWSER_TEST_DB_NAME connection.");
|
|
44
|
-
const [tables] = await db.raw(
|
|
45
|
-
"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'", [database]
|
|
46
|
-
).connection(connection);
|
|
47
|
-
const ledger = knexConfig.migrations?.tableName || "knex_migrations";
|
|
48
|
-
await db.raw("SET FOREIGN_KEY_CHECKS = 0").connection(connection);
|
|
49
|
-
try {
|
|
50
|
-
for (const { name } of tables) {
|
|
51
|
-
if (name === ledger || name === `${ledger}_lock`) continue;
|
|
52
|
-
await db.raw("TRUNCATE TABLE ??", [name]).connection(connection);
|
|
53
|
-
resetTables += 1;
|
|
54
|
-
}
|
|
55
|
-
} finally { await db.raw("SET FOREIGN_KEY_CHECKS = 1").connection(connection); }
|
|
56
|
-
} finally { await db.client.releaseConnection(connection); }
|
|
57
|
-
const resetMs = Math.round(performance.now() - resetStarted);
|
|
58
|
-
console.info(`Browser fixture reset: ${resetTables} tables in ${resetMs}ms.`);
|
|
59
|
-
const seedStarted = performance.now();
|
|
60
|
-
await db.transaction(seed);
|
|
61
|
-
const seedMs = Math.round(performance.now() - seedStarted);
|
|
62
|
-
const totalMs = Math.round(performance.now() - started);
|
|
63
|
-
console.info(`Browser fixture seed: ${seedMs}ms; database preparation total: ${totalMs}ms.`);
|
|
64
|
-
return { database, migrations, schemaExisted, timings: { migrationMs, resetMs, seedMs, totalMs } };
|
|
65
|
-
} finally { await db.destroy(); }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export { prepareBrowserTestDatabase };
|