@jskit-ai/agent-docs 0.1.280 → 0.1.281
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 +75 -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/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/kernel.md +27 -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/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
|
@@ -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
|
|
66
|
-
room
|
|
67
|
-
|
|
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
|
|
70
|
-
rooms; nothing is reused from one publish or pass to the
|
|
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,
|
|
95
|
-
setup. `
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
the
|
|
410
|
-
one
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
so
|
|
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.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
and restores normal Preview after the
|
|
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
|
|
60
|
-
|
|
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
|
|
@@ -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.
|
|
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
|
|
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](/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
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
1105
|
-
|
|
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 {
|
|
1163
|
+
import { browserTestConfig } from "@jskit-ai/jskit-catalog/browser-tests";
|
|
1118
1164
|
|
|
1119
|
-
export default defineConfig({
|
|
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
|
|
1129
|
-
|
|
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
|
|
1132
|
-
|
|
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:
|
|
1149
|
-
|
|
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`,
|
|
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