@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.
Files changed (74) hide show
  1. package/guide/agent/app-extras/assistant-conversation.md +5 -1
  2. package/guide/agent/app-extras/realtime.md +20 -10
  3. package/guide/agent/app-setup/authentication.md +20 -1
  4. package/guide/agent/app-setup/database-layer.md +75 -31
  5. package/guide/agent/app-setup/existing-application-migration.md +7 -1
  6. package/guide/agent/app-setup/multi-homing.md +6 -2
  7. package/guide/agent/app-setup/upgrading-jskit.md +78 -0
  8. package/guide/agent/framework/client-features.md +35 -1
  9. package/guide/agent/framework/sanity-checks.md +4 -3
  10. package/guide/agent/framework/typed-features.md +79 -30
  11. package/package.json +1 -1
  12. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  13. package/patterns/minimal-foundation/example/AGENTS.md +14 -7
  14. package/patterns/minimal-foundation/example/package.json +3 -3
  15. package/patterns/minimal-foundation/example/playwright.config.mjs +3 -33
  16. package/patterns/minimal-foundation/example/server.js +7 -1
  17. package/patterns/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
  18. package/patterns/shell-foundation/example/AGENTS.md +14 -7
  19. package/patterns/shell-foundation/example/package.json +4 -4
  20. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  21. package/patterns/shell-foundation/example/playwright.config.mjs +3 -33
  22. package/patterns/shell-foundation/example/server.js +7 -1
  23. package/patterns/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
  24. package/reference/autogen/KERNEL_MAP.md +2 -0
  25. package/reference/autogen/PATTERN_INDEX.md +37 -37
  26. package/reference/autogen/packages/architecture-inspector.md +1 -0
  27. package/reference/autogen/packages/assistant-runtime.md +1 -1
  28. package/reference/autogen/packages/auth-provider-local-core.md +2 -1
  29. package/reference/autogen/packages/connectors-catalog.md +8 -0
  30. package/reference/autogen/packages/database-runtime.md +33 -0
  31. package/reference/autogen/packages/http-runtime.md +1 -1
  32. package/reference/autogen/packages/kernel.md +29 -1
  33. package/reference/autogen/packages/oidc-issuer-core.md +0 -1
  34. package/reference/autogen/packages/realtime.md +8 -5
  35. package/reference/autogen/packages/shell-web.md +4 -3
  36. package/reference/autogen/packages/workspaces-core.md +1 -1
  37. package/reference/autogen/tooling/jskit-catalog.md +71 -1
  38. package/skills/jskit/SKILL.md +8 -6
  39. package/skills/jskit/references/client-features.md +35 -1
  40. package/skills/jskit/references/existing-application-migration.md +7 -1
  41. package/skills/jskit/references/heuristics/client-requests.md +1 -0
  42. package/skills/jskit/references/heuristics/typed-features.md +2 -2
  43. package/skills/jskit/references/heuristics/ui-testing.md +21 -16
  44. package/skills/jskit/references/multi-homing.md +6 -2
  45. package/skills/jskit/references/pattern-index.md +37 -37
  46. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +14 -7
  47. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +3 -3
  48. package/skills/jskit/references/patterns/app/minimal-foundation/example/playwright.config.mjs +3 -33
  49. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +7 -1
  50. package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
  51. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +14 -7
  52. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +4 -4
  53. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  54. package/skills/jskit/references/patterns/app/shell-foundation/example/playwright.config.mjs +3 -33
  55. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +7 -1
  56. package/skills/jskit/references/patterns/app/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
  57. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
  58. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  59. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  60. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  61. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
  62. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +34 -24
  63. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +3 -2
  64. package/skills/jskit/references/patterns/database/mysql-application/example/tests/e2e/databases.js +25 -0
  65. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +12 -0
  66. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +3 -2
  67. package/skills/jskit/references/patterns/database/postgres-application/example/tests/e2e/databases.js +25 -0
  68. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  69. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  70. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
  71. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
  72. package/skills/jskit/references/sanity-checks.md +4 -3
  73. package/skills/jskit/references/typed-features.md +79 -30
  74. package/skills/jskit/references/patterns/database/mysql-application/example/tests/browser/database.js +0 -68
@@ -196,7 +196,11 @@ storage or infer authorization from browser fields. Authentication may be async;
196
196
  return a context object or throw an application error. A missing context is denied.
197
197
  Apply the application's normal session, Origin/CSRF and socket-handshake rules
198
198
  there. The returned trusted fields extend the action context; the runtime's own
199
- `authorize` still controls each conversation operation and subscription.
199
+ `authorize` still controls each conversation operation and subscription. A
200
+ subscriber is authorized again for every event it hears, except a streamed delta
201
+ (an in-progress message), which reuses an authorization under a second old: a
202
+ reply's hundreds of deltas cost about one check a second, and a revoked subscriber
203
+ stops hearing the turn within a second.
200
204
 
201
205
  The helper assembles `AssistantFeature`, the HTTP/action/bootstrap/event providers
202
206
  and one realtime provider. It closes that hosting on Fastify `preClose`. The
@@ -62,12 +62,18 @@ When `auth.service.authenticateRequest()` is present, each publish reauthenticat
62
62
  every socket that could receive it before sending, and every socket is rechecked
63
63
  every 30 seconds. A socket could receive a publish when the audience is
64
64
  `all_clients` or `broadcast`, when it is already in a target room, or, when an
65
- audience contributor is registered, when a target room is a user or workspace
66
- room it could join on revalidation. Other sockets are not checked for that
67
- publish. Sockets whose authentication requests are identical, such as tabs of
65
+ audience contributor is registered and a target room is a user or workspace
66
+ room, when its actor's contributed rooms include a target room or it has not
67
+ signed in yet. A socket joins only its own actor's contributed rooms, so a
68
+ publish to `workspace:7` checks that workspace's members' sockets, not every
69
+ connected one. To decide, the publish resolves the contributed rooms of every
70
+ signed-in socket's actor in one call to each contributor, whatever the number
71
+ of sockets. Other sockets are not checked for that publish; the 30-second check
72
+ reaches them. Sockets whose authentication requests are identical, such as tabs of
68
73
  one browser, share one `authenticateRequest()` call within a publish or a
69
- 30-second pass, and sockets of one actor share one lookup of their contributed
70
- rooms; nothing is reused from one publish or pass to the next. Revoked,
74
+ 30-second pass, and the sockets a publish or pass checks reuse that one lookup
75
+ of their contributed rooms; nothing is reused from one publish or pass to the
76
+ next. Redis peers select the sockets they check the same way. Revoked,
71
77
  expired, or changed actors are disconnected by the first publish that could
72
78
  reach them or the next 30-second check. Authentication lookup errors fail
73
79
  closed.
@@ -91,14 +97,18 @@ a connected socket is disconnected. The local and OIDC providers never need
91
97
  this: their checks do not rotate tokens.
92
98
 
93
99
  Packages that know which scopes an actor belongs to contribute them through
94
- `runtime.realtime.registerAudienceContributor({ id, resolveActorScopes })` during
95
- setup. `resolveActorScopes(actorId)` returns event scopes (`{ kind: "user", id }`,
100
+ `runtime.realtime.registerAudienceContributor({ id, resolveActorsScopes })` during
101
+ setup. `resolveActorsScopes(actorIds)` answers for many actors at once, so read
102
+ them in one query rather than one per actor: it returns, or resolves to, a `Map`
103
+ from actor id to that actor's event scopes (`{ kind: "user", id }`,
96
104
  `{ kind: "workspace", id }` or `{ kind: "workspace_user", workspaceId, userId }`);
97
- realtime turns each into the same room an `event_scope` audience targets. Sockets
105
+ an actor it leaves out has none. A single actor is asked as a list of one.
106
+ Realtime turns each scope into the same room an `event_scope` audience targets. Sockets
98
107
  join those rooms on connection and, on every revalidation, leave the contributed
99
108
  rooms the actor no longer belongs to; rooms joined by anything else are left
100
- alone. `@jskit-ai/workspaces-core` contributes the actor's active workspace
101
- memberships this way when realtime is installed.
109
+ alone. `@jskit-ai/workspaces-core` contributes the actors' active workspace
110
+ memberships this way when realtime is installed, with
111
+ `listActiveWorkspaceIdsByUserIds()`: one memberships query per 1,000 actors.
102
112
 
103
113
  The authentication request includes parsed cookies, the `Host`, `Origin` and
104
114
  `X-Forwarded-Proto` headers when present, and the socket address and TLS state.
@@ -66,6 +66,23 @@ Protected server operations declare an auth policy; they do not trust a hidden
66
66
  button or a page-local session check. Other packages extend auth through the
67
67
  public extension capability rather than replacing the auth service.
68
68
 
69
+ ## When the app profile is synchronized
70
+
71
+ With users-core, every signed-in identity has an app profile (a `users` row),
72
+ and synchronizing it also ensures the user's settings and runs the profile-sync
73
+ lifecycle, where workspaces-core provisions the personal workspace. The
74
+ providers synchronize in full at sign-in, registration, account changes and
75
+ each session refresh (local auth: when the 15-minute access token has expired).
76
+ An ordinary request checks its session as always, then reads the
77
+ profile once: it synchronizes in full only when the profile is missing or its
78
+ email or display name no longer matches the identity the request carries.
79
+
80
+ So a name or email changed at the provider shows at the next request, while a
81
+ personal workspace, owner membership or settings row deleted while the person
82
+ is signed in comes back at their next sign-in or session refresh, not at their
83
+ next request. Authorization never rests on those rows: sign-out and revoked
84
+ sessions take effect at the next request.
85
+
69
86
  ## Sign-in protection
70
87
 
71
88
  Two limits slow down password guessing, and both are on by default.
@@ -420,7 +437,9 @@ For direct localhost Playwright tests, enable the development auth bypass only
420
437
  in the server process and use `loginAsExistingUser()` from
421
438
  `@jskit-ai/auth-web/test/playwright`. The Node-side helper sends the private
422
439
  `x-jskit-dev-auth-secret`; never pass that secret through browser code, URLs, or
423
- client environment.
440
+ client environment. Under `browserTestConfig()`, a `signIn` module signs each
441
+ worker in once with `loginRequestAsExistingUser()` instead of each test
442
+ signing in ([Upgrading JSKIT](/guide/app-setup/upgrading-jskit#sign-each-browser-worker-in-once)).
424
443
 
425
444
  A managed host instead supplies authenticated state through
426
445
  `PLAYWRIGHT_STORAGE_STATE` and the app-owned Playwright config. Treat the
@@ -398,21 +398,53 @@ database migration table are sufficient.
398
398
 
399
399
  Disposable integration and migration tests use `TEST_DB_NAME`, never the
400
400
  application's `DB_NAME`. A test run prepares that schema once and restores it
401
- from a stored snapshot:
402
-
403
- - The snapshot holds the schema as the engine reports it (each table's
404
- `SHOW CREATE TABLE` on MySQL and MariaDB, `pg_dump --schema-only` on
405
- PostgreSQL), the migration ledger rows and the seed rows. Its key is a digest
406
- of the migration files that the application's `migrations/` and its installed
407
- packages supply, and of the seed source.
408
- - When that digest changes, the harness migrates one database once, outside
409
- the timed tests, and stores a new snapshot. Otherwise it restores the stored
410
- one. On PostgreSQL, `CREATE DATABASE <name> TEMPLATE <prepared>` copies a
411
- prepared database that has no open connections.
412
- - A test that runs migrations up or down, or otherwise changes the schema,
413
- leaves its database dirty. The harness restores the snapshot before another
414
- test uses that database, instead of running `migrate.latest()` over the
415
- whole history.
401
+ from a stored snapshot. `jskit test --databases` (the database patterns' `test`
402
+ script) does this with `@jskit-ai/database-runtime/server/testDatabases`, from
403
+ the application's `knexfile.js`:
404
+
405
+ - Each test file running at once gets a database of its own for as long as it
406
+ runs, as both `DB_NAME` and `TEST_DB_NAME`: `TEST_DB_NAME` itself, then
407
+ copies named `<TEST_DB_NAME>_1`, `_2`, ... up to the machine's test budget,
408
+ as many as the database user may create. With no usable `TEST_DB_NAME`
409
+ (unset, the connected database itself, or one the database user may not
410
+ prepare), the files run one at a time on the connected database. A copy
411
+ named like the application's database or `PROD_DB_NAME`, or a server that
412
+ cannot be reached, fails the run instead.
413
+ - The snapshot holds the schema as the engine reports it and the rows the
414
+ migrations wrote, including the migration ledger. On MySQL and MariaDB it is
415
+ each table's `SHOW CREATE TABLE` and rows, in
416
+ `node_modules/.cache/jskit-test-databases/`; on PostgreSQL it is the database
417
+ `<TEST_DB_NAME>_snapshot`, which `CREATE DATABASE <name> TEMPLATE <snapshot>`
418
+ copies while nothing is connected to it (the user needs `CREATEDB`), file by
419
+ file (`STRATEGY FILE_COPY`) from PostgreSQL 15. Its key
420
+ is a digest of the dialect, of the name and contents of every migration file
421
+ that the application's `migrations/` and its installed packages supply, and
422
+ of `package-lock.json`, since an upgraded package can change a helper its
423
+ migrations import. MySQL test databases hold tables only: a schema with
424
+ views, triggers, routines or events is refused.
425
+ - Each test file gets the environment `jskit test` was given, plus its
426
+ database's connection (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`); the
427
+ rest of the `.env` that `knexfile.js` loads is read for the databases only.
428
+ The run's settings, connection password included, are in a file only the
429
+ user can read; the runner's environment names only its directory.
430
+ - When that digest changes, the harness replays the migrations into the
431
+ designated database once, outside the timed tests, and stores a new
432
+ snapshot. Otherwise it restores the stored one.
433
+ - A schema the migrations alone do not build, such as migrations that start
434
+ from a baseline schema file, or reference rows every test reads, comes from
435
+ the application's own build: `knexfile.js` also exports
436
+ `testDatabaseSchema = { files, build }`, and a browser `prepare` module passes
437
+ the same `schema` to `prepareTestDatabases()`. `build(knex)` runs in the empty
438
+ designated database instead of `knex.migrate.latest()` (the baseline, then
439
+ `knex.migrate.latest()`), and `files` lists every file it reads, its own
440
+ module included, relative to the application root: their contents join the
441
+ digest, so changing one takes a new snapshot.
442
+ - A test that runs migrations up or down, or otherwise changes the tables,
443
+ leaves its database dirty: its tables or migration ledger differ from the
444
+ snapshot's. The harness restores the snapshot before another test file uses
445
+ that database, instead of running `migrate.latest()` over the whole history.
446
+ - A browser run's seed is not part of the snapshot: it runs in each restored
447
+ database, in one transaction ([Browser test preparation](#browser-test-preparation)).
416
448
  - Only the fresh-schema check in [Verification](#verification) replays the
417
449
  whole history, as its own step.
418
450
  - The harness reports each phase (digest, restore or rebuild, seed) with its
@@ -421,27 +453,39 @@ from a stored snapshot:
421
453
  fixed, never absorbed by a longer time limit.
422
454
 
423
455
  Endpoints and batch operations get their own database tests at realistic row
424
- counts that compare their query counts; see the typed features guide's
456
+ counts that compare their query counts, counted with `countQueries()` from
457
+ `@jskit-ai/database-runtime/server/testDatabases`, or `countRuntimeQueries()`
458
+ for a route of the server the test boots; see the typed features guide's
425
459
  [query counts at realistic sizes](/guide/framework/typed-features#query-counts-at-realistic-sizes).
426
460
 
427
461
  ## Browser test preparation
428
462
 
429
- Keep the test schema and migration history between routine browser runs. The
430
- MySQL application pattern includes `example/tests/browser/database.js`: it
431
- validates the exact `BROWSER_TEST_DB_NAME`, applies pending migrations, clears test rows,
432
- and runs one explicit fixture seed. Restore migration-owned baseline rows in
433
- that seed. Prove repeat preparation, stale-data removal, pending migrations, and
434
- isolation from the normal database before adopting the launcher. Give disposable
435
- integration and migration tests their own `TEST_DB_NAME`; require the two names
436
- to differ and provision exact grants for each. Run disposable cleanup between
437
- two browser preparations in the regression test to prove it cannot erase the
438
- reusable schema. Startup reports applied migration counts and phase durations
439
- so a cold rebuild is visible to the caller.
463
+ A browser run gives every Playwright worker its own server and its own copy of
464
+ `BROWSER_TEST_DB_NAME`. The database patterns' `example/tests/e2e/databases.js`
465
+ is the `prepare` module of `browserTestConfig({ prepare: "tests/e2e/databases.js" })`
466
+ from `@jskit-ai/jskit-catalog/browser-tests`: it calls `prepareTestDatabases()`
467
+ with `variable: "BROWSER_TEST_DB_NAME"`, the worker count and the app's fixture
468
+ seed, and returns each worker's `DB_NAME`. The worker's own process gets the
469
+ same environment as its server, so a test that opens the database itself (with
470
+ the app's `knexfile.js`) reaches that worker's copy. The copies (`<name>`, `<name>_1`,
471
+ ...) are restored from the same stored snapshot as the Node test databases
472
+ ([Integration test databases](#integration-test-databases)), then seeded in one
473
+ transaction each, so stale rows never survive a run and the migrations are
474
+ replayed only when one changes. It refuses the application's database,
475
+ `PROD_DB_NAME`, `TEST_DB_NAME` and their copies.
476
+
477
+ Give disposable integration and migration tests their own `TEST_DB_NAME`;
478
+ the two names, and their copies, must differ, and the application account
479
+ needs grants for each (a `<app>_%` grant covers the copies). Each preparation
480
+ reports how many databases it made, whether it replayed the migrations for a
481
+ new snapshot, and its restore and seed durations, so a cold rebuild is visible
482
+ to the caller.
440
483
 
441
484
  Run related checks in one suite invocation and prepare only their required
442
- fixtures. The application foundations use the development launcher for ordinary
443
- browser checks; fresh-schema migration proofs and production builds are separate.
444
- A managed suite uses the host's server URL and identity, starts no second server,
445
- and restores normal Preview after the batch. Database isolation alone does not
485
+ fixtures. A local browser run serves the production build, never the
486
+ development launcher; fresh-schema migration proofs are separate. A managed
487
+ suite (`PLAYWRIGHT_BASE_URL`) uses the host's server URL and identity, as the
488
+ host serves it, starts no second server, and restores normal Preview after the
489
+ batch. Database isolation alone does not
446
490
  disable email, payments or other external effects: configure those test controls
447
491
  and verify the actual server identity before destructive requests.
@@ -133,7 +133,13 @@ The foundation servers reuse compiled response serializers for identical JSON
133
133
  schemas through `createCachedResponseSerializerFactory` from
134
134
  `@jskit-ai/kernel/server/http`. This reduces duplicate startup compilation and
135
135
  retained memory in applications with many routes. Each Fastify compiler context
136
- keeps its own cache; request data and response payloads are not cached.
136
+ keeps its own cache; request data and response payloads are not cached. A
137
+ serializer is compiled when a response first uses it, in every environment, so
138
+ startup compiles none. Startup still checks every response schema as
139
+ fast-json-stringify would before building it: an invalid schema or a `$ref`
140
+ that does not resolve stops the server. A problem fast-json-stringify only
141
+ finds while generating a serializer fails the first response that uses it, so
142
+ keep a test that calls each route.
137
143
 
138
144
  Existing applications adopt the same setup in their own `server.js` after
139
145
  upgrading JSKIT and declaring the directly used
@@ -56,8 +56,12 @@ config.tenancyPolicy = {
56
56
  ```
57
57
 
58
58
  Provisioning runs in the users profile-sync lifecycle after registration and
59
- sign-in. The first run creates the personal workspace, its owner membership and
60
- its settings; later runs reuse it. The default name is the user's display name,
59
+ sign-in, at each session refresh, and on a request whose identity's email or
60
+ display name changed. The first run creates the personal workspace, its owner
61
+ membership and its settings; later runs reuse it. An ordinary request only reads
62
+ the existing profile, so a personal workspace, owner membership or settings row
63
+ deleted while the person is signed in comes back at their next sign-in or
64
+ session refresh, not at their next request. The default name is the user's display name,
61
65
  or email, followed by "'s Workspace". To choose another, provide the
62
66
  `workspaces.personal-workspace-name` capability from an application feature. It
63
67
  receives the synchronized profile (`id`, `displayName`, `email`, `username`), and
@@ -191,6 +191,81 @@ An application adopts it once, at the upgrade that brings `jskit test`:
191
191
  4. The whole suite passes with the budget, and its time before and after is
192
192
  recorded.
193
193
 
194
+ ## Move browser and database tests onto the JSKIT harness
195
+
196
+ The release that brings `browserTestConfig()` and `jskit test --databases`
197
+ makes JSKIT own the fast test harness. Plain `jskit check` fails a Playwright
198
+ webServer on the development server (`test/playwright-dev-server`), one
199
+ webServer every worker shares (`test/playwright-isolation`), and a config
200
+ without a `globalTimeout` (`test/playwright-global-timeout`). At that repin, an
201
+ application:
202
+
203
+ 1. Deletes its own copies of the harness: a local test-budget helper
204
+ (`testConcurrency()` in its own scripts), a launcher that starts a server
205
+ per worker, a cached production-build step, worker port and database-name
206
+ helpers, a Node test runner wrapper that hands each file a database, and
207
+ its schema-snapshot code. It imports `testConcurrency()` from
208
+ `@jskit-ai/jskit-catalog/test-budget` wherever it still needs the number.
209
+ 2. Replaces `playwright.config.*` with
210
+ `defineConfig(browserTestConfig({ testDir: "./tests/e2e", prepare: "tests/e2e/databases.js" }))`
211
+ from `@jskit-ai/jskit-catalog/browser-tests`, with `server`, `build`, `env`
212
+ or `port` only where its own differ from `node ./bin/server.js`,
213
+ `npm run build` and 4173, and `globalTimeout` only for a suite measured to
214
+ need more than 20 minutes. Settings a config adds (projects, a globalSetup
215
+ that proves the server's identity, `timezoneId`) spread over the result:
216
+ `defineConfig({ ...base, projects, use: { ...base.use, timezoneId } })`. A
217
+ second local Playwright config and any `npm run develop` or `vite`
218
+ webServer go.
219
+ 3. Copies its database pattern's `example/tests/e2e/databases.js` and moves
220
+ its browser fixture seed into its `seed`; each worker's server then gets a
221
+ seeded copy of `BROWSER_TEST_DB_NAME`. Server settings for tests only (a
222
+ simulated email or payment driver) go in `browserTestConfig({ env })`.
223
+ 4. Runs Node tests with `"test": "jskit test --databases"`, with
224
+ `TEST_DB_NAME` distinct from `DB_NAME`, `BROWSER_TEST_DB_NAME` and
225
+ `PROD_DB_NAME`, and a database user that may create its numbered copies
226
+ (`CREATEDB` on PostgreSQL). Test files read `DB_NAME`/`TEST_DB_NAME` as
227
+ given rather than naming the test database themselves, and no longer
228
+ create, migrate or drop it: the run hands each file a restored copy.
229
+ 5. Proves it with a timed full run: `npm test` and `npx playwright test`, every
230
+ test passing, the time of each before and after recorded, and the slowest
231
+ tests the run lists read.
232
+
233
+ ## Cache built assets for good
234
+
235
+ The foundations' `server.js` serves each content-hashed file under
236
+ `dist/assets/` with `Cache-Control: public, max-age=31536000, immutable`, so a
237
+ page load no longer revalidates every script and stylesheet the build made;
238
+ `index.html` and unhashed files are still revalidated on every load. An
239
+ application copies `HASHED_ASSET`, `HASHED_ASSET_CACHE` and the
240
+ `reply.sendFile()` choice from its foundation's `example/server.js` into its
241
+ own `server.js`, with the browser test "built assets are kept for good and the
242
+ page itself is revalidated" from `example/tests/e2e/startup.spec.ts`. A spec
243
+ that mocks requests with `page.route()` turns Playwright's HTTP cache off, so
244
+ it still fetches every asset on each navigation.
245
+
246
+ ## Sign each browser worker in once
247
+
248
+ A suite whose tests sign in one by one, such as a helper that calls
249
+ `loginAsExistingUser()` at the start of each test, signs each worker's server
250
+ in once instead, with `browserTestConfig({ signIn: "tests/e2e/sign-in.js" })`:
251
+
252
+ ```js
253
+ // tests/e2e/sign-in.js: `env` is the worker server's environment.
254
+ import { loginRequestAsExistingUser } from "@jskit-ai/auth-web/test/playwright";
255
+
256
+ export default ({ request, env }) =>
257
+ loginRequestAsExistingUser(request, { email: "owner@example.com", secret: env.AUTH_DEV_BYPASS_SECRET });
258
+ ```
259
+
260
+ The servers' launcher runs it once per server, after the servers start. Each
261
+ test of that worker then starts from the signed-in storage state, which the
262
+ worker's process also finds in `PLAYWRIGHT_STORAGE_STATE`, as on a managed
263
+ target, so a helper that skips signing in under `PLAYWRIGHT_STORAGE_STATE`
264
+ skips it locally too. A test that must start signed out uses
265
+ `test.use({ storageState: { cookies: [], origins: [] } })`; a test that signs
266
+ out or ends other sessions signs in on its own first, so the worker's shared
267
+ session lives on. A login state the host supplies is used as it is.
268
+
194
269
  ## Application migration
195
270
 
196
271
  The updater changes package manifests and the lockfile only. It does not
@@ -251,6 +326,9 @@ the client, or `timeoutMs` on an individual request, for a different positive
251
326
  finite integer duration in milliseconds. CSRF session reads use `readTimeoutMs`.
252
327
  Cancellation and deadline errors are not retried by the transient HTTP client;
253
328
  the deadline applies per HTTP attempt, not to an entire query/retry lifecycle.
329
+ A transient failure of a `GET`/`HEAD` that the client already retried is not
330
+ retried again by `shouldRetryTransientQueryFailure`, so the app QueryClient
331
+ defaults need no change and an unreachable read makes three requests, not nine.
254
332
 
255
333
  Writes and `requestStream()` have no default overall deadline. They accept an
256
334
  explicit `timeoutMs`; long-lived streams still need lifecycle cancellation.
@@ -237,7 +237,7 @@ const ConversationClientProvider = defineProvider({
237
237
  },
238
238
  /** @param {{ components: import("@jskit-ai/kernel/client/types").ClientComponents }} capabilities */
239
239
  setup({ components }) {
240
- components.register("conversation.web.settings.element", ConversationSettingsElement);
240
+ components.register("conversation.web.status.widget", ConversationStatusWidget);
241
241
  }
242
242
  });
243
243
 
@@ -246,6 +246,40 @@ const components = new Map();
246
246
  export function registerComponent(id, component) { components.set(id, component); }
247
247
  ```
248
248
 
249
+ ### Provider subpaths
250
+
251
+ **Export each client provider by a subpath of its own. The provider's file
252
+ imports only what every page needs, such as a widget the shell always shows; a
253
+ screen that only some routes show is imported by those routes.**
254
+
255
+ Why: every page of every app loads the client providers, and what they import,
256
+ before it shows anything. The client bootstrap imports a provider through the
257
+ subpath export that names its file, so the screens `./client` re-exports, and
258
+ their libraries, load only with the routes that show them. A provider without a
259
+ subpath, or a `./client` that declares `routeComponents` or `clientRoutes` or
260
+ uses `export *`, brings all of `./client` to every page.
261
+
262
+ ```json
263
+ // good: package.json
264
+ "exports": {
265
+ "./client": "./src/client/index.js",
266
+ "./client/providers/ConversationClientProvider": "./src/client/providers/ConversationClientProvider.js"
267
+ }
268
+
269
+ // bad: package.json, no subpath for the provider, so every page loads all of ./client
270
+ "exports": {
271
+ "./client": "./src/client/index.js"
272
+ }
273
+ ```
274
+
275
+ ```js
276
+ // good: the provider's file imports the widget every page shows
277
+ import ConversationStatusWidget from "../components/ConversationStatusWidget.vue";
278
+
279
+ // bad: the provider's file imports a screen that only its route shows
280
+ import ConversationSettingsScreen from "../components/ConversationSettingsScreen.vue";
281
+ ```
282
+
249
283
  ### Injected values
250
284
 
251
285
  **`inject()` takes the owner's exported key, never a literal. A required value
@@ -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 the test Knex instance's `query` event at a few
82
- rows and at a realistic number; the count should follow tables and pages,
83
- not rows ([Query counts at realistic
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](/guide/framework/typed-features#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.
@@ -1030,12 +1030,27 @@ export function resetInvoiceCacheForTests() {
1030
1030
 
1031
1031
  ### Query counts at realistic sizes
1032
1032
 
1033
- **A list, batch or write path's database test counts its queries with the
1034
- test Knex instance's `query` event at a few rows and at a realistic number,
1035
- and fails when the count grows with the rows instead of the pages.**
1036
-
1033
+ **A list, batch or write path's database test counts its queries with
1034
+ `countQueries()` at a few rows and at a realistic number, and fails when the
1035
+ count grows with the rows instead of the pages.**
1036
+
1037
+ - `countQueries(knex, work)`, from
1038
+ `@jskit-ai/database-runtime/server/testDatabases`, runs `work` and returns
1039
+ its `result`, the `count` of statements the Knex instance or transaction
1040
+ sent meanwhile (a transaction's `BEGIN` and `COMMIT` included) and their
1041
+ SQL in `queries`. Pass the Knex the path runs on: the one the test built its
1042
+ repository or service with.
1043
+ - A route is counted through the server the test boots in its own process
1044
+ (`createServer()`, then `app.inject()`): `countRuntimeQueries(work)`, from
1045
+ the same module, records what the app's own `runtime.database` sends while
1046
+ `work` runs, and leaves out the test's own Knex. A server in another process
1047
+ is out of its reach, so the route's guard boots it in the test.
1037
1048
  - Pages add a few queries; rows should add none. A path that pages nothing
1038
1049
  (one read, one write) compares a short and a long history.
1050
+ - When the counts differ more than the pages explain, read `queries`: the
1051
+ statement that repeats once per row is the one to batch. Judge the
1052
+ comparison by what the path has to read; never tune a fixed ceiling until
1053
+ it passes.
1039
1054
  - When it fails, fix the reads ([Lists read related rows in
1040
1055
  batches](#lists-read-related-rows-in-batches), [Bounded access
1041
1056
  checks](#bounded-access-checks)).
@@ -1046,17 +1061,7 @@ cannot.
1046
1061
 
1047
1062
  ```js
1048
1063
  // good
1049
- async function countQueries(knex, work) {
1050
- let queries = 0;
1051
- const count = () => { queries += 1; };
1052
- knex.on("query", count);
1053
- try {
1054
- await work();
1055
- } finally {
1056
- knex.off("query", count);
1057
- }
1058
- return queries;
1059
- }
1064
+ import { countQueries, countRuntimeQueries } from "@jskit-ai/database-runtime/server/testDatabases";
1060
1065
 
1061
1066
  test("the invoice list's queries follow its pages, not its invoices", async () => {
1062
1067
  const listAt = async (count) => {
@@ -1067,7 +1072,22 @@ test("the invoice list's queries follow its pages, not its invoices", async () =
1067
1072
  const few = await listAt(10);
1068
1073
  const many = await listAt(300);
1069
1074
  // Thirty times the invoices may add pages; a read per invoice would add about 290 queries.
1070
- assert.ok(many <= few * 2, `${few} queries for 10 invoices, ${many} for 300`);
1075
+ assert.ok(many.count <= few.count * 2,
1076
+ `${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
1077
+ });
1078
+
1079
+ // good: the same comparison for the route, through the server this test booted
1080
+ test("GET /api/invoices reads per page, not per invoice", async () => {
1081
+ const app = await createServer({ runtimeEnv });
1082
+ const listAt = async (count) => {
1083
+ const workspace = await createWorkspace(knex);
1084
+ await seedInvoices(knex, { workspace, count, linesEach: 5 });
1085
+ return countRuntimeQueries(() => app.inject({ url: invoicesUrl(workspace), headers: signedIn }));
1086
+ };
1087
+ const few = await listAt(10);
1088
+ const many = await listAt(300);
1089
+ assert.ok(many.count <= few.count * 2,
1090
+ `${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
1071
1091
  });
1072
1092
 
1073
1093
  // bad: three invoices, nothing counted, so a read per invoice passes
@@ -1101,37 +1121,63 @@ and Vitest's `maxWorkers` come from `testConcurrency()`.**
1101
1121
  file uses, its own Vite `cacheDir` for each dev server it starts (Vite
1102
1122
  replaces a shared dependency cache under servers still reading it), and no
1103
1123
  writes into the source tree. Files that collide are isolated, never
1104
- serialised. A Playwright spec works on its own records, so
1105
- specs can share one server.
1124
+ serialised. A database test file gets its own copy of `TEST_DB_NAME` from
1125
+ `jskit test --databases`
1126
+ ([integration test databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)).
1127
+ - An application's `playwright.config.mjs` is
1128
+ `defineConfig(browserTestConfig({ testDir: "./tests/e2e" }))` from
1129
+ `@jskit-ai/jskit-catalog/browser-tests`. It builds the production frontend
1130
+ once (reused while its sources are unchanged) and gives each worker its own
1131
+ server, port and, through its `prepare` module, its own seeded database; a
1132
+ spec file's tests run in order in one worker. With `PLAYWRIGHT_BASE_URL`, the
1133
+ tests use that managed target as its host serves it. Every run is bounded by
1134
+ a `globalTimeout` (20 minutes, or the suite's own measured bound) and ends
1135
+ with its 10 slowest tests. With a `signIn` module, each worker's server is
1136
+ signed in once and every test of that worker starts signed in, instead of
1137
+ each test signing in. `jskit check` fails a webServer that runs the development server
1138
+ (`npm run develop`, `vite`), one webServer shared by every worker, and a
1139
+ config without a `globalTimeout`.
1106
1140
  - A file that truly cannot run beside others, because it holds a resource of
1107
1141
  the whole machine, says why on its first line,
1108
1142
  `// jskit-test: alone - <reason>`. `jskit test` runs it after the others,
1109
1143
  one such file at a time, and fails on a marker without a reason or below the
1110
1144
  first line.
1145
+ - `jskit test` ends with its 10 slowest test files and tests, records each
1146
+ file's time in `node_modules/.cache/jskit-test/file-times.json`, and fails
1147
+ when a file takes more than 10 minutes or the whole command more than 45:
1148
+ bounds only a clearly broken suite reaches, such as a file waiting out its
1149
+ timeouts. A suite whose measured work needs more sets `runMs`, `fileMs` or a
1150
+ file's own bound in `test-time-bounds.json`, with the measurement in the
1151
+ commit that raises it; a file that got slow is fixed instead. A file still
1152
+ running at twice its bound, or sooner at ten times the time it last finished
1153
+ in (at least two minutes), is stopped and fails the run by name, so the other
1154
+ files still finish and report: look for what it leaves open (a server, child
1155
+ process, timer or socket) or a test without a timeout. A file with its own
1156
+ bound is stopped at twice that bound.
1111
1157
 
1112
1158
  Why: `jskit test` and `testConcurrency()` read one budget, so every runner
1113
1159
  uses the machine without starving the other runs on it.
1114
1160
 
1115
1161
  ```js
1116
1162
  // good: package.json "test": "jskit test", and playwright.config.mjs
1117
- import { testConcurrency } from "@jskit-ai/jskit-catalog/test-budget";
1163
+ import { browserTestConfig } from "@jskit-ai/jskit-catalog/browser-tests";
1118
1164
 
1119
- export default defineConfig({ workers: testConcurrency() });
1165
+ export default defineConfig(browserTestConfig({ testDir: "./tests/e2e" }));
1120
1166
 
1121
1167
  // bad: "test": "node --test --test-concurrency=1", and
1122
- export default defineConfig({ workers: 1 });
1168
+ export default defineConfig({ workers: 1, webServer: { command: "npm run develop" } });
1123
1169
  ```
1124
1170
 
1125
1171
  ### Test databases are prepared once
1126
1172
 
1127
1173
  **A test run prepares each database once and restores it from a stored
1128
- snapshot of the schema, migration ledger and seed rows, checked by a digest of
1129
- their sources.**
1174
+ snapshot of the schema and the rows the migrations write, migration ledger
1175
+ included, checked by a digest of the migrations.**
1130
1176
 
1131
- - The snapshot is rebuilt only when the digest of the migration and seed
1132
- sources changes, once and outside the timed tests; every disposable
1177
+ - The snapshot is rebuilt only when the digest of the migrations' names and
1178
+ contents changes, once and outside the timed tests; every disposable
1133
1179
  database, and every database a test migrated up or down, is restored from
1134
- it.
1180
+ it. A browser run's fixture seed runs after each restore.
1135
1181
  - The full migration history runs once, in its own fresh-schema check
1136
1182
  ([database verification](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#verification)).
1137
1183
  - The harness reports each preparation phase and its duration, and those
@@ -1145,8 +1191,8 @@ Why: an application's migrations include those of every installed package, so
1145
1191
  replaying them costs more with each package as well as each migration.
1146
1192
 
1147
1193
  ```js
1148
- // good: the application's test support restores the snapshot its digest names
1149
- before(async () => restoreTestDatabase(knex, await testDatabaseSnapshot()));
1194
+ // good: package.json "test": "jskit test --databases"; each file gets a restored copy of TEST_DB_NAME
1195
+ const { databases } = await prepareTestDatabases({ knexConfig, environment: process.env, count: workers, seed });
1150
1196
 
1151
1197
  // bad: every file replays every migration
1152
1198
  before(() => knex.migrate.latest());
@@ -1346,8 +1392,11 @@ that must run alone says so on its own first line instead.
1346
1392
  | Rule | Reports | Section |
1347
1393
  | --- | --- | --- |
1348
1394
  | `test/node-test` | a script that runs `node --test` directly instead of `jskit test` | [Test files run in parallel](#test-files-run-in-parallel) |
1349
- | `test/fixed-concurrency` | `--test-concurrency` in a script | [Test files run in parallel](#test-files-run-in-parallel) |
1350
- | `test/playwright-workers` | `--workers` or `-j` passed to `playwright test`, or its config without `workers: testConcurrency()` | [Test files run in parallel](#test-files-run-in-parallel) |
1395
+ | `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) |
1396
+ | `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) |
1397
+ | `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) |
1398
+ | `test/playwright-isolation` | a Playwright webServer every worker shares, without `browserTestConfig()` | [Test files run in parallel](#test-files-run-in-parallel) |
1399
+ | `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) |
1351
1400
  | `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) |
1352
1401
 
1353
1402
  The rules see shapes, not intent. Feature files, owned state beyond
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.280",
3
+ "version": "0.1.282",
4
4
  "description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -5,7 +5,7 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@jskit-ai/kernel": "0.2.11",
8
+ "@jskit-ai/kernel": "0.2.13",
9
9
  "json-rest-schema": "^1.0.17"
10
10
  },
11
11
  "exports": {