@cosmicdrift/kumiko-testing 0.386.0 → 0.388.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -57
- package/package.json +7 -7
- package/src/bunfig.ts +2 -2
- package/src/changes.json +18 -0
- package/src/e2e/boundary-guard.ts +37 -13
- package/src/e2e/index.ts +5 -1
- package/src/e2e/screenshots.ts +2 -2
- package/src/e2e/seed-route.ts +8 -5
- package/src/helpers/barrier.ts +5 -1
- package/src/helpers/boot-validator-fixture.ts +1 -1
- package/src/helpers/console-calls.ts +12 -0
- package/src/helpers/e2e-generator.ts +2 -2
- package/src/helpers/index.ts +4 -3
- package/src/helpers/sse-frames.ts +6 -1
- package/src/helpers/without-ambient-temporal.ts +1 -1
- package/src/seed-types.ts +1 -1
- package/src/stack/db.ts +2 -2
- package/src/stack/redis.ts +9 -4
- package/src/stack/request-helper.ts +1 -1
- package/src/stack/test-stack-setup.ts +607 -0
- package/src/stack/test-stack.ts +45 -528
package/LICENSE
CHANGED
|
@@ -1,57 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
controlling, or under common control with the Licensor ("Affiliates"), or
|
|
23
|
-
contractors acting on their behalf. The Licensor remains free to use the
|
|
24
|
-
Licensed Work for any purpose, including for the operation of kumiko.rocks.
|
|
25
|
-
|
|
26
|
-
Change Date: 2030-05-05
|
|
27
|
-
Change License: Apache License, Version 2.0
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Terms
|
|
31
|
-
|
|
32
|
-
The Licensor hereby grants you the right to copy, modify, create derivative works,
|
|
33
|
-
redistribute, and make non-production use of the Licensed Work. The Licensor may
|
|
34
|
-
make an Additional Use Grant, above, permitting limited production use.
|
|
35
|
-
|
|
36
|
-
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
37
|
-
available distribution of the Licensed Work under this License, whichever comes
|
|
38
|
-
first, this License will convert to the Change License.
|
|
39
|
-
|
|
40
|
-
This Business Source License governs use of the Licensed Work in all cases, except
|
|
41
|
-
as to any use that is explicitly granted in the Additional Use Grant above or
|
|
42
|
-
under the Change License after the Change Date.
|
|
43
|
-
|
|
44
|
-
If your use of the Licensed Work does not comply with the requirements of this
|
|
45
|
-
License, you must cease use of the Licensed Work immediately.
|
|
46
|
-
|
|
47
|
-
All copies of the Licensed Work, and all derivative works thereof, must include
|
|
48
|
-
this License.
|
|
49
|
-
|
|
50
|
-
This License does not grant you any right, title, or interest in any trademark,
|
|
51
|
-
logo, or branding of the Licensor, except as required to comply with this License.
|
|
52
|
-
|
|
53
|
-
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
|
|
54
|
-
“AS IS” BASIS. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
|
|
55
|
-
WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
56
|
-
TITLE, AND NON-INFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES
|
|
57
|
-
ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF THE LICENSED WORK.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Frost
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.388.0",
|
|
4
4
|
"description": "Test template for Kumiko apps: per-flow seedTenant, app test stack, canonical test preloads, bunfig generator and integration runner.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -99,9 +99,9 @@
|
|
|
99
99
|
"kumiko-testing": "./bin/kumiko-testing.ts"
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
103
|
-
"@cosmicdrift/kumiko-guards": "0.
|
|
104
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
102
|
+
"@cosmicdrift/kumiko-framework": "0.388.0",
|
|
103
|
+
"@cosmicdrift/kumiko-guards": "0.388.0",
|
|
104
|
+
"@cosmicdrift/kumiko-types": "0.388.0",
|
|
105
105
|
"zod": "^4.4.3"
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
@@ -125,8 +125,8 @@
|
|
|
125
125
|
}
|
|
126
126
|
},
|
|
127
127
|
"devDependencies": {
|
|
128
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
129
|
-
"@cosmicdrift/kumiko-dev-server": "0.
|
|
128
|
+
"@cosmicdrift/kumiko-bundled-features": "0.388.0",
|
|
129
|
+
"@cosmicdrift/kumiko-dev-server": "0.388.0",
|
|
130
130
|
"@happy-dom/global-registrator": "^20.11.2",
|
|
131
131
|
"@playwright/test": "^1.62.1",
|
|
132
132
|
"@testing-library/dom": "^10.4.1",
|
package/src/bunfig.ts
CHANGED
|
@@ -102,7 +102,7 @@ type BunfigBlock = { readonly header: string; readonly lines: readonly string[]
|
|
|
102
102
|
// contain "[" are never alone on their own line. Anything before the first
|
|
103
103
|
// header (top-level keys like `preload`/`telemetry`/`smol`/`logLevel`, plus
|
|
104
104
|
// the header comments) is its own block under the synthetic header "" —
|
|
105
|
-
// dropping it silently lost those keys
|
|
105
|
+
// dropping it silently lost those keys.
|
|
106
106
|
function splitBunfigBlocks(content: string): readonly BunfigBlock[] {
|
|
107
107
|
const blocks: { header: string; lines: string[] }[] = [];
|
|
108
108
|
let current: { header: string; lines: string[] } = { header: "", lines: [] };
|
|
@@ -224,7 +224,7 @@ function appendArrayExtras(
|
|
|
224
224
|
* verbatim, appended after the generated content — so a section the
|
|
225
225
|
* template doesn't own this run is never silently dropped. A key inside a
|
|
226
226
|
* section the template DOES emit that isn't in this run's own version of
|
|
227
|
-
* that section (e.g. `concurrency`, `timeout
|
|
227
|
+
* that section (e.g. `concurrency`, `timeout`) fails
|
|
228
228
|
* the merge instead of silently disappearing. Within an array key the
|
|
229
229
|
* template does emit on both sides (`preload`, `pathIgnorePatterns`,
|
|
230
230
|
* `coveragePathIgnorePatterns`), entries the app appended on top of the
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "setupTestStack split into setup phases",
|
|
6
|
+
"detail": "setupTestStack() keeps its signature and behavior. Feature validation, storage provisioning with its DDL cleanup, cache sync, services, the job runner and the server start are separate phases in stack/test-stack-setup.ts. The cleanup on a failed setup still stops the job runner, closes cache sync and drops the test database and Redis in that order."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Barrier, SSE reader, boundary guard and Redis helper fixes",
|
|
12
|
+
"detail": "createBarrier rejects late arrivals after a timeout instead of hanging. readSseFrames accepts CRLF and CR line endings. The error-boundary guard observes every page of the context and no longer fails on closed pages. createTestRedis disconnects the client when the initial connect fails. createE2eSeedRoutes of the e2e/seed-route subpath is also available as createE2eSeedRoutesWith, the old name stays as a deprecated alias."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.388.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "consoleCallsMentioning helper for console spies",
|
|
18
|
+
"detail": "`@cosmicdrift/kumiko-testing/helpers` exports `consoleCallsMentioning(spy, fragment)` and `ConsoleSpyLike`, replacing the per-file filters tests used to ignore console output of other test files."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.386.0",
|
|
4
22
|
"type": "fix",
|
|
@@ -3,7 +3,40 @@ import {
|
|
|
3
3
|
ROOT_ERROR_FALLBACK_TEST_ID,
|
|
4
4
|
SCREEN_ERROR_FALLBACK_TEST_ID,
|
|
5
5
|
} from "@cosmicdrift/kumiko-types/error-boundary";
|
|
6
|
-
import { test as base, expect } from "@playwright/test";
|
|
6
|
+
import { type BrowserContext, test as base, expect } from "@playwright/test";
|
|
7
|
+
|
|
8
|
+
export type BoundaryFindings = {
|
|
9
|
+
readonly boundaryLogs: readonly string[];
|
|
10
|
+
readonly visibleFallbacks: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const FALLBACK_SELECTOR = `[data-testid="${SCREEN_ERROR_FALLBACK_TEST_ID}"], [data-testid="${ROOT_ERROR_FALLBACK_TEST_ID}"]`;
|
|
14
|
+
|
|
15
|
+
// Listens on the context so pages opened later (popups, context.newPage()) are covered too.
|
|
16
|
+
export function observeErrorBoundaries(context: BrowserContext): {
|
|
17
|
+
readonly findings: () => Promise<BoundaryFindings>;
|
|
18
|
+
} {
|
|
19
|
+
const boundaryLogs: string[] = [];
|
|
20
|
+
context.on("console", (msg) => {
|
|
21
|
+
if (msg.type() === "error" && msg.text().includes(ERROR_BOUNDARY_LOG_MARKER)) {
|
|
22
|
+
boundaryLogs.push(msg.text());
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
async findings() {
|
|
27
|
+
let visibleFallbacks = 0;
|
|
28
|
+
for (const page of context.pages()) {
|
|
29
|
+
// A spec may close its page or a page may crash; counting then would throw and mask the test result.
|
|
30
|
+
if (page.isClosed()) continue;
|
|
31
|
+
visibleFallbacks += await page
|
|
32
|
+
.locator(FALLBACK_SELECTOR)
|
|
33
|
+
.count()
|
|
34
|
+
.catch(() => 0);
|
|
35
|
+
}
|
|
36
|
+
return { boundaryLogs: [...boundaryLogs], visibleFallbacks };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
7
40
|
|
|
8
41
|
// An error boundary keeps the page alive, which also hides the failure from `pageerror`.
|
|
9
42
|
// Every spec using `test` from this package fails when a boundary caught something.
|
|
@@ -13,21 +46,12 @@ export const boundaryGuardedTest = base.extend<{
|
|
|
13
46
|
}>({
|
|
14
47
|
expectErrorBoundary: [false, { option: true }],
|
|
15
48
|
failOnErrorBoundary: [
|
|
16
|
-
async ({
|
|
17
|
-
const
|
|
18
|
-
page.on("console", (msg) => {
|
|
19
|
-
if (msg.type() === "error" && msg.text().includes(ERROR_BOUNDARY_LOG_MARKER)) {
|
|
20
|
-
boundaryLogs.push(msg.text());
|
|
21
|
-
}
|
|
22
|
-
});
|
|
49
|
+
async ({ context, expectErrorBoundary }, use) => {
|
|
50
|
+
const observer = observeErrorBoundaries(context);
|
|
23
51
|
await use(undefined);
|
|
24
52
|
// skip: the spec provokes a boundary on purpose and asserts the fallback itself
|
|
25
53
|
if (expectErrorBoundary) return;
|
|
26
|
-
const visibleFallbacks = await
|
|
27
|
-
.locator(
|
|
28
|
-
`[data-testid="${SCREEN_ERROR_FALLBACK_TEST_ID}"], [data-testid="${ROOT_ERROR_FALLBACK_TEST_ID}"]`,
|
|
29
|
-
)
|
|
30
|
-
.count();
|
|
54
|
+
const { boundaryLogs, visibleFallbacks } = await observer.findings();
|
|
31
55
|
expect(
|
|
32
56
|
{ boundaryLogs, visibleFallbacks },
|
|
33
57
|
"an error boundary caught a render/effect error",
|
package/src/e2e/index.ts
CHANGED
|
@@ -22,7 +22,11 @@ export {
|
|
|
22
22
|
syntheticClientIpFor,
|
|
23
23
|
totpCode,
|
|
24
24
|
} from "./auth-kit";
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
type BoundaryFindings,
|
|
27
|
+
boundaryGuardedTest,
|
|
28
|
+
observeErrorBoundaries,
|
|
29
|
+
} from "./boundary-guard";
|
|
26
30
|
export {
|
|
27
31
|
CSRF_COOKIE_NAME,
|
|
28
32
|
CSRF_HEADER_NAME,
|
package/src/e2e/screenshots.ts
CHANGED
|
@@ -324,7 +324,7 @@ export function runScreenshots(scenarios: readonly Scenario[], opts: FlatOptions
|
|
|
324
324
|
// Browser-context locale for JS-side Intl/navigator.language (e.g. money-input's
|
|
325
325
|
// resolvedLocale) — pinEnglishLocale() only seeds the app's own kumiko:locale.
|
|
326
326
|
// ponytail: native <input type="number"> still formats per the host OS region,
|
|
327
|
-
// unreachable from Playwright (context.locale and --lang both no-op there).
|
|
327
|
+
// unreachable from Playwright (context.locale and --lang both no-op there).
|
|
328
328
|
if (opts.pinLocale) test.use({ locale: "en-US" });
|
|
329
329
|
for (const s of scenarios) {
|
|
330
330
|
test(
|
|
@@ -556,7 +556,7 @@ export function runMatrix<T extends string>(
|
|
|
556
556
|
test.describe(locale, () => {
|
|
557
557
|
// Browser-context locale for JS-side Intl/navigator.language — the
|
|
558
558
|
// kumiko:locale seed below only drives the app's own i18n strings.
|
|
559
|
-
// ponytail: see the runScreenshots() locale comment above
|
|
559
|
+
// ponytail: see the runScreenshots() locale comment above.
|
|
560
560
|
const tag = resolveLocaleTag(locale, opts.localeTags);
|
|
561
561
|
test.use({ locale: tag });
|
|
562
562
|
|
package/src/e2e/seed-route.ts
CHANGED
|
@@ -51,7 +51,7 @@ function parseJsonBody<S extends z.ZodType>(schema: S, raw: string): ParsedBody<
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Shared by every verify() below — a failed parse becomes a 400 the same way
|
|
54
|
-
// the
|
|
54
|
+
// the handler-level parseJsonBody/parseWith checks did.
|
|
55
55
|
function parseOrReject<T>(value: ParsedBody<T>): T {
|
|
56
56
|
if (value.success) return value.data;
|
|
57
57
|
throw new ExtraRouteRejection(400, { error: value.error });
|
|
@@ -153,10 +153,10 @@ function toCapturedMail(message: InboxMessage): CapturedMail {
|
|
|
153
153
|
// still throw in every environment) — the *mounting* decision is the
|
|
154
154
|
// caller's: use isE2eSeedingEnabled() to keep prod from ever registering
|
|
155
155
|
// these route objects at all, e.g. `...(isE2eSeedingEnabled() ?
|
|
156
|
-
// createE2eSeedRoutes() : [])` in a server entry that also runs in prod
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
export function
|
|
156
|
+
// createE2eSeedRoutes() : [])` in a server entry that also runs in prod.
|
|
157
|
+
// Each route's own assertGateOpen() 404s outside the seed condition regardless
|
|
158
|
+
// — this is belt-and-suspenders, not the only gate.
|
|
159
|
+
export function createE2eSeedRoutesWith(
|
|
160
160
|
options: E2eSeedRoutesOptions,
|
|
161
161
|
bundled: SeedRouteBundledApi,
|
|
162
162
|
): readonly ExtraRouteDefinition[] {
|
|
@@ -327,3 +327,6 @@ export function createE2eSeedRoutes(
|
|
|
327
327
|
|
|
328
328
|
return [seedTenantRoute, seedUserRoute, inboxRoute, extraSeedRoute];
|
|
329
329
|
}
|
|
330
|
+
|
|
331
|
+
/** @deprecated Use `createE2eSeedRoutesWith`; the old name collides with the bound `createE2eSeedRoutes` of dev-server/testing. */
|
|
332
|
+
export const createE2eSeedRoutes = createE2eSeedRoutesWith;
|
package/src/helpers/barrier.ts
CHANGED
|
@@ -19,6 +19,8 @@ export function createBarrier(options: {
|
|
|
19
19
|
let arrived = 0;
|
|
20
20
|
let skipped = 0;
|
|
21
21
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
22
|
+
// Late arrivals after a timeout must fail loudly too instead of hanging until the test timeout.
|
|
23
|
+
let timedOutError: Error | undefined;
|
|
22
24
|
const waiting: { resolve: () => void; reject: (error: Error) => void }[] = [];
|
|
23
25
|
|
|
24
26
|
function openIfComplete(): void {
|
|
@@ -30,15 +32,17 @@ export function createBarrier(options: {
|
|
|
30
32
|
|
|
31
33
|
return {
|
|
32
34
|
arrive() {
|
|
35
|
+
if (timedOutError !== undefined) return Promise.reject(timedOutError);
|
|
33
36
|
arrived++;
|
|
34
37
|
const pending = new Promise<void>((resolve, reject) => {
|
|
35
38
|
waiting.push({ resolve, reject });
|
|
36
39
|
});
|
|
37
|
-
if (
|
|
40
|
+
if (timer === undefined && arrived + skipped < expected) {
|
|
38
41
|
timer = setTimeout(() => {
|
|
39
42
|
const error = new Error(
|
|
40
43
|
`barrier timed out: ${arrived} of ${expected} arrived, ${skipped} skipped`,
|
|
41
44
|
);
|
|
45
|
+
timedOutError = error;
|
|
42
46
|
for (const waiter of waiting.splice(0)) waiter.reject(error);
|
|
43
47
|
}, timeoutMs);
|
|
44
48
|
}
|
|
@@ -68,7 +68,7 @@ function ensureEntityListRowNavigation(
|
|
|
68
68
|
// No `params` here: this same-entity target always resolves to
|
|
69
69
|
// UPDATE mode (the renderer auto-fills entityId from row["id"]),
|
|
70
70
|
// and entityEdit-update never reads search-params as initial
|
|
71
|
-
// values — the boot-validator now rejects that combination
|
|
71
|
+
// values — the boot-validator now rejects that combination.
|
|
72
72
|
{
|
|
73
73
|
kind: "navigate",
|
|
74
74
|
id: "edit",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ConsoleSpyLike = {
|
|
2
|
+
readonly mock: { readonly calls: ReadonlyArray<ReadonlyArray<unknown>> };
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
// Other test files in the same bun process emit async console output while a console spy is
|
|
6
|
+
// active, so assertions only count the calls that mention the message under test.
|
|
7
|
+
export function consoleCallsMentioning(
|
|
8
|
+
spy: ConsoleSpyLike,
|
|
9
|
+
fragment: string,
|
|
10
|
+
): ReadonlyArray<ReadonlyArray<unknown>> {
|
|
11
|
+
return spy.mock.calls.filter((args) => args.some((arg) => String(arg).includes(fragment)));
|
|
12
|
+
}
|
|
@@ -309,7 +309,7 @@ function buildEditFillOps(
|
|
|
309
309
|
ops.push({ kind: "fill", field, value: String(v) });
|
|
310
310
|
break;
|
|
311
311
|
// embedded/money/timestamp/locatedTimestamp/file/image/files/images:
|
|
312
|
-
// no generic interaction. Since
|
|
312
|
+
// no generic interaction. Since timestamp renders two inputs
|
|
313
313
|
// (date + time) that a single .fill() cannot drive; the test author
|
|
314
314
|
// supplies a hand override or skips it.
|
|
315
315
|
default:
|
|
@@ -489,7 +489,7 @@ function fieldToFixture(name: string, field: FieldDefinition): unknown {
|
|
|
489
489
|
case "images":
|
|
490
490
|
// File-upload fixtures are not generic yet: Playwright would have to supply
|
|
491
491
|
// file contents, and the write API already expects uploaded fileRef UUIDs.
|
|
492
|
-
// Can be added once a sample settles the paths
|
|
492
|
+
// Can be added once a sample settles the paths.
|
|
493
493
|
return undefined;
|
|
494
494
|
default:
|
|
495
495
|
return undefined;
|
package/src/helpers/index.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
// The four cache/injection resets stay in their own modules (they close over
|
|
6
6
|
// module-private state) and are only re-exported here — they are out of /crypto
|
|
7
|
-
// and /db
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// and /db. A production call to resetPiiSubjectKmsForTests() silently switches
|
|
8
|
+
// the PII layer off, and subject-annotated fields are written in plaintext from
|
|
9
|
+
// then on: no error, no log.
|
|
10
10
|
export {
|
|
11
11
|
resetBlindIndexKeyForTests,
|
|
12
12
|
resetEntityFieldEncryptionCacheForTests,
|
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
terminateBackendsByApplicationName,
|
|
24
24
|
testDatabaseUrl,
|
|
25
25
|
} from "./closed-connection-error.ts";
|
|
26
|
+
export { type ConsoleSpyLike, consoleCallsMentioning } from "./console-calls.ts";
|
|
26
27
|
export { type ClearableTable, clearTables, resetTestTables } from "./db-cleanup.ts";
|
|
27
28
|
export {
|
|
28
29
|
type E2EGeneratorOptions,
|
|
@@ -25,6 +25,8 @@ export async function readSseFrames(
|
|
|
25
25
|
const decoder = new TextDecoder();
|
|
26
26
|
const frames: SseFrame[] = [];
|
|
27
27
|
let buffer = "";
|
|
28
|
+
// A "\r" at a chunk end may be the first half of "\r\n", so it is held back until the next chunk.
|
|
29
|
+
let heldCarriageReturn: boolean = false;
|
|
28
30
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
29
31
|
const timedOut = new Promise<never>((_, reject) => {
|
|
30
32
|
timer = setTimeout(
|
|
@@ -40,7 +42,10 @@ export async function readSseFrames(
|
|
|
40
42
|
if (chunk.done) {
|
|
41
43
|
throw new Error(`sse stream ended before condition: ${frames.length} frames read`);
|
|
42
44
|
}
|
|
43
|
-
|
|
45
|
+
const decodedText = decoder.decode(chunk.value, { stream: true });
|
|
46
|
+
const decoded: string = (heldCarriageReturn ? "\r" : "") + decodedText;
|
|
47
|
+
heldCarriageReturn = decoded.endsWith("\r");
|
|
48
|
+
buffer += (heldCarriageReturn ? decoded.slice(0, -1) : decoded).replace(/\r\n?/g, "\n");
|
|
44
49
|
let separator = buffer.indexOf("\n\n");
|
|
45
50
|
while (separator !== -1) {
|
|
46
51
|
const rawFrame = buffer.slice(0, separator);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Run `fn` with `globalThis.Temporal` deleted, then restore.
|
|
3
|
-
* Used by ambient-independence regression tests
|
|
3
|
+
* Used by ambient-independence regression tests.
|
|
4
4
|
*/
|
|
5
5
|
export async function withoutAmbientTemporal<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
6
6
|
const savedGlobal: unknown = Reflect.get(globalThis, "Temporal");
|
package/src/seed-types.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type SeededUser = SeededCredentials & {
|
|
|
28
28
|
export type SeedPart = (ctx: { readonly tenant: SeededTenant }) => Promise<void>;
|
|
29
29
|
|
|
30
30
|
// email may contain the literal "{tenantId}" placeholder, substituted with the
|
|
31
|
-
// seeded tenant's id — user:create's email uniqueness is global
|
|
31
|
+
// seeded tenant's id — user:create's email uniqueness is global,
|
|
32
32
|
// so a fixed literal email collides across per-scenario tenants.
|
|
33
33
|
export type SeedAdminIdentity = {
|
|
34
34
|
readonly displayName?: string;
|
package/src/stack/db.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function testDbTemplatePrefix(runId: string): string {
|
|
|
54
54
|
return `kumiko_test_tpl_${runId}_`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function adminUrlOf(url: string): string {
|
|
57
|
+
export function adminUrlOf(url: string): string {
|
|
58
58
|
return url.replace(/\/[^/]+$/, "/postgres");
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -132,7 +132,7 @@ export function ensureTestDbTemplate(
|
|
|
132
132
|
return pending;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
async function buildTestDbTemplate(
|
|
135
|
+
export async function buildTestDbTemplate(
|
|
136
136
|
templateDbName: string,
|
|
137
137
|
buildSchema: (db: DbConnection) => Promise<void>,
|
|
138
138
|
): Promise<void> {
|
package/src/stack/redis.ts
CHANGED
|
@@ -7,9 +7,8 @@ export type TestRedis = {
|
|
|
7
7
|
// The exact REDIS_URL used to build `redis` above — for a second, unrelated
|
|
8
8
|
// connection (e.g. the test-stack's JobRunner) that needs its own client
|
|
9
9
|
// rather than sharing this one's keyPrefix. Reconstructing a URL from
|
|
10
|
-
// `redis.options` loses password/username/tls/path
|
|
11
|
-
//
|
|
12
|
-
// this raw string, not rebuild one from parsed options.
|
|
10
|
+
// `redis.options` loses password/username/tls/path — callers needing a fresh
|
|
11
|
+
// connection should use this raw string, not rebuild one from parsed options.
|
|
13
12
|
redisUrl: string;
|
|
14
13
|
/** The per-test `keyPrefix` on `redis`, e.g. `kt:1a2b3c4d:`. */
|
|
15
14
|
keyPrefix: string;
|
|
@@ -41,7 +40,13 @@ export async function createTestRedis(opts: CreateTestRedisOptions = {}): Promis
|
|
|
41
40
|
const prefix = opts.borrowKeyPrefix ?? `kt:${generateId().slice(-8)}:`;
|
|
42
41
|
// Callers that check redis.status (rate-limit resolver) must not see "connect" on their first call.
|
|
43
42
|
const redis = new Redis(redisUrl, { keyPrefix: prefix, lazyConnect: true });
|
|
44
|
-
|
|
43
|
+
try {
|
|
44
|
+
await redis.connect();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Without this the client keeps retrying in the background and holds the process open.
|
|
47
|
+
redis.disconnect();
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
45
50
|
|
|
46
51
|
async function deleteKeysMatching(pattern: string): Promise<void> {
|
|
47
52
|
// Open a prefix-less client for the scan — ioredis' keyPrefix is applied
|
|
@@ -109,7 +109,7 @@ export type RequestHelper = {
|
|
|
109
109
|
export type RequestHelperOptions = {
|
|
110
110
|
// When sessionChecker is wired (sessions feature), JWTs without jti are
|
|
111
111
|
// rejected as no_sid. Seed/test helpers that only jwt.sign(user) need a
|
|
112
|
-
// live sid — create one via the same sessionCreator login uses
|
|
112
|
+
// live sid — create one via the same sessionCreator login uses.
|
|
113
113
|
readonly sessionCreator?: SessionCreator;
|
|
114
114
|
};
|
|
115
115
|
|