@elench/testkit 0.1.149 → 0.1.151
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/README.md +29 -9
- package/lib/cli/assistant/view-model.mjs +1 -1
- package/lib/cli/components/blocks/run-tree.mjs +1 -1
- package/lib/cli/renderers/run/events.mjs +4 -3
- package/lib/cli/renderers/run/failure.mjs +2 -2
- package/lib/cli/renderers/run/inline-detail.mjs +2 -2
- package/lib/cli/renderers/run/interactive.mjs +2 -2
- package/lib/cli/renderers/run/text-reporter.mjs +9 -9
- package/lib/cli/state/run/model.mjs +7 -7
- package/lib/cli/state/run/state.mjs +3 -3
- package/lib/cli/terminal/colors.mjs +1 -1
- package/lib/config/database-materialization.mjs +25 -0
- package/lib/config/database.mjs +30 -0
- package/lib/config/index.mjs +47 -1
- package/lib/config/runtime.mjs +130 -0
- package/lib/config-api/index.d.ts +28 -0
- package/lib/config-api/index.mjs +6 -0
- package/lib/database/cleanup.mjs +76 -1
- package/lib/database/constants.mjs +3 -0
- package/lib/database/index.mjs +6 -0
- package/lib/database/local-postgres.mjs +123 -4
- package/lib/database/naming.mjs +7 -0
- package/lib/database/resource-postgres.mjs +13 -0
- package/lib/database/state-files.mjs +17 -0
- package/lib/docker-compat/matrix.mjs +5 -3
- package/lib/kiln/client.mjs +8 -0
- package/lib/local/kiln-driver.mjs +96 -68
- package/lib/ownership/docker.mjs +67 -1
- package/lib/regressions/github-transport.mjs +178 -4
- package/lib/regressions/github.mjs +52 -16
- package/lib/regressions/index.d.ts +58 -29
- package/lib/regressions/index.mjs +171 -58
- package/lib/regressions/workflow.mjs +266 -0
- package/lib/results/artifacts.mjs +8 -7
- package/lib/runner/formatting.mjs +17 -16
- package/lib/runner/orchestrator.mjs +6 -5
- package/lib/runner/planning.mjs +40 -0
- package/lib/runner/regressions.mjs +183 -33
- package/lib/runner/reporting.mjs +1 -1
- package/lib/runner/run-finalization.mjs +34 -4
- package/lib/runner/runtime-manager.mjs +91 -10
- package/lib/runner/scheduler/index.mjs +30 -1
- package/lib/runtime/index.d.ts +5 -5
- package/lib/runtime-src/k6/http.js +11 -11
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/node_modules/es-toolkit/CHANGELOG.md +801 -0
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/package.json +6 -5
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +0 -188
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +0 -1
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +0 -293
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +0 -1
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +0 -25
package/README.md
CHANGED
|
@@ -148,19 +148,28 @@ results without any separate follow-up maintenance command.
|
|
|
148
148
|
- `new regressions`
|
|
149
149
|
- `known regressions`
|
|
150
150
|
- `fixed known regressions`
|
|
151
|
-
- `
|
|
151
|
+
- `case store stale`
|
|
152
152
|
|
|
153
153
|
The default CLI keeps those signals lightweight:
|
|
154
154
|
|
|
155
155
|
- failed files print inline diagnosis immediately under the file line
|
|
156
156
|
- the final summary box reports aggregate regression counts only
|
|
157
157
|
- machine-readable artifacts gain per-file `diagnosis` plus top-level
|
|
158
|
-
`regressions.summary`, `regressions.
|
|
158
|
+
`regressions.summary`, `regressions.caseStore`, `regressions.workflow`, and prepared `regressions.drafts`
|
|
159
159
|
|
|
160
|
-
`
|
|
161
|
-
|
|
160
|
+
`case store stale` is repo hygiene, not a test failure. It means the regression
|
|
161
|
+
case store or linked issue tracker metadata needs attention, for example because a
|
|
162
162
|
linked issue is closed but the regression still reproduces.
|
|
163
163
|
|
|
164
|
+
The canonical durable file is `testkit.regressions.json` with
|
|
165
|
+
`schemaVersion: 2` and a top-level `cases` array. When a configured full run sees
|
|
166
|
+
a new failure, Testkit records an untriaged case automatically. When a known case
|
|
167
|
+
passes, Testkit moves it through `fixed_pending_verification`; after the
|
|
168
|
+
configured stability window it can close the linked GitHub issue and mark the
|
|
169
|
+
case resolved. Open GitHub bug issues with no linked regression case are recorded
|
|
170
|
+
as `coverage_missing` cases so missing regression coverage is visible in normal
|
|
171
|
+
run output instead of becoming a separate manual audit.
|
|
172
|
+
|
|
164
173
|
## Tooling Adapters
|
|
165
174
|
|
|
166
175
|
`testkit` also ships tool-specific config helpers so consumer repos do not need
|
|
@@ -271,6 +280,8 @@ export default defineConfig({
|
|
|
271
280
|
api: resource.postgres({
|
|
272
281
|
version: "16",
|
|
273
282
|
extensions: ["vector"],
|
|
283
|
+
maxConnections: 240,
|
|
284
|
+
runtimeConnections: 20,
|
|
274
285
|
}),
|
|
275
286
|
},
|
|
276
287
|
},
|
|
@@ -283,6 +294,7 @@ export default defineConfig({
|
|
|
283
294
|
port: 3004,
|
|
284
295
|
envFiles: [".env.testkit"],
|
|
285
296
|
database: database.postgres({
|
|
297
|
+
runtimeConnections: 20,
|
|
286
298
|
sourceSchema: database.schema.fromEnv("PRODUCTION_DATABASE_URL"),
|
|
287
299
|
template: {
|
|
288
300
|
inputs: ["db/schema.sql", "scripts/seed.ts"],
|
|
@@ -379,7 +391,9 @@ destroyed. `testkit cleanup --resources` cleans stale resources for the current
|
|
|
379
391
|
product; add `--global` to clean labelled resources whose product directory no
|
|
380
392
|
longer exists, and add `--include-legacy` only when you intentionally want to
|
|
381
393
|
remove old unlabelled `testkit_pg_*` containers from pre-ownership Testkit
|
|
382
|
-
versions.
|
|
394
|
+
versions. Host-local Postgres data lives in a Testkit-labelled named Docker
|
|
395
|
+
volume instead of an image-declared anonymous volume, so cleanup can account for
|
|
396
|
+
and remove it safely. Use `--dry-run` first when cleaning globally.
|
|
383
397
|
|
|
384
398
|
`database.template` is the database-side equivalent for reusable template DB
|
|
385
399
|
state. When `database.sourceSchema` is configured, Testkit treats the configured
|
|
@@ -485,7 +499,7 @@ If `regressions.file` is configured, `testkit` enriches
|
|
|
485
499
|
- per-file `diagnosis` metadata (new regression, known regression, fixed known regression)
|
|
486
500
|
- top-level `regressions` summary and prepared draft updates
|
|
487
501
|
|
|
488
|
-
Regression
|
|
502
|
+
Regression case authoring uses this contract:
|
|
489
503
|
|
|
490
504
|
- `summary`
|
|
491
505
|
- concise local statement of the regression slice
|
|
@@ -495,12 +509,12 @@ Regression-catalog entry authoring uses this contract:
|
|
|
495
509
|
- selectors that let testkit automatically recognize the regression in future runs
|
|
496
510
|
|
|
497
511
|
If `regressions.sync` is also configured, `testkit` syncs linked GitHub issues and
|
|
498
|
-
adds top-level regression
|
|
499
|
-
important
|
|
512
|
+
adds top-level regression case health to the run/status artifacts. The most
|
|
513
|
+
important stale-case signal is:
|
|
500
514
|
|
|
501
515
|
- a known regression still fails, but the linked GitHub issue is closed
|
|
502
516
|
|
|
503
|
-
In `mode: "error"`,
|
|
517
|
+
In `mode: "error"`, case-store health can also fail the run for problems such as:
|
|
504
518
|
|
|
505
519
|
- closed issues that still reproduce
|
|
506
520
|
- missing issue refs
|
|
@@ -607,6 +621,11 @@ export default suite;
|
|
|
607
621
|
|
|
608
622
|
DAL files for the same service database are serialized by default to avoid
|
|
609
623
|
exhausting Postgres connection limits during highly parallel runs.
|
|
624
|
+
Runtime services also reserve a configurable Postgres connection budget for each
|
|
625
|
+
active runtime slot. Use `database.postgres({ runtimeConnections })` for the
|
|
626
|
+
service pool size, and `resource.postgres({ maxConnections, reservedConnections })`
|
|
627
|
+
or `database.postgres({ maxConnections, reservedConnections })` for the backing
|
|
628
|
+
Postgres capacity.
|
|
610
629
|
|
|
611
630
|
`defineDalFixtures(...)` is the package-owned DAL seeding model. It gives every
|
|
612
631
|
suite a deterministic `fixtureScope` with:
|
|
@@ -803,6 +822,7 @@ npm run test:unit
|
|
|
803
822
|
npm run test:integration
|
|
804
823
|
npm run test:system
|
|
805
824
|
npm run test:live:github
|
|
825
|
+
npm run test:live:regression-workflow
|
|
806
826
|
npm run test:live:neon
|
|
807
827
|
npm run test:database-version:compat
|
|
808
828
|
```
|
|
@@ -118,7 +118,7 @@ export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel
|
|
|
118
118
|
const issues = [
|
|
119
119
|
rowValue("New regressions") ? `${rowValue("New regressions")} new regression${rowValue("New regressions") === "1" ? "" : "s"}` : null,
|
|
120
120
|
rowValue("Known regressions") ? `${rowValue("Known regressions")} known` : null,
|
|
121
|
-
rowValue("
|
|
121
|
+
rowValue("Case store stale") ? `${rowValue("Case store stale")} stale` : null,
|
|
122
122
|
].filter(Boolean);
|
|
123
123
|
|
|
124
124
|
return {
|
|
@@ -120,7 +120,7 @@ function renderTreeLine(snapshot, spinnerFrame, terminalWidth, entry) {
|
|
|
120
120
|
|
|
121
121
|
if (entry.kind === "file" && !entry.collapsed && snapshot.finished) {
|
|
122
122
|
const detailLines = entry.status === "failed"
|
|
123
|
-
? renderFailureDetail(entry, { width: terminalWidth,
|
|
123
|
+
? renderFailureDetail(entry, { width: terminalWidth, regressionCaseStore: snapshot.regressionCaseStore })
|
|
124
124
|
: entry.status === "passed"
|
|
125
125
|
? renderPassedDetail(entry, { width: terminalWidth })
|
|
126
126
|
: [];
|
|
@@ -6,9 +6,10 @@ export function createRunEventsReporter({ stdout = process.stdout, stderr = proc
|
|
|
6
6
|
return {
|
|
7
7
|
outputMode: "events",
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
writeEvent("run.
|
|
11
|
-
configured: Boolean(document
|
|
9
|
+
setRegressionCaseStore(document) {
|
|
10
|
+
writeEvent("run.regression_case_store", {
|
|
11
|
+
configured: Boolean(document),
|
|
12
|
+
caseCount: document?.cases?.length || 0,
|
|
12
13
|
});
|
|
13
14
|
},
|
|
14
15
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildFailurePresentation } from "../../../runner/formatting.mjs";
|
|
2
2
|
import { renderIndentedBlock } from "../../terminal/layout.mjs";
|
|
3
3
|
|
|
4
|
-
export function renderFailureBlock(task, outcome, { width,
|
|
4
|
+
export function renderFailureBlock(task, outcome, { width, regressionCaseStore } = {}) {
|
|
5
5
|
const failureView = buildFailurePresentation(
|
|
6
6
|
{
|
|
7
7
|
service: task.serviceName,
|
|
@@ -11,7 +11,7 @@ export function renderFailureBlock(task, outcome, { width, regressionCatalog } =
|
|
|
11
11
|
failureDetails: Array.isArray(outcome.failureDetails) ? outcome.failureDetails : [],
|
|
12
12
|
suiteError: null,
|
|
13
13
|
},
|
|
14
|
-
|
|
14
|
+
regressionCaseStore
|
|
15
15
|
);
|
|
16
16
|
|
|
17
17
|
const lines = [];
|
|
@@ -3,7 +3,7 @@ import { renderIndentedBlock } from "../../terminal/layout.mjs";
|
|
|
3
3
|
import { dim, green, red } from "../../terminal/colors.mjs";
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
|
|
6
|
-
export function renderFailureDetail(entry, { width,
|
|
6
|
+
export function renderFailureDetail(entry, { width, regressionCaseStore } = {}) {
|
|
7
7
|
const fileSummary = {
|
|
8
8
|
service: entry.serviceName,
|
|
9
9
|
type: normalizeType(entry),
|
|
@@ -13,7 +13,7 @@ export function renderFailureDetail(entry, { width, regressionCatalog } = {}) {
|
|
|
13
13
|
suiteError: null,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const failureView = buildFailurePresentation(fileSummary,
|
|
16
|
+
const failureView = buildFailurePresentation(fileSummary, regressionCaseStore);
|
|
17
17
|
const lines = [];
|
|
18
18
|
const indent = " ".repeat(2 + (entry.depth || 0) * 2 + 2);
|
|
19
19
|
|
|
@@ -32,8 +32,8 @@ export function createRunSession({
|
|
|
32
32
|
runState.setTotalFileCount(count);
|
|
33
33
|
},
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
runState.
|
|
35
|
+
setRegressionCaseStore(document) {
|
|
36
|
+
runState.setRegressionCaseStore(document);
|
|
37
37
|
},
|
|
38
38
|
|
|
39
39
|
serviceSkipped(config, reason) {
|
|
@@ -13,15 +13,15 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
13
13
|
const mode = outputMode || "compact";
|
|
14
14
|
let completedCount = 0;
|
|
15
15
|
let totalFileCount = 0;
|
|
16
|
-
let
|
|
16
|
+
let regressionCaseStore = null;
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
19
|
outputMode: mode,
|
|
20
20
|
setTotalFileCount(count) {
|
|
21
21
|
totalFileCount = count;
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
setRegressionCaseStore(document) {
|
|
24
|
+
regressionCaseStore = document;
|
|
25
25
|
},
|
|
26
26
|
writeLine(line = "") {
|
|
27
27
|
stdout.write(`${line}\n`);
|
|
@@ -92,7 +92,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
92
92
|
if (status === "FAIL") {
|
|
93
93
|
const detailLines = renderFailureBlock(task, outcome, {
|
|
94
94
|
width: getTerminalWidth(stdout, 100),
|
|
95
|
-
|
|
95
|
+
regressionCaseStore,
|
|
96
96
|
});
|
|
97
97
|
for (const line of detailLines) {
|
|
98
98
|
stdout.write(`${line}\n`);
|
|
@@ -138,13 +138,13 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
138
138
|
if (summary.fixedKnownRegressions > 0) {
|
|
139
139
|
rows.push(["Fixed known", String(summary.fixedKnownRegressions)]);
|
|
140
140
|
}
|
|
141
|
-
if (summary.
|
|
142
|
-
rows.push(["
|
|
141
|
+
if (summary.staleCases > 0) {
|
|
142
|
+
rows.push(["Case store stale", String(summary.staleCases)]);
|
|
143
143
|
}
|
|
144
|
-
if (summary.
|
|
145
|
-
rows.push(["
|
|
144
|
+
if (summary.syncUnavailable) {
|
|
145
|
+
rows.push(["Case store sync", "Unavailable"]);
|
|
146
146
|
} else if (summary.usedStaleCache) {
|
|
147
|
-
rows.push(["
|
|
147
|
+
rows.push(["Case store sync", "Used stale cache"]);
|
|
148
148
|
}
|
|
149
149
|
const boxed = renderSummaryBox(rows, { stdout });
|
|
150
150
|
stdout.write("\n");
|
|
@@ -32,8 +32,8 @@ export function buildSummaryRows({
|
|
|
32
32
|
if ((regressionSummary?.fixedKnownRegressions || 0) > 0) {
|
|
33
33
|
rows.push(["Fixed known", String(regressionSummary.fixedKnownRegressions)]);
|
|
34
34
|
}
|
|
35
|
-
if ((regressionSummary?.
|
|
36
|
-
rows.push(["
|
|
35
|
+
if ((regressionSummary?.staleCases || 0) > 0) {
|
|
36
|
+
rows.push(["Case store stale", String(regressionSummary.staleCases)]);
|
|
37
37
|
}
|
|
38
38
|
return rows;
|
|
39
39
|
}
|
|
@@ -44,7 +44,7 @@ export function createEmptyRunModel(dataSource = "live", options = {}) {
|
|
|
44
44
|
autoCollapsePassedTreeBranches: options.autoCollapsePassedTreeBranches !== false,
|
|
45
45
|
services: new Map(),
|
|
46
46
|
summaryData: null,
|
|
47
|
-
|
|
47
|
+
regressionCaseStore: null,
|
|
48
48
|
runArtifact: null,
|
|
49
49
|
finished: dataSource === "artifact",
|
|
50
50
|
totalCount: 0,
|
|
@@ -59,7 +59,7 @@ export function resetRunModel(model, dataSource = model.dataSource) {
|
|
|
59
59
|
model.dataSource = dataSource;
|
|
60
60
|
model.services = new Map();
|
|
61
61
|
model.summaryData = null;
|
|
62
|
-
model.
|
|
62
|
+
model.regressionCaseStore = null;
|
|
63
63
|
model.runArtifact = null;
|
|
64
64
|
model.finished = dataSource === "artifact";
|
|
65
65
|
model.totalCount = 0;
|
|
@@ -250,8 +250,8 @@ export function setPhase(model, label) {
|
|
|
250
250
|
model.phase = label;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
export function
|
|
254
|
-
model.
|
|
253
|
+
export function setRegressionCaseStore(model, document) {
|
|
254
|
+
model.regressionCaseStore = document;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
export function finishModel(model, results, durationMs, regressionReport) {
|
|
@@ -316,7 +316,7 @@ export function buildSnapshot(model) {
|
|
|
316
316
|
phase: model.phase,
|
|
317
317
|
finished: model.finished,
|
|
318
318
|
summaryData: model.summaryData,
|
|
319
|
-
|
|
319
|
+
regressionCaseStore: model.regressionCaseStore,
|
|
320
320
|
runArtifact: model.runArtifact,
|
|
321
321
|
};
|
|
322
322
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
resetRunModel,
|
|
15
15
|
revealEntry,
|
|
16
16
|
setPhase,
|
|
17
|
-
|
|
17
|
+
setRegressionCaseStore,
|
|
18
18
|
setTotalFileCount,
|
|
19
19
|
toggleCollapsed,
|
|
20
20
|
} from "./model.mjs";
|
|
@@ -66,8 +66,8 @@ export function createRunState({ dataSource = "live", autoCollapsePassedTreeBran
|
|
|
66
66
|
notify();
|
|
67
67
|
},
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
setRegressionCaseStore(document) {
|
|
70
|
+
setRegressionCaseStore(model, document);
|
|
71
71
|
notify();
|
|
72
72
|
},
|
|
73
73
|
|
|
@@ -82,7 +82,7 @@ export function colorResultLine(line) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export function colorSectionLine(line) {
|
|
85
|
-
if (line === "
|
|
85
|
+
if (line === "Case store issues:" || line === "Diagnosis:") {
|
|
86
86
|
return pc.bold(line);
|
|
87
87
|
}
|
|
88
88
|
if (/^Summary: /.test(line)) return line.replace("Summary:", `${pc.bold("Summary:")}`);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const DEFAULT_LOCAL_IMAGE = "pgvector/pgvector:pg16";
|
|
2
2
|
const DEFAULT_LOCAL_USER = "testkit";
|
|
3
3
|
const DEFAULT_LOCAL_PASSWORD = "testkit";
|
|
4
|
+
const DEFAULT_LOCAL_MAX_CONNECTIONS = 200;
|
|
5
|
+
const DEFAULT_LOCAL_RESERVED_CONNECTIONS = 20;
|
|
6
|
+
const DEFAULT_RUNTIME_CONNECTIONS = 20;
|
|
4
7
|
|
|
5
8
|
export function buildEnvironmentDatabaseMaterialization(environment = {}) {
|
|
6
9
|
const databases = environment.resources?.databases || {};
|
|
@@ -8,6 +11,16 @@ export function buildEnvironmentDatabaseMaterialization(environment = {}) {
|
|
|
8
11
|
resourcesByService: Object.fromEntries(
|
|
9
12
|
Object.keys(databases).map((serviceName) => [serviceName, serviceName])
|
|
10
13
|
),
|
|
14
|
+
resourceCapacityByName: Object.fromEntries(
|
|
15
|
+
Object.entries(databases).map(([resourceName, resource]) => [
|
|
16
|
+
resourceName,
|
|
17
|
+
{
|
|
18
|
+
...(resource.maxConnections ? { maxConnections: Number(resource.maxConnections) } : {}),
|
|
19
|
+
...(resource.reservedConnections != null ? { reservedConnections: Number(resource.reservedConnections) } : {}),
|
|
20
|
+
...(resource.runtimeConnections != null ? { runtimeConnections: Number(resource.runtimeConnections) } : {}),
|
|
21
|
+
},
|
|
22
|
+
])
|
|
23
|
+
),
|
|
11
24
|
};
|
|
12
25
|
}
|
|
13
26
|
|
|
@@ -24,6 +37,15 @@ export function materializeDatabaseConfig(database, serviceName, materialization
|
|
|
24
37
|
provider: "resource",
|
|
25
38
|
selectedBackend: "resource",
|
|
26
39
|
resource: resourceName,
|
|
40
|
+
maxConnections: database.maxConnections || materialization.resourceCapacityByName?.[resourceName]?.maxConnections || DEFAULT_LOCAL_MAX_CONNECTIONS,
|
|
41
|
+
reservedConnections:
|
|
42
|
+
database.reservedConnections ??
|
|
43
|
+
materialization.resourceCapacityByName?.[resourceName]?.reservedConnections ??
|
|
44
|
+
DEFAULT_LOCAL_RESERVED_CONNECTIONS,
|
|
45
|
+
runtimeConnections:
|
|
46
|
+
database.runtimeConnections ??
|
|
47
|
+
materialization.resourceCapacityByName?.[resourceName]?.runtimeConnections ??
|
|
48
|
+
DEFAULT_RUNTIME_CONNECTIONS,
|
|
27
49
|
};
|
|
28
50
|
}
|
|
29
51
|
|
|
@@ -34,6 +56,9 @@ export function materializeDatabaseConfig(database, serviceName, materialization
|
|
|
34
56
|
image: database.image || DEFAULT_LOCAL_IMAGE,
|
|
35
57
|
user: database.user || DEFAULT_LOCAL_USER,
|
|
36
58
|
password: database.password || DEFAULT_LOCAL_PASSWORD,
|
|
59
|
+
maxConnections: database.maxConnections || DEFAULT_LOCAL_MAX_CONNECTIONS,
|
|
60
|
+
reservedConnections: database.reservedConnections ?? DEFAULT_LOCAL_RESERVED_CONNECTIONS,
|
|
61
|
+
runtimeConnections: database.runtimeConnections ?? DEFAULT_RUNTIME_CONNECTIONS,
|
|
37
62
|
};
|
|
38
63
|
}
|
|
39
64
|
|
package/lib/config/database.mjs
CHANGED
|
@@ -23,6 +23,18 @@ export function normalizeDatabaseConfig(explicitService, serviceName) {
|
|
|
23
23
|
return {
|
|
24
24
|
...rawDatabase,
|
|
25
25
|
binding: normalizeDatabaseBinding(rawDatabase.binding || "per-runtime", `Service "${serviceName}" database.binding`),
|
|
26
|
+
maxConnections: normalizeOptionalPositiveInteger(
|
|
27
|
+
rawDatabase.maxConnections,
|
|
28
|
+
`Service "${serviceName}" database.maxConnections`
|
|
29
|
+
),
|
|
30
|
+
reservedConnections: normalizeOptionalNonNegativeInteger(
|
|
31
|
+
rawDatabase.reservedConnections,
|
|
32
|
+
`Service "${serviceName}" database.reservedConnections`
|
|
33
|
+
),
|
|
34
|
+
runtimeConnections: normalizeOptionalPositiveInteger(
|
|
35
|
+
rawDatabase.runtimeConnections,
|
|
36
|
+
`Service "${serviceName}" database.runtimeConnections`
|
|
37
|
+
),
|
|
26
38
|
reset: rawDatabase.reset !== false,
|
|
27
39
|
sourceSchema: normalizeSourceSchemaConfig(rawDatabase.sourceSchema, serviceName),
|
|
28
40
|
template: normalizeDatabaseTemplateConfig(rawDatabase.template, serviceName),
|
|
@@ -30,6 +42,24 @@ export function normalizeDatabaseConfig(explicitService, serviceName) {
|
|
|
30
42
|
};
|
|
31
43
|
}
|
|
32
44
|
|
|
45
|
+
function normalizeOptionalPositiveInteger(value, label) {
|
|
46
|
+
if (value == null) return undefined;
|
|
47
|
+
const number = Number(value);
|
|
48
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
49
|
+
throw new Error(`${label} must be a positive integer`);
|
|
50
|
+
}
|
|
51
|
+
return number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeOptionalNonNegativeInteger(value, label) {
|
|
55
|
+
if (value == null) return undefined;
|
|
56
|
+
const number = Number(value);
|
|
57
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
58
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
59
|
+
}
|
|
60
|
+
return number;
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
export function normalizeDatabaseTemplateConfig(value, serviceName) {
|
|
34
64
|
if (value == null) {
|
|
35
65
|
return {
|
package/lib/config/index.mjs
CHANGED
|
@@ -278,11 +278,57 @@ function normalizeEnvironmentResourceGroup(environmentName, groupName, group = {
|
|
|
278
278
|
`Environment "${environmentName}" resources.${groupName}.${name} kind must be ${allowedKinds.map((entry) => `"${entry}"`).join(" or ")}`
|
|
279
279
|
);
|
|
280
280
|
}
|
|
281
|
-
return [
|
|
281
|
+
return [
|
|
282
|
+
normalizedName,
|
|
283
|
+
{
|
|
284
|
+
...resource,
|
|
285
|
+
kind,
|
|
286
|
+
...(resource.maxConnections != null
|
|
287
|
+
? {
|
|
288
|
+
maxConnections: normalizePositiveInteger(
|
|
289
|
+
resource.maxConnections,
|
|
290
|
+
`Environment "${environmentName}" resources.${groupName}.${name}.maxConnections`
|
|
291
|
+
),
|
|
292
|
+
}
|
|
293
|
+
: {}),
|
|
294
|
+
...(resource.reservedConnections != null
|
|
295
|
+
? {
|
|
296
|
+
reservedConnections: normalizeNonNegativeInteger(
|
|
297
|
+
resource.reservedConnections,
|
|
298
|
+
`Environment "${environmentName}" resources.${groupName}.${name}.reservedConnections`
|
|
299
|
+
),
|
|
300
|
+
}
|
|
301
|
+
: {}),
|
|
302
|
+
...(resource.runtimeConnections != null
|
|
303
|
+
? {
|
|
304
|
+
runtimeConnections: normalizePositiveInteger(
|
|
305
|
+
resource.runtimeConnections,
|
|
306
|
+
`Environment "${environmentName}" resources.${groupName}.${name}.runtimeConnections`
|
|
307
|
+
),
|
|
308
|
+
}
|
|
309
|
+
: {}),
|
|
310
|
+
},
|
|
311
|
+
];
|
|
282
312
|
})
|
|
283
313
|
);
|
|
284
314
|
}
|
|
285
315
|
|
|
316
|
+
function normalizePositiveInteger(value, label) {
|
|
317
|
+
const number = Number(value);
|
|
318
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
319
|
+
throw new Error(`${label} must be a positive integer`);
|
|
320
|
+
}
|
|
321
|
+
return number;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeNonNegativeInteger(value, label) {
|
|
325
|
+
const number = Number(value);
|
|
326
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
327
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
328
|
+
}
|
|
329
|
+
return number;
|
|
330
|
+
}
|
|
331
|
+
|
|
286
332
|
function normalizeKilnEnvironmentConfig(name, kiln) {
|
|
287
333
|
if (!kiln) return null;
|
|
288
334
|
if (typeof kiln !== "object" || Array.isArray(kiln)) {
|
package/lib/config/runtime.mjs
CHANGED
|
@@ -32,9 +32,139 @@ export function normalizeRegressionsConfig(value) {
|
|
|
32
32
|
return {
|
|
33
33
|
file,
|
|
34
34
|
sync,
|
|
35
|
+
workflow: normalizeRegressionWorkflowConfig(value.workflow),
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function normalizeRegressionWorkflowConfig(value) {
|
|
40
|
+
const defaults = {
|
|
41
|
+
mode: "autopilot",
|
|
42
|
+
github: {
|
|
43
|
+
read: true,
|
|
44
|
+
write: false,
|
|
45
|
+
bugLabels: ["bug"],
|
|
46
|
+
issueLabels: ["bug"],
|
|
47
|
+
createIssues: "confirmed",
|
|
48
|
+
reopenIssues: "closed_reproduced",
|
|
49
|
+
closeIssues: "after_stability_window",
|
|
50
|
+
comment: "state_changes",
|
|
51
|
+
},
|
|
52
|
+
evidence: {
|
|
53
|
+
autoRerunThinFailures: true,
|
|
54
|
+
maxAutoReruns: 2,
|
|
55
|
+
requireForIssueCreation: true,
|
|
56
|
+
},
|
|
57
|
+
stability: {
|
|
58
|
+
cleanRunsBeforeClose: 3,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
if (value == null) return defaults;
|
|
62
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
63
|
+
throw new Error("testkit.config.ts regressions.workflow must be an object");
|
|
64
|
+
}
|
|
65
|
+
const mode = normalizeOptionalString(value.mode) || defaults.mode;
|
|
66
|
+
if (!["off", "observe", "autopilot"].includes(mode)) {
|
|
67
|
+
throw new Error('testkit.config.ts regressions.workflow.mode must be one of: off, observe, autopilot');
|
|
68
|
+
}
|
|
69
|
+
const github = normalizeObject(value.github, "testkit.config.ts regressions.workflow.github");
|
|
70
|
+
const evidence = normalizeObject(value.evidence, "testkit.config.ts regressions.workflow.evidence");
|
|
71
|
+
const stability = normalizeObject(value.stability, "testkit.config.ts regressions.workflow.stability");
|
|
72
|
+
return {
|
|
73
|
+
mode,
|
|
74
|
+
github: {
|
|
75
|
+
...defaults.github,
|
|
76
|
+
...normalizeRegressionWorkflowGithub(github),
|
|
77
|
+
},
|
|
78
|
+
evidence: {
|
|
79
|
+
...defaults.evidence,
|
|
80
|
+
...normalizeRegressionWorkflowEvidence(evidence),
|
|
81
|
+
},
|
|
82
|
+
stability: {
|
|
83
|
+
...defaults.stability,
|
|
84
|
+
...normalizeRegressionWorkflowStability(stability),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeRegressionWorkflowGithub(value) {
|
|
90
|
+
if (!value) return {};
|
|
91
|
+
return {
|
|
92
|
+
...(value.read == null ? {} : { read: normalizeBoolean(value.read, "testkit.config.ts regressions.workflow.github.read") }),
|
|
93
|
+
...(value.write == null ? {} : { write: normalizeBoolean(value.write, "testkit.config.ts regressions.workflow.github.write") }),
|
|
94
|
+
...(value.apiBaseUrl == null ? {} : { apiBaseUrl: requireNonEmptyString(value.apiBaseUrl, "testkit.config.ts regressions.workflow.github.apiBaseUrl") }),
|
|
95
|
+
...(value.bugLabels == null ? {} : { bugLabels: normalizeStringArray(value.bugLabels, "testkit.config.ts regressions.workflow.github.bugLabels") }),
|
|
96
|
+
...(value.issueLabels == null ? {} : { issueLabels: normalizeStringArray(value.issueLabels, "testkit.config.ts regressions.workflow.github.issueLabels") }),
|
|
97
|
+
...(value.createIssues == null ? {} : { createIssues: normalizeEnum(value.createIssues, ["never", "confirmed"], "testkit.config.ts regressions.workflow.github.createIssues") }),
|
|
98
|
+
...(value.reopenIssues == null ? {} : { reopenIssues: normalizeEnum(value.reopenIssues, ["never", "closed_reproduced"], "testkit.config.ts regressions.workflow.github.reopenIssues") }),
|
|
99
|
+
...(value.closeIssues == null ? {} : { closeIssues: normalizeEnum(value.closeIssues, ["never", "after_stability_window"], "testkit.config.ts regressions.workflow.github.closeIssues") }),
|
|
100
|
+
...(value.comment == null ? {} : { comment: normalizeEnum(value.comment, ["never", "state_changes"], "testkit.config.ts regressions.workflow.github.comment") }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeRegressionWorkflowEvidence(value) {
|
|
105
|
+
if (!value) return {};
|
|
106
|
+
return {
|
|
107
|
+
...(value.autoRerunThinFailures == null ? {} : { autoRerunThinFailures: normalizeBoolean(value.autoRerunThinFailures, "testkit.config.ts regressions.workflow.evidence.autoRerunThinFailures") }),
|
|
108
|
+
...(value.maxAutoReruns == null ? {} : { maxAutoReruns: normalizeNonNegativeInteger(value.maxAutoReruns, "testkit.config.ts regressions.workflow.evidence.maxAutoReruns") }),
|
|
109
|
+
...(value.requireForIssueCreation == null ? {} : { requireForIssueCreation: normalizeBoolean(value.requireForIssueCreation, "testkit.config.ts regressions.workflow.evidence.requireForIssueCreation") }),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeRegressionWorkflowStability(value) {
|
|
114
|
+
if (!value) return {};
|
|
115
|
+
return {
|
|
116
|
+
...(value.cleanRunsBeforeClose == null ? {} : { cleanRunsBeforeClose: normalizePositiveInteger(value.cleanRunsBeforeClose, "testkit.config.ts regressions.workflow.stability.cleanRunsBeforeClose") }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeObject(value, label) {
|
|
121
|
+
if (value == null) return null;
|
|
122
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
123
|
+
throw new Error(`${label} must be an object`);
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeBoolean(value, label) {
|
|
129
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeEnum(value, allowed, label) {
|
|
134
|
+
const normalized = requireNonEmptyString(value, label);
|
|
135
|
+
if (!allowed.includes(normalized)) {
|
|
136
|
+
throw new Error(`${label} must be one of: ${allowed.join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeStringArray(value, label) {
|
|
142
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
143
|
+
throw new Error(`${label} must be a non-empty array`);
|
|
144
|
+
}
|
|
145
|
+
return value.map((entry, index) => requireNonEmptyString(entry, `${label}[${index}]`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeNonNegativeInteger(value, label) {
|
|
149
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
150
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizePositiveInteger(value, label) {
|
|
156
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
157
|
+
throw new Error(`${label} must be a positive integer`);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function requireNonEmptyString(value, label) {
|
|
163
|
+
const normalized = normalizeOptionalString(value);
|
|
164
|
+
if (!normalized) throw new Error(`${label} must be a non-empty string`);
|
|
165
|
+
return normalized;
|
|
166
|
+
}
|
|
167
|
+
|
|
38
168
|
export function inferLocalRuntime(productDir, cwd) {
|
|
39
169
|
const absoluteCwd = resolveServiceCwd(productDir, cwd);
|
|
40
170
|
if (!fs.existsSync(absoluteCwd)) return undefined;
|
|
@@ -88,8 +88,11 @@ export type BuildConfig = TscBuildConfig | ScriptBuildConfig | NextBuildConfig |
|
|
|
88
88
|
export interface PostgresDatabaseConfig {
|
|
89
89
|
binding?: "shared" | "per-runtime";
|
|
90
90
|
image?: string;
|
|
91
|
+
maxConnections?: number;
|
|
91
92
|
password?: string;
|
|
93
|
+
reservedConnections?: number;
|
|
92
94
|
reset?: boolean;
|
|
95
|
+
runtimeConnections?: number;
|
|
93
96
|
sourceSchema?: DatabaseSourceSchemaConfig | null;
|
|
94
97
|
template?: DatabaseTemplateConfig;
|
|
95
98
|
user?: string;
|
|
@@ -309,8 +312,11 @@ export interface PostgresResourceConfig {
|
|
|
309
312
|
database?: string;
|
|
310
313
|
extensions?: string[];
|
|
311
314
|
image?: string;
|
|
315
|
+
maxConnections?: number;
|
|
312
316
|
password?: string;
|
|
313
317
|
port?: number;
|
|
318
|
+
reservedConnections?: number;
|
|
319
|
+
runtimeConnections?: number;
|
|
314
320
|
user?: string;
|
|
315
321
|
version?: string;
|
|
316
322
|
vm?: KilnVMResourceConfig;
|
|
@@ -580,6 +586,28 @@ export interface TestkitConfig {
|
|
|
580
586
|
regressions?: {
|
|
581
587
|
file?: string;
|
|
582
588
|
sync?: RegressionSyncConfig;
|
|
589
|
+
workflow?: {
|
|
590
|
+
mode?: "off" | "observe" | "autopilot";
|
|
591
|
+
github?: {
|
|
592
|
+
read?: boolean;
|
|
593
|
+
write?: boolean;
|
|
594
|
+
apiBaseUrl?: string;
|
|
595
|
+
bugLabels?: string[];
|
|
596
|
+
issueLabels?: string[];
|
|
597
|
+
createIssues?: "never" | "confirmed";
|
|
598
|
+
reopenIssues?: "never" | "closed_reproduced";
|
|
599
|
+
closeIssues?: "never" | "after_stability_window";
|
|
600
|
+
comment?: "never" | "state_changes";
|
|
601
|
+
};
|
|
602
|
+
evidence?: {
|
|
603
|
+
autoRerunThinFailures?: boolean;
|
|
604
|
+
maxAutoReruns?: number;
|
|
605
|
+
requireForIssueCreation?: boolean;
|
|
606
|
+
};
|
|
607
|
+
stability?: {
|
|
608
|
+
cleanRunsBeforeClose?: number;
|
|
609
|
+
};
|
|
610
|
+
};
|
|
583
611
|
};
|
|
584
612
|
services?: Record<string, ServiceConfig>;
|
|
585
613
|
toolchains?: Record<string, ToolchainConfig>;
|