@cosmicdrift/kumiko-testing 0.380.0 → 0.381.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/package.json +6 -6
- package/src/changes.json +6 -0
- package/src/helpers/barrier.ts +59 -0
- package/src/helpers/burst.ts +22 -0
- package/src/helpers/index.ts +2 -1
- package/src/stack/db.ts +6 -4
- package/src/stack/index.ts +1 -0
- package/src/stack/test-stack.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.381.0",
|
|
4
4
|
"description": "Test template for Kumiko apps: per-flow seedTenant, app test stack, canonical test preloads, bunfig generator and integration runner.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -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.381.0",
|
|
103
|
+
"@cosmicdrift/kumiko-guards": "0.381.0",
|
|
104
|
+
"@cosmicdrift/kumiko-types": "0.381.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.381.0",
|
|
129
|
+
"@cosmicdrift/kumiko-dev-server": "0.381.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/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.381.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Deterministic concurrency test helpers",
|
|
6
|
+
"detail": "createBarrier proves handler calls overlapped, burst fires N requests and tallies statuses with a guard against the pool size, TestStack exposes dbPoolMax."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.376.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type Barrier = {
|
|
2
|
+
readonly arrive: () => Promise<void>;
|
|
3
|
+
readonly skip: () => void;
|
|
4
|
+
readonly arrived: number;
|
|
5
|
+
readonly skipped: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BARRIER_TIMEOUT_MS = 5000;
|
|
9
|
+
|
|
10
|
+
export function createBarrier(options: {
|
|
11
|
+
readonly expected: number;
|
|
12
|
+
readonly timeoutMs?: number;
|
|
13
|
+
}): Barrier {
|
|
14
|
+
const { expected, timeoutMs = DEFAULT_BARRIER_TIMEOUT_MS } = options;
|
|
15
|
+
if (expected < 1) {
|
|
16
|
+
throw new Error(`barrier expected must be at least 1, got ${expected}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let arrived = 0;
|
|
20
|
+
let skipped = 0;
|
|
21
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
22
|
+
const waiting: { resolve: () => void; reject: (error: Error) => void }[] = [];
|
|
23
|
+
|
|
24
|
+
function openIfComplete(): void {
|
|
25
|
+
if (arrived + skipped >= expected) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
for (const waiter of waiting.splice(0)) waiter.resolve();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
arrive() {
|
|
33
|
+
arrived++;
|
|
34
|
+
const pending = new Promise<void>((resolve, reject) => {
|
|
35
|
+
waiting.push({ resolve, reject });
|
|
36
|
+
});
|
|
37
|
+
if (arrived === 1 && arrived + skipped < expected) {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
const error = new Error(
|
|
40
|
+
`barrier timed out: ${arrived} of ${expected} arrived, ${skipped} skipped`,
|
|
41
|
+
);
|
|
42
|
+
for (const waiter of waiting.splice(0)) waiter.reject(error);
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
}
|
|
45
|
+
openIfComplete();
|
|
46
|
+
return pending;
|
|
47
|
+
},
|
|
48
|
+
skip() {
|
|
49
|
+
skipped++;
|
|
50
|
+
openIfComplete();
|
|
51
|
+
},
|
|
52
|
+
get arrived() {
|
|
53
|
+
return arrived;
|
|
54
|
+
},
|
|
55
|
+
get skipped() {
|
|
56
|
+
return skipped;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type BurstResult = {
|
|
2
|
+
readonly responses: readonly Response[];
|
|
3
|
+
readonly statuses: Readonly<Record<number, number>>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export async function burst(
|
|
7
|
+
stack: { readonly dbPoolMax: number },
|
|
8
|
+
count: number,
|
|
9
|
+
request: (index: number) => Promise<Response>,
|
|
10
|
+
): Promise<BurstResult> {
|
|
11
|
+
if (count > stack.dbPoolMax) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`burst of ${count} exceeds dbPoolMax ${stack.dbPoolMax}: extra requests queue on the pool and a barrier inside the handler deadlocks; raise TestStackOptions.dbPoolMax or lower the count`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const responses = await Promise.all(Array.from({ length: count }, (_, index) => request(index)));
|
|
17
|
+
const statuses: Record<number, number> = {};
|
|
18
|
+
for (const response of responses) {
|
|
19
|
+
statuses[response.status] = (statuses[response.status] ?? 0) + 1;
|
|
20
|
+
}
|
|
21
|
+
return { responses, statuses };
|
|
22
|
+
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -13,10 +13,11 @@ export {
|
|
|
13
13
|
resetEventPiiCatalogForTests,
|
|
14
14
|
resetPiiSubjectKmsForTests,
|
|
15
15
|
} from "@cosmicdrift/kumiko-framework/internal/testing";
|
|
16
|
-
|
|
17
16
|
export { rolesOf } from "./access-assertions.ts";
|
|
18
17
|
export { expectError, expectSuccess } from "./assertions.ts";
|
|
18
|
+
export { type Barrier, createBarrier } from "./barrier.ts";
|
|
19
19
|
export { withBootValidatorFixture } from "./boot-validator-fixture.ts";
|
|
20
|
+
export { type BurstResult, burst } from "./burst.ts";
|
|
20
21
|
export {
|
|
21
22
|
captureClosedConnectionError,
|
|
22
23
|
terminateBackendsByApplicationName,
|
package/src/stack/db.ts
CHANGED
|
@@ -22,8 +22,11 @@ function requireEnv(name: string): string {
|
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export const DEFAULT_TEST_DB_POOL_MAX = 10;
|
|
26
|
+
|
|
25
27
|
export type TestDb = {
|
|
26
28
|
db: DbConnection;
|
|
29
|
+
poolMax: number;
|
|
27
30
|
outsideTransactionDb: DbConnection;
|
|
28
31
|
client: PgListenClient;
|
|
29
32
|
dbName: string;
|
|
@@ -59,10 +62,8 @@ export async function createTestDb(arg?: string | CreateTestDbOptions): Promise<
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
const testUrl = url.replace(/\/[^/]+$/, `/${dbName}`);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
...(opts.poolMax !== undefined ? { maxConnections: opts.poolMax } : {}),
|
|
65
|
-
});
|
|
65
|
+
const poolMax = opts.poolMax ?? DEFAULT_TEST_DB_POOL_MAX;
|
|
66
|
+
const conn = createDbConnection({ url: testUrl, maxConnections: poolMax });
|
|
66
67
|
const outsideConn = createDbConnection({
|
|
67
68
|
url: testUrl,
|
|
68
69
|
maxConnections: opts.outsideTransactionPoolMax ?? DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX,
|
|
@@ -73,6 +74,7 @@ export async function createTestDb(arg?: string | CreateTestDbOptions): Promise<
|
|
|
73
74
|
|
|
74
75
|
return {
|
|
75
76
|
db: conn.db,
|
|
77
|
+
poolMax,
|
|
76
78
|
outsideTransactionDb: outsideConn.db,
|
|
77
79
|
client: conn.client,
|
|
78
80
|
dbName,
|
package/src/stack/index.ts
CHANGED
package/src/stack/test-stack.ts
CHANGED
|
@@ -61,6 +61,8 @@ export type TestStack = {
|
|
|
61
61
|
jwt: JwtHelper;
|
|
62
62
|
registry: Registry;
|
|
63
63
|
db: import("@cosmicdrift/kumiko-framework/db").DbConnection;
|
|
64
|
+
/** Resolved connection pool size of `db`. */
|
|
65
|
+
readonly dbPoolMax: number;
|
|
64
66
|
redis: TestRedis;
|
|
65
67
|
// The bus this stack publishes/subscribes on; pass to TierResolverPlugin.build
|
|
66
68
|
// so a tier-engine resolver built outside buildServer joins the same sync.
|
|
@@ -686,6 +688,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
686
688
|
jwt: server.jwt,
|
|
687
689
|
registry,
|
|
688
690
|
db: testDb.db,
|
|
691
|
+
dbPoolMax: testDb.poolMax,
|
|
689
692
|
redis: testRedis,
|
|
690
693
|
cacheSync,
|
|
691
694
|
search: searchAdapter,
|