@gfdelarue/testkit 0.3.4 → 0.3.6
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 +11 -73
- package/dist/app/doctor/index.js +1 -1
- package/dist/app/inspect/artifacts.d.ts +1 -0
- package/dist/app/inspect/artifacts.js +1 -1
- package/dist/app/inspect/index.js +1 -1
- package/dist/app/lint/index.js +1 -1
- package/dist/app/lint/native.js +1 -1
- package/dist/app/lint/policy.js +1 -1
- package/dist/app/lint/rules/data.js +1 -1
- package/dist/app/lint/rules/engine-imports.js +1 -1
- package/dist/app/lint/rules/syntax.js +1 -1
- package/dist/app/lint/rules/unit.js +1 -1
- package/dist/app/lint/shared.d.ts +1 -1
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/request.js +1 -1
- package/dist/app/setup/playwright.js +1 -1
- package/dist/app/status/index.d.ts +2 -0
- package/dist/app/status/index.js +1 -1
- package/dist/app/status/runs.d.ts +2 -2
- package/dist/app/status/runs.js +1 -1
- package/dist/app/stop/index.js +1 -1
- package/dist/app/typecheck/index.d.ts +2 -0
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/commands/artifacts.js +2 -2
- package/dist/cli/commands/db/schema/refresh.js +1 -1
- package/dist/cli/commands/db/schema/verify.js +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/operations/db/schema/refresh/operation.d.ts +1 -0
- package/dist/cli/operations/db/schema/refresh/operation.js +1 -1
- package/dist/cli/operations/db/schema/verify/operation.d.ts +1 -0
- package/dist/cli/operations/db/schema/verify/operation.js +1 -1
- package/dist/cli/operations/run/operation.js +1 -1
- package/dist/config/loader/discover-tests.d.ts +5 -0
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery.js +2 -2
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/index.d.ts +2 -1
- package/dist/config/loader/index.js +1 -1
- package/dist/config/loader/load.d.ts +2 -0
- package/dist/config/loader/load.js +1 -1
- package/dist/config/loader/managed-configs.d.ts +2 -0
- package/dist/config/loader/managed-configs.js +1 -1
- package/dist/config/loader/paths.d.ts +1 -1
- package/dist/config/loader/paths.js +1 -1
- package/dist/config/loader/skip-config.d.ts +1 -1
- package/dist/config/loader/test-selection.d.ts +15 -0
- package/dist/config/loader/test-selection.js +1 -0
- package/dist/database/index.d.ts +1 -3
- package/dist/database/index.js +1 -1
- package/dist/database/lifecycle-cleanup.d.ts +7 -0
- package/dist/database/lifecycle-cleanup.js +1 -0
- package/dist/database/local-postgres.js +1 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/engines/adapters/evidence-files.js +1 -1
- package/dist/engines/catalog.d.ts +3 -8
- package/dist/engines/catalog.js +1 -1
- package/dist/engines/definitions.d.ts +8 -1
- package/dist/engines/definitions.js +1 -1
- package/dist/engines/generated-performance.d.ts +1 -0
- package/dist/engines/generated-performance.js +12 -0
- package/dist/engines/installations/node-vitest.d.ts +8 -22
- package/dist/engines/installations/node-vitest.js +1 -1
- package/dist/engines/installations/package-requirements.d.ts +28 -0
- package/dist/engines/installations/package-requirements.js +1 -0
- package/dist/engines/installations/playwright.d.ts +1 -11
- package/dist/engines/installations/playwright.js +1 -1
- package/dist/engines/k6/adapter/repository-configuration.d.ts +1 -1
- package/dist/engines/k6/adapter/repository-configuration.js +1 -1
- package/dist/engines/managed-module-resolution.js +111 -21
- package/dist/engines/manager.d.ts +1 -0
- package/dist/engines/manager.js +1 -1
- package/dist/engines/node-vitest/adapter/adapter.d.ts +1 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/consumer-typescript-paths.js +1 -1
- package/dist/engines/node-vitest/adapter/resolution.d.ts +1 -1
- package/dist/engines/node-vitest/adapter/resolution.js +1 -1
- package/dist/engines/node-vitest/adapter/workspace.js +33 -9
- package/dist/engines/node-vitest/runtime/runtime-input.d.ts +1 -0
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/runtime-input-file.d.ts +3 -1
- package/dist/engines/runtime-input-file.js +1 -1
- package/dist/environment/lifecycle-controller.d.ts +1 -2
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/maintenance.d.ts +1 -1
- package/dist/environment/maintenance.js +1 -1
- package/dist/environment/ready-environment.d.ts +1 -1
- package/dist/environment/ready-environment.js +1 -1
- package/dist/environment/runtime-contexts.d.ts +1 -1
- package/dist/environment/runtime-contexts.js +1 -1
- package/dist/environment/runtime-preparation.js +1 -1
- package/dist/environment/services.js +1 -1
- package/dist/environment/template.d.ts +1 -1
- package/dist/environment/template.js +1 -1
- package/dist/evidence/bundles.js +1 -1
- package/dist/evidence/read-run.js +1 -1
- package/dist/evidence/reading.d.ts +1 -1
- package/dist/evidence/reading.js +1 -1
- package/dist/evidence/storage.js +2 -2
- package/dist/kernel/error-serialization.js +1 -1
- package/dist/kernel/work-metrics.d.ts +6 -0
- package/dist/kernel/work-metrics.js +1 -0
- package/dist/process/capture-output.js +1 -1
- package/dist/process/execa-bundle.json +1 -1
- package/dist/process/files/product-dir.d.ts +1 -0
- package/dist/process/files/product-dir.js +1 -0
- package/dist/process/locks.js +4 -4
- package/dist/process/readiness.js +1 -1
- package/dist/process/runs/inspection.d.ts +5 -0
- package/dist/process/runs/inspection.js +1 -0
- package/dist/process/runs/manifest.d.ts +1 -0
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/service-processes.d.ts +1 -1
- package/dist/process/service-processes.js +1 -1
- package/dist/toolchains/index.js +2 -2
- package/docs/cloud-control.md +3 -0
- package/docs/engine-architecture.md +4 -1
- package/docs/test-types.md +9 -0
- package/docs/testkit-cloud-boundary.md +12 -24
- package/package.json +2 -1
- package/testkit-config-schema.md +2 -0
- package/dist/cli/renderers/artifact-preview.d.ts +0 -4
- package/dist/cli/renderers/artifact-preview.js +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
`@gfdelarue/testkit` is the orchestrator and managed distribution for product test
|
|
8
8
|
suites. It discovers `*.testkit.ts` files, infers suite ownership from the
|
|
9
9
|
filesystem, starts local services, provisions Docker-managed local Postgres
|
|
10
|
-
databases, schedules suites, and collects results and artifacts.
|
|
10
|
+
databases, schedules suites, and collects results and artifacts. Coding agents
|
|
11
|
+
use it to write tests and hunt bugs, and CI uses it to run those tests. It needs
|
|
12
|
+
no account or server.
|
|
11
13
|
|
|
12
14
|
Testkit exists to replace false confidence with inspectable execution evidence.
|
|
13
15
|
It cannot prove that an entire application works. It can make narrower claims
|
|
@@ -47,33 +49,6 @@ Testkit writes the immutable, verifiable run bundle under
|
|
|
47
49
|
`.testkit/results/runs/<run-id>/`. Upload `.testkit/results` with `if: always()`
|
|
48
50
|
when CI evidence should survive a failed job.
|
|
49
51
|
|
|
50
|
-
Publishing that evidence to Testkit Cloud is an additional explicit step. Set
|
|
51
|
-
`TESTKIT_CLOUD_PROJECT_ID` and the secret `TESTKIT_CLOUD_TOKEN`, then run:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
npx testkit cloud publish
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The command verifies the immutable bundle before upload. Ordinary runs never
|
|
58
|
-
contact Cloud, and the presence of `CI` does not authorize publication.
|
|
59
|
-
|
|
60
|
-
## Connect to Cloud
|
|
61
|
-
|
|
62
|
-
For interactive use, approve your CLI through the browser once:
|
|
63
|
-
|
|
64
|
-
```sh
|
|
65
|
-
npx testkit cloud login
|
|
66
|
-
npx testkit cloud project create
|
|
67
|
-
npx testkit cloud publish --project PROJECT_ID --run RUN_ID
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Login works outside Git. Project creation infers the repository from Git origin
|
|
71
|
-
or accepts `--repository owner/repo`. It resumes after any required provider
|
|
72
|
-
consent. `cloud list`, `cloud show`, and `cloud apply --file` provide discovery,
|
|
73
|
-
settings and product control. See the [Cloud control boundary](docs/testkit-cloud-boundary.md#complete-cloud-control)
|
|
74
|
-
and [complete operation reference](https://github.com/gfdelarue/testkit-cloud/blob/master/docs/cloud-control.md).
|
|
75
|
-
Explicit `TESTKIT_CLOUD_TOKEN` still takes precedence over the saved account.
|
|
76
|
-
|
|
77
52
|
## Execution engines
|
|
78
53
|
|
|
79
54
|
Testkit has three explicit built-in engines rather than one implicit runtime:
|
|
@@ -219,40 +194,9 @@ Batch `run` output stays intentionally short: one line per completed file,
|
|
|
219
194
|
concise failure blocks, and a final summary. Service logs, captured runtime
|
|
220
195
|
output, and emitted artifacts are persisted under `.testkit/results/`.
|
|
221
196
|
|
|
222
|
-
Testkit
|
|
223
|
-
|
|
224
|
-
output
|
|
225
|
-
Publish that exact evidence with `testkit cloud publish --run <id>`. Omitting
|
|
226
|
-
`--run` selects the newest bundle for interactive use. Historical summaries
|
|
227
|
-
and trends belong to Testkit Cloud.
|
|
228
|
-
|
|
229
|
-
For CI, preserve the run exit status and publish the ID from that invocation:
|
|
230
|
-
|
|
231
|
-
```sh
|
|
232
|
-
run_exit=0
|
|
233
|
-
testkit run --json > testkit-run.json || run_exit=$?
|
|
234
|
-
run_id=$(node -e 'const r = JSON.parse(require("node:fs").readFileSync("testkit-run.json", "utf8")); if (typeof r.runId !== "string" || !r.runId) process.exit(1); process.stdout.write(r.runId)') || {
|
|
235
|
-
if [ "$run_exit" -ne 0 ]; then exit "$run_exit"; fi
|
|
236
|
-
exit 1
|
|
237
|
-
}
|
|
238
|
-
publish_exit=0
|
|
239
|
-
testkit cloud publish --run "$run_id" || publish_exit=$?
|
|
240
|
-
if [ "$publish_exit" -ne 0 ]; then
|
|
241
|
-
echo "Cloud publication failed with exit $publish_exit" >&2
|
|
242
|
-
fi
|
|
243
|
-
exit "$run_exit"
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Set `TESTKIT_CLOUD_PROJECT_ID` and `TESTKIT_CLOUD_TOKEN` in the CI environment.
|
|
247
|
-
`TESTKIT_CLOUD_URL`, or the `--url` flag on `cloud publish`, `cloud apply`, and
|
|
248
|
-
`cloud snapshot`, changes the Testkit Cloud origin. The bearer token and the
|
|
249
|
-
run bundle are sent only to that origin, so protect the variable like the
|
|
250
|
-
token itself. Leave it unset to use `https://testkit.cloud`.
|
|
251
|
-
A failed run can still publish a valid sealed bundle. Publication reports its
|
|
252
|
-
own exit status and does not change the test outcome. This recipe reports a
|
|
253
|
-
publication failure and retains the run status as the job status. See the
|
|
254
|
-
[Cloud boundary](docs/testkit-cloud-boundary.md) and
|
|
255
|
-
[breaking-change migration](https://github.com/gfdelarue/testkit/blob/master/docs/release-notes.md).
|
|
197
|
+
Testkit seals an immutable local run bundle. `testkit run` prints
|
|
198
|
+
`Run ID: <id>` after sealing in text output and returns `runId` in `--json`
|
|
199
|
+
output, for passed and failed runs.
|
|
256
200
|
|
|
257
201
|
Test execution also maintains a scheduler cache at `.testkit/timings.json`.
|
|
258
202
|
Completed file-level task durations are used to rank future runs with a
|
|
@@ -645,20 +589,14 @@ testkit artifacts --run <run-id> --verify --json
|
|
|
645
589
|
testkit artifacts --run <run-id> path/to/test.testkit.ts --json
|
|
646
590
|
```
|
|
647
591
|
|
|
592
|
+
The default artifact list shows names, kinds, summaries, and paths without
|
|
593
|
+
reading payload files. Select a file with `--format text` to read its contents,
|
|
594
|
+
or use `--json` for structured output including the selected payloads.
|
|
595
|
+
|
|
648
596
|
Programmatic consumers can import `listRunBundles`, `loadRunBundleArtifact`,
|
|
649
597
|
`loadRunBundleManifest`, and `verifyRunBundle` from
|
|
650
598
|
`@gfdelarue/testkit/workspace`.
|
|
651
599
|
|
|
652
|
-
The run bundle is also the boundary for Testkit Cloud. The local runner never
|
|
653
|
-
reads regression state or mutates GitHub. See
|
|
654
|
-
[`docs/testkit-cloud-boundary.md`](docs/testkit-cloud-boundary.md) for ownership,
|
|
655
|
-
ingestion, and migration rules.
|
|
656
|
-
|
|
657
|
-
Use `testkit cloud snapshot` to read versioned Cloud regression state. Apply
|
|
658
|
-
reviewed mutations from a file with `testkit cloud apply --file commands.json`;
|
|
659
|
-
the file must contain a schema version, idempotency key, expected snapshot
|
|
660
|
-
version, and commands.
|
|
661
|
-
|
|
662
600
|
## Authoring
|
|
663
601
|
|
|
664
602
|
### Node setup beside a test
|
|
@@ -1035,7 +973,7 @@ identifier plus canonical metadata such as:
|
|
|
1035
973
|
- `dependsOn`
|
|
1036
974
|
- `displayName`
|
|
1037
975
|
|
|
1038
|
-
|
|
976
|
+
Discovery output has no cross-run counts, last outcomes, durations, or trends.
|
|
1039
977
|
See the [status and history migration](https://github.com/gfdelarue/testkit/blob/master/docs/release-notes.md#local-status-and-history-removal)
|
|
1040
978
|
for the removed discovery fields and consumer migration.
|
|
1041
979
|
|
package/dist/app/doctor/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import m from"path";import{randomUUID as v}from"node:crypto";import{loadDiscoveryContext as k,loadConfigContext as w}from"../../config/loader/index.js";import{runLintForConfig as D}from"../lint/index.js";export async function runDoctor(e={}){const t=[],r=e.dir?m.resolve(process.cwd(),e.dir):process.cwd(),s=await w({dir:r,configImportCacheSalt:v()});t.push({code:"config-load",level:"pass",message:"Loaded testkit config and service config"});const l=await k({dir:r,diagnostics:"report"},s),u=l.discovery,f=b(u,"discovery result"),g=a(f.diagnostics,"discovery result diagnostics"),y=a(f.files,"discovery result files"),c=g.filter(S);t.push({code:"discovery",level:c.length===0?"pass":"fail",message:c.length===0?`Discovered ${String(y.length)} testkit files without diagnostics`:`Discovery reported ${String(c.length)} error diagnostics`,details:c});const o=await D(n(s,"config"),{dir:r});if(t.push({code:"lint",level:o.ok?"pass":"fail",message:o.ok?`No Testkit lint violations across ${String(o.summary.testkitFiles)} suite file(s)`:`Found ${String(o.summary.violations)} Testkit lint violation(s)`,details:o.violations}),e.typecheck!==!1)try{const{runTestkitTypecheckForContext:i}=await import("../typecheck/index.js"),p=await i(s,l.discovery,{dir:r}),h=a(n(p,"results"),"typecheck results");t.push({code:"typecheck",level:"pass",message:`Typechecked ${String(h.length)} generated program(s)`})}catch(i){t.push({code:"typecheck",level:"fail",message:$(i)})}return{ok:!t.some(i=>i.level==="fail"),productDir:r,checks:t}}function S(e){return d(e)&&Reflect.get(e,"severity")==="error"}function $(e){const t=n(e,"result"),r=n(t,"stderr");if(typeof r=="string"&&r.length>0)return r;const s=n(t,"stdout");return typeof s=="string"&&s.length>0?s:e instanceof Error?e.message:String(e)}function a(e,t){if(!Array.isArray(e))throw new Error(`${t} must be an array`);return e}function b(e,t){if(!d(e))throw new Error(`${t} must be an object`);return e}function n(e,t){return d(e)?Reflect.get(e,t):void 0}function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import d from"node:path";import{collectArtifactEntries as f,listRunBundles as l,loadRunArtifact as c,verifyRunBundle as a}from"../../evidence/index.js";export function inspectArtifacts(r){const e=d.resolve(r.dir||process.cwd());if(r.mode==="runs")return{mode:"runs",runs:l(e,{file:r.file})};const n=r.runId?null:c(e),t=r.runId||n?.run.id||null;if(r.mode==="verify")return{mode:"verify",runId:t,verification:a(e,t)};const i=n??c(e,t),u=f(e,i,r.file,r.service,r.kind,r.includePayload);return{mode:"artifacts",runId:t,entries:u}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{formatTestTypeLabel}from"../../domain/test-types.js";import{readRunDocument as
|
|
1
|
+
export{formatTestTypeLabel}from"../../domain/test-types.js";import{readRunDocument as R}from"../../evidence/read-run.js";import c from"fs";import i from"path";import{errorMessage as u}from"../../kernel/error-serialization.js";import{isPublicTestType as w}from"../../domain/identifiers.js";import{loadDiscoveryContext as I,hasDiscoveryErrors as k,buildDiscoveryErrorFromDiagnostics as D,formatDisplayName as S}from"../../config/loader/index.js";import{collectServiceStatus as b,collectConfiguredServiceStatus as A}from"../status/index.js";export{discoverTests,fileDisplayName,formatDisplayName,testTypeContract}from"../../config/loader/index.js";export{listRunBundles,loadRunBundleArtifact,loadRunBundleManifest,verifyRunBundle}from"../../evidence/index.js";const N=2,$=24,C=String;export async function inspectWorkspace(t={}){const e=_(t.targetLimit,$),s=new Date().toISOString(),r=await F(t),a=r.data?.product.directory||i.resolve(process.cwd(),t.dir||"."),[n,o,l]=await Promise.all([t.status===!1?j():M(t,r.context),d(a,"latest"),d(a,"live")]),y=p(o.data,"latest"),g=p(l.data,"live"),T=P({discovery:r.data,latestRunArtifact:o.data,liveRunArtifact:l.data,limit:e}),h=[...r.warnings,...n.warnings,...o.warnings,...l.warnings];return{schemaVersion:N,source:"testkit-workspace",generatedAt:s,product:r.data?.product||{directory:a,name:i.basename(a)},discovery:r.data?{configFile:r.data.configFile||null,diagnostics:r.data.diagnostics,summary:r.data.summary}:null,status:n.data,runs:{latest:y,live:g},targets:T,activity:O(n.data),warnings:h}}async function F(t){try{const{discovery:e,context:s}=await I({diagnostics:"report",dir:t.dir,runnableOnly:!1,service:t.service||null}),r=e.diagnostics.filter(a=>a.severity==="error").map(a=>`Discovery diagnostic: ${a.message}`);return{data:e,warnings:r,context:s}}catch(e){return{data:null,context:null,warnings:[`Discovery unavailable: ${u(e)}`]}}}async function M(t,e){try{if(e&&k(e.discovery.diagnostics))throw D(e.discovery.diagnostics);const s=e?A(e.configs,t.service||null):await b({dir:t.dir,service:t.service||null});return{data:x(s),warnings:[]}}catch(s){return{data:{ok:!1,results:[]},warnings:[`Status unavailable: ${u(s)}`]}}}function j(){return Promise.resolve({data:{ok:!0,results:[]},warnings:[]})}function d(t,e){const s=e==="live"?"live.json":"latest.json",r=i.join(t,".testkit","results",s);if(!c.existsSync(r))return Promise.resolve({data:null,warnings:[]});try{const a=R(JSON.parse(c.readFileSync(r,"utf8")),r);return Promise.resolve({data:a,warnings:[]})}catch(a){return Promise.resolve({data:null,warnings:[`${e} run artifact unavailable: ${u(a)}`]})}}function x(t){return{ok:!0,results:t.map(e=>({caches:e.caches,hasState:e.hasState,name:e.name,product:e.product,runtimeGraphs:e.runtimeGraphs.map(s=>({currentRuntimeDirCount:s.currentRuntimeDirs.length,desired:s.desired,exists:s.exists,name:s.name,orphan:s.orphan,runtimeDirCount:s.runtimeDirCount,staleRuntimeDirCount:s.staleRuntimeDirs.length})),runs:{active:e.runs.active,stale:e.runs.stale,total:e.runs.total},warnings:e.warnings}))}}function p(t,e){if(t===null)return null;const{run:s,summary:r}=t;return{durationMs:s.durationMs??null,generatedAt:t.generatedAt??null,id:s.id,startedAt:s.startedAt,status:s.status,types:s.types??[],source:e,files:{failed:r?.files.failed??0,notRun:r?.files.notRun??0,passed:r?.files.passed??0,skipped:r?.files.skipped??0,total:r?.files.total??0},services:{failed:r?.services.failed??0,passed:r?.services.passed??0,skipped:r?.services.skipped??0,total:r?.services.total??0},suites:{completed:r?.suites.completed??0,failed:r?.suites.failed??0,passed:r?.suites.passed??0,skipped:r?.suites.skipped??0,total:r?.suites.total??0}}}function P({discovery:t,latestRunArtifact:e,liveRunArtifact:s,limit:r}){const a=[E(t),...m(s,"live"),...m(e,"latest"),...B(t).slice(0,Math.ceil(r/2)),...L(t).slice(0,r)].filter(n=>n!==null);return W(a).slice(0,r)}function E(t){return t?{detail:t.product.directory,id:"project:all",kind:"project",label:`All tests in ${t.product.name}`,runnable:!0,source:"discovery"}:null}function B(t){return(t?.suites||[]).map(e=>({detail:[e.service,e.testType,e.engineId,e.profileId].filter(Boolean).join(" - "),id:`suite:${e.service}:${e.testType}:${e.engineId}:${e.profileId}:${e.name}`,engineId:e.engineId,kind:"suite",label:e.displayName||e.name,profileId:e.profileId,runnable:e.activeFileCount>0,service:e.service,source:"discovery",suiteName:e.name,testType:e.testType}))}function L(t){return(t?.files||[]).filter(e=>!e.skipped).map(e=>f(e,"discovery"))}function m(t,e){const s=[];for(const r of t?.services??[])for(const a of r.suites)for(const n of a.files??[])n.status!=="failed"&&!n.error||s.push(f({displayName:v(n.path),engineId:a.engineId,path:n.path,profileId:a.profileId,service:r.name,suiteName:a.name,testType:a.testType},e,"failed"));return s}function f(t,e,s=null){return{detail:[t.service,t.testType,t.engineId,t.profileId,t.suiteName].filter(Boolean).join(" - "),filePath:t.path,engineId:t.engineId,id:`file:${t.service||""}:${t.testType}:${t.engineId}:${t.profileId}:${t.path}`,kind:"file",label:t.displayName||v(t.path),profileId:t.profileId,runnable:w(t.testType),service:t.service||null,source:e,status:s||null,suiteName:t.suiteName||null,testType:t.testType}}function O(t){return{activeRuns:t.results.reduce((e,s)=>e+s.runs.active,0),staleRuns:t.results.reduce((e,s)=>e+s.runs.stale,0)}}function W(t){const e=new Set,s=[];for(const r of t)e.has(r.id)||(e.add(r.id),s.push(r));return s}function _(t,e){const s=Number(t);return Number.isFinite(s)?Math.max(1,Math.min(100,Math.round(s))):e}function v(t){return S(i.basename(C(t||"test")).replace(/\.testkit\.[cm]?[jt]sx?$/,""))}
|
package/dist/app/lint/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{loadTestkitConfig as f,resolveProductDir as u}from"../../config/loader/index.js";export async function executeLintOperation(t={}){const
|
|
1
|
+
import{loadTestkitConfig as f,resolveProductDir as u}from"../../config/loader/index.js";export async function executeLintOperation(t={}){const i=u(process.cwd(),t.dir),n=await f(i);return runLintForConfig(m(n),{dir:i})}function m(t){if(!(typeof t!="object"||t===null||Array.isArray(t)))return Reflect.get(t,"config")}import c from"node:fs";import r from"node:path";import{normalizeLintOptions as p,normalizeLintConfig as l}from"./options.js";import{collectCurrentFiles as d,collectTrackedFiles as g,isTestkitSuitePath as y}from"./shared.js";export async function runLint(t={}){const{dir:i,...n}=t,o=r.resolve(process.cwd(),i||".");return a(o,p(n))}export async function runLintForConfig(t,i={}){const n=r.resolve(process.cwd(),i.dir||".");return a(n,l(t))}async function a(t,i){const n=await g(t),o=await d(t,n!==null),e=o.length===0?[]:await(await import("./native.js")).lintProject({productDir:t,files:o,options:i,trackedFiles:n}),s={ok:e.length===0,productDir:t,summary:{files:o.length,testkitFiles:o.filter(y).length,violations:e.length},violations:e};return w(t,s),s}export function normalizeLintConfig(t={}){return l(t)}function w(t,i){try{const n=r.join(t,".testkit","lint");c.mkdirSync(n,{recursive:!0}),c.writeFileSync(r.join(n,"latest.json"),`${JSON.stringify(i,null,2)}
|
|
2
2
|
`,"utf8")}catch{}}
|
package/dist/app/lint/native.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import y from"node:path";import{
|
|
1
|
+
import y from"node:path";import{LINT_RULE_NAMES as _}from"./options.js";import{compareFindings as $,findingKey as B,isTypeScriptLikePath as M,matchesAnyPathPattern as h,normalizeFinding as c,normalizePath as E,SOURCE_IGNORES as F}from"./shared.js";import{createSyntaxPolicy as R}from"./rules/syntax.js";import{createDataPolicy as U}from"./rules/data.js";import{createUnitPolicy as z}from"./rules/unit.js";import{createEngineImportPolicy as A}from"./rules/engine-imports.js";export async function lintProject({productDir:i,files:n,trackedFiles:o,options:s}){const x=await K(),[{ESLint:k},v]=x;if(k.version!=="10.7.0"||v.version!=="8.65.0")throw new Error("Testkit lint requires its managed eslint@10.7.0 and @typescript-eslint/parser@8.65.0 installation");const I=new Map,w=[],f=_.filter(e=>s.rules[e].enabled),L=Object.fromEntries(f.filter(e=>e!=="testkitBoundary").map(e=>{const t=s.rules[e],r={meta:{type:"problem",schema:[],messages:{violation:"{{message}}"}},defaultOptions:[],create(l){return(()=>{const S=E(y.relative(i,l.filename)),u={context:l,file:S,report:(a,b,C,g)=>{const O=c({file:S,ruleName:e,ruleId:t.ruleId,kind:b,target:C,message:g,line:a.loc.start.line,snippet:l.sourceCode.getText(a).split(/\r?\n/u)[0]});I.set(T(l.filename,a.loc.start.line,a.loc.start.column+1,g),O),l.report({node:a,messageId:"violation",data:{message:g}})},options:s,literals:w};return e==="engineImports"?A(u):e==="dataSuiteContract"?U(u):e==="unitTestBoundaries"?z(u):R(e,u,n)})()}};return[t.ruleId,r]})),N=new k({cwd:i,overrideConfigFile:!0,allowInlineConfig:!1,errorOnUnmatchedPattern:!1,globInputPaths:!1,warnIgnored:!1,overrideConfig:[{ignores:F},{files:["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"],languageOptions:{parser:v,ecmaVersion:"latest",sourceType:"module"},plugins:{testkit:{rules:L}}},...f.filter(e=>e!=="testkitBoundary").map(e=>{const t=s.rules[e];return{files:t.include.length?t.include:["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"],ignores:t.exclude,rules:{[`testkit/${t.ruleId}`]:"error"}}})]}),P=[],j=n.filter(M);if(j.length){const e=j.map(r=>y.join(i,r)),t=await N.lintFiles(e);P.push(...t)}const d=P.flatMap(e=>e.messages.map(t=>{if(t.ruleId===null)return c({file:E(y.relative(i,e.filePath)),line:t.line,ruleName:"syntax",ruleId:"syntax",kind:"parse-error",target:t.message,message:t.message});const r=I.get(T(e.filePath,t.line,t.column,t.message));if(!r)throw new Error(`Missing Testkit finding for ${t.ruleId}: ${t.message}`);return r}));if(f.includes("testkitBoundary")){const e=s.rules.testkitBoundary,t=o&&new Set(o);for(const r of n)e.trackedOnly&&t&&!t.has(r)||!q(r,e)||!r.split("/").some(l=>l.toLowerCase().includes("testkit"))||h(r,e.allowed??[])||d.push(c({file:r,ruleName:"testkitBoundary",ruleId:e.ruleId,kind:"testkit-path",target:r,message:"Tracked Testkit plumbing must live in testkit.config.* or __testkit__/ suites, or move into @gfdelarue/testkit"}))}const p=new Map;for(const e of w){const t=`${e.field}:${e.value}`,r=p.get(t)??[];r.push(e),p.set(t,r)}for(const[e,t]of p)if(!(new Set(t.map(r=>r.file)).size<2))for(const r of t)d.push(c({...r,ruleName:"dalParallelSafety",ruleId:"dal-parallel-safety",kind:"duplicate-literal",target:e,message:`Duplicate ${r.field} literal "${r.value}" appears across DAL specs; use fixture-derived namespaced values`}));const m=new Map;for(const e of d.sort($)){const t=B(e);m.has(t)||m.set(t,e)}return[...m.values()]}function T(i,n,o,s){return[i,n,o,s].join("\0")}function q(i,n){return(!n.include.length||h(i,n.include))&&!h(i,n.exclude)}function K(){return Promise.all([import("eslint"),import("@typescript-eslint/parser")])}
|
package/dist/app/lint/policy.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as t}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as t}from"@typescript-eslint/types";export function stringValue(e){return e?.type===t.Literal&&typeof e.value=="string"?e.value:e?.type===t.TemplateLiteral&&e.expressions.length===0?e.quasis[0]?.value.cooked??null:null}export function memberName(e){return!e.computed&&e.property.type===t.Identifier?e.property.name:stringValue(e.property)}export function binding(e,p){for(let r=e.sourceCode.getScope(p);r;r=r.upper){const u=r.set.get(p.name);if(u)return u}return null}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as r}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as r}from"@typescript-eslint/types";import{binding as p,memberName as w,stringValue as E}from"../policy.js";const T=new Set(["addFinding","actor","budget","bytes","count","duration","event","externalCall","expectMetric","expectSequence","fixture","finding","http","metric","money","ratio","reconcile","record","recordCache","recordHttpRequest","recordLlmUsage","recordRpc","sequence","sequenceBudget","system","transition"]),N=new Set(["actor","bytes","duration","externalCall","http","money","ratio","reconcile","record","recordCache","recordHttpRequest","recordLlmUsage","recordRpc","sequence","system","transition"]),f=new Set(["bench","describe","it","suite","test","BenchmarkRunner","TestRunner","EvaluatedModules"]);export function createDataPolicy({context:o,file:C,report:n}){if(!C.endsWith(".data.testkit.ts"))return{};const d=new Set,h=new Set,v=new Map,g=new Map;let y=!1,b=!1;const u=(e,t,a="vitest-runner-import")=>{n(e,a,t,`Data suites cannot use ${t} for Vitest case registration; declare cases with dataScenario(...)`)},x=e=>{for(const t of o.sourceCode.getAncestors(e).reverse())if(t.type===r.ArrowFunctionExpression||t.type===r.FunctionExpression){const a=g.get(t);if(a)return a}},m=e=>e.startsWith("vitest/")&&e!=="vitest/package.json"||e==="@gfdelarue/testkit/node-vitest/contract",k=e=>{E(e)?.includes("{serviceContextUrl:")&&n(e,"service-namespace-url","serviceContextUrl","Data suites must not use serviceContextUrl placeholders because they run outside the deployed service namespace")};return{ImportDeclaration(e){const t=e.source.value;if(t==="@playwright/test"&&n(e,"browser-runtime-import",t,"Browser/runtime driving belongs in ui or e2e suites, not data suites"),(t.startsWith("@testing-library/")||t==="jsdom"||t==="happy-dom")&&n(e,"dom-runtime-import","dom-runtime","DOM/rendering tests belong in ui suites, not data suites"),e.importKind==="type")return;const a=e.specifiers.filter(s=>s.type!==r.ImportSpecifier||s.importKind!=="type");if(!(e.specifiers.length&&!a.length)){m(t)&&u(e,t),t==="@gfdelarue/testkit/node-vitest/data"&&(y=!0);for(const s of a){const i=o.sourceCode.getDeclaredVariables(e).find(l=>l.name===s.local.name);if(t==="vitest"&&s.type===r.ImportNamespaceSpecifier&&i&&h.add(i),s.type!==r.ImportSpecifier)continue;const c=s.imported.type===r.Identifier?s.imported.name:s.imported.value;t==="vitest"&&f.has(c)&&u(s,c),t==="@gfdelarue/testkit/node-vitest/data"&&c==="dataScenario"&&i&&d.add(i)}}},ExportNamedDeclaration(e){if(!e.source||e.exportKind==="type")return;const t=e.specifiers.filter(a=>a.exportKind!=="type");if(!(e.specifiers.length&&!t.length)&&(m(e.source.value)&&u(e,e.source.value),e.source.value==="vitest"))for(const a of t){const s=a.local.type===r.Identifier?a.local.name:a.local.value;f.has(s)&&u(a,s)}},ExportAllDeclaration(e){e.exportKind!=="type"&&m(e.source.value)&&u(e,e.source.value)},MemberExpression(e){if(e.object.type!==r.Identifier)return;const t=p(o,e.object),a=w(e);t&&h.has(t)&&a&&f.has(a)&&u(e,a,"vitest-runner-call")},CallExpression(e){const t=e.callee.type===r.Identifier?p(o,e.callee):null;if(t&&d.has(t)){b=!0;const i=e.arguments.at(-1);if(!i||i.type!==r.ArrowFunctionExpression&&i.type!==r.FunctionExpression){n(e,"missing-scenario-callback","dataScenario","dataScenario(...) must provide a callback that receives the Testkit data context");return}const c=i.params[0]?.type===r.AssignmentPattern?i.params[0].left:i.params.at(0),l=c?.type===r.Identifier?o.sourceCode.getDeclaredVariables(i).find(D=>D.name===c.name):null;if(!l){n(e,"missing-data-context","dataScenario","dataScenario callbacks must accept the Testkit data context and record data evidence");return}const S={callback:i,context:l,contextName:l.name,methods:new Set,assertion:!1};v.set(e,S),g.set(i,S);return}const a=o.sourceCode.getText(e.callee);e.callee.type===r.MemberExpression&&a.includes(".serviceContextUrl")&&n(e,"service-namespace-url","serviceContextUrl","Data suites run in the managed Node.js runner, not inside a service namespace; use baseUrl/public endpoints or move service-namespace checks to int/e2e");const s=x(e);if(s&&(/^(expect|assert)(?:\(|\.)/u.test(a)&&(s.assertion=!0),e.callee.type===r.MemberExpression&&e.callee.object.type===r.Identifier&&p(o,e.callee.object)===s.context)){const i=w(e.callee);i&&T.has(i)&&s.methods.add(i)}},"CallExpression:exit"(e){const t=v.get(e);if(!t)return;const{callback:a,contextName:s,methods:i,assertion:c}=t;i.size?[...i].some(l=>N.has(l))||(n(a,"weak-data-evidence",s,"Data scenarios must record product data-flow evidence such as http, bytes, sequence, record, transition, system, externalCall, recordCache, recordLlmUsage, or reconcile; ordinary Node/Vitest-style checks belong in *.contract.testkit.ts with @gfdelarue/testkit/node-vitest/contract"),c&&n(a,"vitest-shaped-data-suite",s,"This dataScenario looks like an ordinary assertion test with weak data bookkeeping; move Node-backed contract checks to *.contract.testkit.ts or model the product data flow with structured evidence")):n(a,"missing-data-evidence",s,"Data scenarios must record Testkit-owned data-flow evidence with the data context, such as http, bytes, sequence, record, transition, system, externalCall, recordCache, recordLlmUsage, or reconcile")},ThrowStatement(e){const t=x(e);t&&(t.assertion=!0)},Literal:k,TemplateLiteral:k,"Program:exit"(e){y?d.size?b||n(e,"missing-data-scenario","dataScenario","Data suites must declare at least one dataScenario(...)"):n(e,"missing-data-scenario-import","dataScenario","Data suites must import dataScenario from @gfdelarue/testkit/node-vitest/data"):n(e,"missing-data-import","@gfdelarue/testkit/node-vitest/data","Data suites must import dataScenario from @gfdelarue/testkit/node-vitest/data")}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as l}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as l}from"@typescript-eslint/types";import{isPublicTestType as g}from"../../../domain/identifiers.js";import{BUILT_IN_ENGINE_CATALOG as I,expectedEngineAuthoringModule as h,invalidEngineAuthoringImport as E,requireMatchingHostImport as x}from"../../../engines/catalog.js";import{binding as y,stringValue as c}from"../policy.js";export function createEngineImportPolicy({context:u,file:i,report:m}){const n=/\.([^.]+)\.testkit\.ts$/u.exec(i)?.[1];if(!g(n))return{};const p=I.profilesForTestType(n),o=p[0];if(p.length!==1)return{};const a={engineId:o.engineId,profileId:o.id},f=h(o.id),r=(t,e)=>{if(e!==null)try{if(x(i,a,e),e!==f&&(e.startsWith("@gfdelarue/testkit/")||e.startsWith("@gfdelarue/testkit-engine-")))throw E(i,a,e)}catch(s){if(!(s instanceof Error))throw s;m(t,"engine-import",e,s.message)}};return{ImportDeclaration(t){t.importKind!=="type"&&(t.specifiers.length===0||t.specifiers.some(e=>e.type!==l.ImportSpecifier||e.importKind!=="type"))&&r(t,t.source.value)},ExportNamedDeclaration(t){t.source&&t.exportKind!=="type"&&t.specifiers.some(e=>e.exportKind!=="type")&&r(t,t.source.value)},ExportAllDeclaration(t){t.exportKind!=="type"&&r(t,t.source.value)},ImportExpression(t){r(t,c(t.source))},CallExpression(t){t.callee.type===l.Identifier&&t.callee.name==="require"&&!y(u,t.callee)?.defs.length&&r(t,c(t.arguments[0]))}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as a}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as a}from"@typescript-eslint/types";import{memberName as O,stringValue as u}from"../policy.js";export function createSyntaxPolicy(n,{file:s,context:c,report:l,options:r,literals:m},N){if(n==="uiSpecShape"&&s.endsWith(".ui.testkit.ts")){let t=0;return{CallExpression(e){e.callee.type===a.Identifier&&e.callee.name==="test"&&u(e.arguments[0])!==null&&(t+=1)},"Program:exit"(e){const i=c.sourceCode.text.split(/\r?\n/u).length;(i>r.ui.maxLines||t>r.ui.maxTests)&&l(e,"shape-limit",`${String(i)}:${String(t)}`,`UI spec exceeds shape limit (${String(i)}/${String(r.ui.maxLines)} lines, ${String(t)}/${String(r.ui.maxTests)} tests)`)}}}if(n==="configImports"&&s===N.find(t=>/^testkit\.config\.[cm]?[jt]s$/u.test(t)))return{ImportDeclaration(t){const e=t.source.value;(e.startsWith(".")||e.startsWith("/"))&&e.includes("__testkit__")&&l(t,"config-helper-import",e,"testkit.config.ts must not import repo-local __testkit__ helper modules")}};if(n!=="dalParallelSafety")return{};const o=s.endsWith(".dal.testkit.ts"),p=s.split("/").includes("__testkit__")&&s.split("/").includes("helpers")&&s.endsWith(".ts");return{CallExpression(t){if(!o)return;const e=t.callee;(e.type===a.Identifier&&e.name==="truncate"||e.type===a.MemberExpression&&O(e)==="truncate")&&l(t,"truncate","truncate","DAL specs must not truncate shared tables; seed namespaced rows and scope queries instead")},Property(t){if(!o)return;const e=!t.computed&&t.key.type===a.Identifier?t.key.name:u(t.key),i=u(t.value);e&&["slug","email","authProviderId"].includes(e)&&i&&m.push({field:e,value:i,file:s,line:t.loc.start.line,snippet:c.sourceCode.getText(t).split(/\r?\n/u)[0]})},Literal(t){p&&typeof t.value=="string"&&/\bON\s+CONFLICT(?:\s*\([^)]*\))?\s+DO\s+NOTHING\b/iu.test(t.value)&&l(t,"silent-conflict","ON CONFLICT DO NOTHING","Testkit helper seed code must not use ON CONFLICT DO NOTHING; collisions should fail loudly")},TemplateLiteral(t){p&&/\bON\s+CONFLICT(?:\s*\([^)]*\))?\s+DO\s+NOTHING\b/iu.test(t.quasis.map(e=>e.value.cooked??"").join("\0"))&&l(t,"silent-conflict","ON CONFLICT DO NOTHING","Testkit helper seed code must not use ON CONFLICT DO NOTHING; collisions should fail loudly")}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as o}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as o}from"@typescript-eslint/types";import{memberName as a,stringValue as m}from"../policy.js";const u=new Set(["vi.mock","jest.mock","mock.method"]),p=new Set(["vi.fn","vi.spyOn","vi.stubGlobal","vi.useFakeTimers","jest.fn","jest.spyOn","jest.useFakeTimers"]),b=new Set(["sinon","nock","msw","jest-mock"]),f=new Set(["fetch","http.createServer","net.createServer"]),l=new Set(["fs","node:fs","child_process","node:child_process","execa"]);export function createUnitPolicy({file:r,report:s}){if(!/\.(test|spec)\.[cm]?[jt]sx?$/u.test(r)||r.split("/").includes("__testkit__"))return{};const i=(e,t)=>{if(!t)return;const c=t.split("/"),n=t.startsWith("@")?c.slice(0,2).join("/"):c[0];b.has(n)&&s(e,"test-double-library",n,"Unit tests must not use test-double libraries"),(l.has(t)||l.has(n))&&s(e,"io-boundary-import",t,"Unit tests must not import process, network, filesystem, or runtime boundary modules")};return{ImportDeclaration(e){i(e,e.source.value)},ImportExpression(e){i(e,m(e.source))},CallExpression(e){const t=h(e.callee);u.has(t)&&s(e,"module-replacement",t,"Unit tests must not replace modules; move boundary behavior to a Testkit suite"),p.has(t)&&s(e,"function-replacement",t,"Unit tests must not replace collaborators; extract pure logic or move behavior to a Testkit suite"),f.has(t)&&s(e,"io-boundary-call",t,"Unit tests must not cross process, network, filesystem, or runtime boundaries"),t==="require"&&e.arguments.length===1&&i(e,m(e.arguments[0]))},MemberExpression(e){e.object.type===o.Identifier&&e.object.name==="process"&&a(e)==="env"&&s(e,"io-boundary-global","process.env","Unit tests must not read process-global environment state")}}}function h(r){if(r.type===o.Identifier)return r.name;if(r.type!==o.MemberExpression)return"";let s=r.object;for(;s.type===o.MemberExpression;)s=s.object;const i=a(r);return s.type===o.Identifier&&i?`${s.name}.${i}`:""}
|
|
@@ -22,7 +22,7 @@ export declare const DEFAULT_EXCLUDED_DIRS: string[];
|
|
|
22
22
|
export declare const SOURCE_PATTERNS: string[];
|
|
23
23
|
export declare const SOURCE_IGNORES: string[];
|
|
24
24
|
export declare const DEFAULT_TESTKIT_BOUNDARY_ALLOWED: string[];
|
|
25
|
-
export declare function collectCurrentFiles(productDir: string): Promise<string[]>;
|
|
25
|
+
export declare function collectCurrentFiles(productDir: string, gitAvailable: boolean): Promise<string[]>;
|
|
26
26
|
export declare function collectTrackedFiles(productDir: string): Promise<string[] | null>;
|
|
27
27
|
export declare function normalizePath(value: string): string;
|
|
28
28
|
export declare function isObjectRecord(value: unknown): value is Record<string, unknown>;
|
package/dist/app/lint/shared.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import i from"node:fs";import n from"node:path";import{minimatch as l}from"minimatch";import{glob as p}from"tinyglobby";import{TEST_TYPE_ORDER as u}from"../../domain/test-types.js";import{runManagedGitCommand as a}from"../../repo/managed-git.js";export const DEFAULT_EXCLUDED_DIRS=[".git",".next",".next-testkit",".testkit","coverage","dist","node_modules","playwright-report"],SOURCE_PATTERNS=["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"],SOURCE_IGNORES=DEFAULT_EXCLUDED_DIRS.map(t=>`**/${t}/**`),DEFAULT_TESTKIT_BOUNDARY_ALLOWED=["testkit.config.ts","testkit.config.mts","testkit.config.mjs","testkit.config.js","**/__testkit__/**"];export async function collectCurrentFiles(t,e){const o=e?await a(t,["ls-files","--cached","--others","--exclude-standard","-z"]):void 0,s=o?.exitCode===0?o.stdout.split("\0").filter(r=>r&&i.existsSync(n.join(t,r))):await p(SOURCE_PATTERNS,{cwd:t,ignore:SOURCE_IGNORES,dot:!0,followSymbolicLinks:!1});return[...new Set(s.map(normalizePath))].filter(r=>isSourceLikePath(r)&&!isExcludedRelativePath(r)).sort((r,c)=>r.localeCompare(c))}export async function collectTrackedFiles(t){const e=await a(t,["ls-files","-z"]);return e.exitCode!==0?null:e.stdout.split("\0").map(o=>normalizePath(o)).filter(Boolean).filter(o=>i.existsSync(n.join(t,o))).sort((o,s)=>o.localeCompare(s))}export function normalizePath(t){return t.split(n.sep).join("/")}export function isObjectRecord(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}export function isSourceLikePath(t){return/\.(?:[cm]?[jt]sx?|json)$/u.test(t)}export function isTypeScriptLikePath(t){return/\.[cm]?[jt]sx?$/u.test(t)}export function isTestkitSuitePath(t){return u.some(e=>t.endsWith(`.${e}.testkit.ts`))}export function isExcludedRelativePath(t){return SOURCE_IGNORES.some(e=>l(t,e,{dot:!0}))}export function matchesAnyPathPattern(t,e){return e.some(o=>l(t,o,{dot:!0}))}export function compareFindings(t,e){return t.file.localeCompare(e.file)||t.line-e.line||t.ruleId.localeCompare(e.ruleId)||t.kind.localeCompare(e.kind)||t.target.localeCompare(e.target)}export function normalizeFinding(t){return{ruleName:t.ruleName,ruleId:t.ruleId,kind:t.kind||"violation",target:t.target||"",file:normalizePath(t.file),line:t.line||1,message:t.message,...t.snippet?{snippet:t.snippet}:{}}}export function findingKey(t){return[t.file,String(t.line),t.ruleId,t.kind,t.target].join("\0")}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveRuntimeConfigs as re}from"../../config/loader/dependencies.js";import{buildRuntimeGraphs as ne,claimNextTask as ie,collectSuites as ae}from"./planning.js";import{buildRunPlanningMetadata as se,buildScheduledQueue as oe}from"./scheduler/index.js";import{addTrackerError as B,buildServiceTrackers as ue,finalizeServiceResult as _,recordGraphError as le,recordTaskOutcome as ce,summarizeDbBackend as me}from"./results.js";import{loadTimings as fe,saveTimings as de,writeRunArtifact as pe}from"../../evidence/index.js";import{createRunLogRegistry as ge}from"../../evidence/logs.js";import{createOperationTimingRegistry as ke,timeOperation as L}from"../../evidence/operation-timings.js";import{createRunReporterFromListener as he}from"./run-events.js";import{createSetupOperationRegistry as Se}from"../../evidence/setup-progress.js";import{createLocalRunEnvironment as ve}from"../../environment/run-environment.js";import{readPackageMetadata as Re,safeHostname as ye,safeUsername as Ce}from"../../process/host/metadata.js";import{collectGitMetadata as we}from"../../repo/metadata.js";import{resolveExecutionConfig as Fe}from"../../config/loader/decode-execution.js";import"../../environment/run-environment.js";import{createEngineManager as Ae}from"../../engines/factory.js";import{createWorker as xe,runWorker as Ie}from"./worker-loop.js";import{ensureRequestedFilesMatch as Ee}from"./run-guards.js";import{createLiveSnapshotWriter as Te}from"./live-run.js";import{finalizeRunArtifacts as be}from"./run-finalization.js";import{buildRunArtifact as ze}from"../../evidence/reporting.js";import{createOperationReporterBridge as Me}from"./operation-progress.js";import{formatError as $,serializeError as J}from"../../kernel/error-serialization.js";export async function runAll(e,l,
|
|
1
|
+
import{resolveRuntimeConfigs as re}from"../../config/loader/dependencies.js";import{buildRuntimeGraphs as ne,claimNextTask as ie,collectSuites as ae}from"./planning.js";import{buildRunPlanningMetadata as se,buildScheduledQueue as oe}from"./scheduler/index.js";import{addTrackerError as B,buildServiceTrackers as ue,finalizeServiceResult as _,recordGraphError as le,recordTaskOutcome as ce,summarizeDbBackend as me}from"./results.js";import{loadTimings as fe,saveTimings as de,writeRunArtifact as pe}from"../../evidence/index.js";import{createRunLogRegistry as ge}from"../../evidence/logs.js";import{createOperationTimingRegistry as ke,timeOperation as L}from"../../evidence/operation-timings.js";import{createRunReporterFromListener as he}from"./run-events.js";import{createSetupOperationRegistry as Se}from"../../evidence/setup-progress.js";import{createLocalRunEnvironment as ve}from"../../environment/run-environment.js";import{readPackageMetadata as Re,safeHostname as ye,safeUsername as Ce}from"../../process/host/metadata.js";import{collectGitMetadata as we}from"../../repo/metadata.js";import{resolveExecutionConfig as Fe}from"../../config/loader/decode-execution.js";import"../../environment/run-environment.js";import{createEngineManager as Ae}from"../../engines/factory.js";import{createWorker as xe,runWorker as Ie}from"./worker-loop.js";import{ensureRequestedFilesMatch as Ee}from"./run-guards.js";import{createLiveSnapshotWriter as Te}from"./live-run.js";import{finalizeRunArtifacts as be}from"./run-finalization.js";import{buildRunArtifact as ze}from"../../evidence/reporting.js";import{createOperationReporterBridge as Me}from"./operation-progress.js";import{formatError as $,serializeError as J}from"../../kernel/error-serialization.js";export async function runAll(e,l,g,t,p=e){const k=t.fileNames||[];Ee(e,l,g,k);const a=e[0]?.productDir||process.cwd(),m=ve(a),I=Ae(a),w=new Map(p.map(n=>[n.name,n])),h=Date.now(),f=e[0]?.artifacts??null,z=[...new Set(e.flatMap(n=>Object.keys(n.testkit.serviceEnv)))].sort(),Q={git:await we(a),host:{hostname:ye(),username:Ce()},testkitVersion:Re().version},S=t.onEvent?he(t.onEvent,{debug:t.debug}):null,F=ge(a),v={workerCount:0,runtimeInstanceCount:0,runtimeStats:[]},q=resolveRunExecutionConfig(e[0],t),N=collectServicePlans(e,w,l,g,t,q,S);S?.setServicePlans?.(N);const A=ue(N,h),Y=g.map(n=>({...n})),D={typeValues:l,suiteSelectors:Y,fileNames:k,serviceFilter:t.serviceFilter||null,planning:null};let d=()=>{};const Z=Me(S),x=ke({onChange:n=>{Z(n),d()}}),P=Se({logRegistry:F,onChange:()=>{d()}}),O=N.filter(n=>!n.skipped);let o;const r=await m.start({keepAlive:!0,resetResultsBeforePublish:!0});d=Te({productDir:a,runId:r.runId,configs:e,trackers:A,startedAt:h,execution:q,workerState:v,selection:D,archiveConfig:f,declaredEnvironmentKeys:z,metadata:Q,logRegistry:F,setupRegistry:P,operationTimingRegistry:x,reporter:S});const M={artifacts:null},ee=async(n,c)=>(r.beginFinalization(),await r.finish(n,u=>{const i=c(u);return M.artifacts={runArtifact:i.runArtifact},i},"normal"));let R,E;const U=(n,c,s)=>({productDir:a,runId:r.runId,results:n,startedAt:h,finishedAt:c,execution:q,workerCount:v.workerCount,runtimeInstanceCount:v.runtimeInstanceCount,runtimeStats:v.runtimeStats,selection:D,archiveConfig:f,declaredEnvironmentKeys:z,metadata:Q,logRegistry:F,setupRegistry:P,operationTimingRegistry:x,reporter:S,runInterruptReason:r.getInterruptReason(),runStatus:s,publishRunFinalization:ee});d();let y=null;try{try{if(O.length>0){const s=fe(a),{graphs:u,queue:i}=await L(x,{name:"testkit.plan",kind:"planning",summary:"plan test execution",scope:{serviceCount:O.length}},()=>{const T=ne(O),C=oe(O,T,{timings:s});return{graphs:T,queue:C}});D.planning=se(i,u),d(),S?.setTotalFileCount?.(i.length),v.workerCount=Math.max(1,Math.min(q.taskConcurrency,i.length)),v.runtimeInstanceCount=u.reduce((T,C)=>T+C.instanceCount,0);const V=Array.from({length:v.workerCount},(T,C)=>xe(C+1,a));r.provision(u,{reporter:S,logRegistry:F,setupRegistry:P,operationTimingRegistry:x,skipSchemaSourceVerify:t.skipSchemaSourceVerify});const H=[];try{const C=(await L(x,{name:"testkit.execute",kind:"execution",summary:"execute local worker queue",scope:{workerCount:V.length,taskCount:i.length}},async()=>Promise.allSettled(V.map(j=>Ie(j,i,r,I,A,H,ie,(b,G,K,W)=>{ce(b,G,K,W),d()},(b,G,K,W,te)=>{le(b,G,K,W,te),d()},S))))).flatMap(workerResultErrorsForOrchestrator);for(const j of C)for(const b of A.values())b.skipped||B(b,j);C.length>0&&d(),v.runtimeStats=r.getStats(),d()}finally{await L(x,{name:"testkit.cleanup",kind:"cleanup",summary:"stop managed runtimes and services"},()=>r.cleanupAll())}de(a,s,H)}}catch(s){y=J(s);const u=y.message||$(s);for(const i of A.values())i.skipped||B(i,u);d()}o=y===null?0:r.isStopRequested()?130:1,E=Date.now(),R=e.map(s=>_(A.get(s.name),h,E));const n=resolveRunGateFailure(R,t.allowEmpty===!0);await L(x,{name:"testkit.collect",kind:"collection",summary:"close and collect run logs"},()=>F.closeAll());const c=await be(U(R,E,r.isStopRequested()?"interrupted":y===null&&n===null?null:"failed"));return y!==null&&(o=1),R.some(s=>s.failed||s.infraFailed)&&(o=1),n!==null&&(o=1),c.shouldFailArchivePublication&&(o=1),c.shouldFailArchiveCleanup&&(o=1),(c.runArtifact.run.status==="interrupted"||r.isStopRequested()||r.committedFinalization?.artifactStatus==="interrupted")&&(o=Math.max(o,130)),{runArtifact:c.runArtifact,results:R,exitCode:o,...y===null?{}:{failure:y}}}catch(n){const c=J(n),s=c.message||$(n);for(const i of A.values())i.skipped||B(i,s);d(),r.beginFinalization(),o=r.isStopRequested()||r.committedFinalization?.artifactStatus==="interrupted"?130:1,E=Date.now(),R=e.map(i=>_(A.get(i.name),h,E)),await X(F);let u=M.artifacts;if(u===null)try{u=await qe(U(R,E,r.isStopRequested()?"interrupted":"failed"),r,M,c)}catch{if(M.artifacts===null)throw n;u=M.artifacts}return(u.runArtifact.run.status==="interrupted"||r.isStopRequested()||r.committedFinalization?.artifactStatus==="interrupted")&&(o=Math.max(o,130)),{runArtifact:u.runArtifact,results:R,exitCode:o,failure:y??c}}finally{r.dispose(),await X(F)}}async function qe(e,l,g,t){return l.finish("failed",p=>{const k=p.artifactStatus==="interrupted"?p:{artifactStatus:"failed",interruptReason:null},a=e.declaredEnvironmentKeys??[],m=ze({productDir:e.productDir,runId:e.runId,results:e.results,startedAt:e.startedAt,finishedAt:e.finishedAt,execution:e.execution,workerCount:e.workerCount,runtimeInstanceCount:e.runtimeInstanceCount,runtimeStats:e.runtimeStats,typeValues:e.selection.typeValues,suiteSelectors:e.selection.suiteSelectors,fileNames:e.selection.fileNames,serviceFilter:e.selection.serviceFilter,planning:e.selection.planning??null,archiveConfig:e.archiveConfig,declaredEnvironmentKeys:a,metadata:e.metadata,summarizeDbBackend:me,serviceLogs:e.logRegistry.listServiceLogs(),setupLogs:e.logRegistry.listSetupLogs(),setupOperations:e.setupRegistry.listOperations(),operationTimings:e.operationTimingRegistry?.listOperations()??[],runInterruptReason:k.interruptReason,runStatus:k.artifactStatus});m.finalizationFailure={...t};const w={runArtifact:pe(e.productDir,m,{archiveConfig:e.archiveConfig,declaredEnvironmentKeys:a})};return g.artifacts=w,w},"recovery")}async function X(e){try{await e.closeAll()}catch{}}export function workerResultErrorsForOrchestrator(e){return e.status==="rejected"?[$(e.reason)]:[...new Set(e.value.unpersistedErrors)]}export function resolveRunGateFailure(e,l=!1){return e.some(t=>t.failed)?"test failure":e.some(t=>t.infraFailed)?"test infrastructure failure":e.some(t=>t.notRunFileCount>0)?"one or more required test files did not run":e.some(t=>t.skippedFileCount>0)?"one or more selected test files were skipped":e.reduce((t,p)=>t+p.completedFileCount,0)===0&&!l?"no runnable test files executed":null}export function resolveRunExecutionConfig(e,l){return Fe({cli:{fileTimeoutSeconds:l.fileTimeoutSeconds,taskConcurrency:l.taskConcurrency},repo:e?{fileTimeoutSeconds:e.testkit.execution.fileTimeoutSeconds,taskConcurrency:e.testkit.execution.taskConcurrency}:void 0})}export function collectServicePlans(e,l,g,t,p,k,a=null){return e.map(m=>{const I=ae(m,g,t,p.fileNames||[],p);if(I.length===0)return a?.serviceSkipped?.(m,`no matching files (types=${g.join(",")||"all"} suites=${t.map(f=>f.raw).join(",")||"all"} files=${(p.fileNames||[]).join(",")||"all"})`),{config:m,execution:k,skipped:!0,suites:[],runtimeConfigs:[],runtimeNames:[],runtimeKey:null};for(const f of I)for(const z of f.skippedFiles)a?.plannedSkip?.({serviceName:m.name,testType:f.testType,engineId:f.engineId,profileId:f.profileId,file:z.path,reason:z.reason});const w=re(m,l),h=w.map(f=>f.name).sort();return{config:m,execution:k,skipped:!1,suites:I,runtimeConfigs:w,runtimeNames:h,runtimeKey:h.join("|")}})}
|
package/dist/app/run/request.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{resolveProductDir as u}from"../../process/files/product-dir.js";import{loadManagedConfigs as d}from"../../config/loader/managed-configs.js";import{parseFileTimeoutOption as S}from"../../domain/file-timeout.js";import{parseSuiteOption as y,parseTypeOption as k}from"../../domain/suite-selection.js";import{resolveRequestedFiles as v}from"../file-selection.js";import{parseTaskConcurrencyOptions as w}from"../../config/loader/decode-execution.js";export async function buildRunRequest(e,c=null,l=process.cwd(),t=process.cwd()){const n=w(e.taskConcurrency,e.workers),a=e.fileTimeoutSeconds==null?null:S(e.fileTimeoutSeconds),r=k(e.type,c),s=y(e.suite),m=e.file??[],o=u(l,e.dir),i=v(m,o,t),{allConfigs:f,configs:p}=await d({dir:o,service:e.service,testSelection:{service:e.service,fileNames:i,typeValues:r,suiteSelectors:s}});return{allConfigs:f,configs:p,productDir:o,typeValues:r,suiteSelectors:s,runOptions:{allowEmpty:e.allowEmpty,fileNames:i,taskConcurrency:n,fileTimeoutSeconds:a,serviceFilter:e.service||null,ignoreSkipRules:e.ignoreSkipRules,skipSchemaSourceVerify:e.skipSchemaSourceVerify}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveProductDir as
|
|
1
|
+
import{resolveProductDir as m}from"../../config/loader/paths.js";import g from"node:os";import{recordWork as l}from"../../kernel/work-metrics.js";import{runManagedProcess as d}from"../../process/index.js";import{currentLinuxLibc as h,currentPlatformTarget as c}from"../../process/host/platform.js";import{RootPackageResolver as f}from"../../engines/installations/package-resolution.js";import{playwrightRegistryEnvironment as w,PlaywrightInstallationError as p,requireSupportedPlaywrightHost as y,resolvePlaywrightInstallation as P,resolvePlaywrightPackageFamily as E,resolvePlaywrightRegistry as x,throwIfPlaywrightProcessAborted as R}from"../../engines/installations/playwright.js";export const PLAYWRIGHT_SETUP_TIMEOUT_MS=1200*1e3;export class PlaywrightSetupError extends Error{code="TESTKIT_PLAYWRIGHT_SETUP_FAILED";constructor(e,t){super(`Playwright setup failed: ${e}`,t),this.name="PlaywrightSetupError"}}export function playwrightSetupArguments(r,e){return Object.freeze([r,"install","chromium","--no-shell",...e.withDeps?["--with-deps"]:[],...e.force?["--force"]:[]])}export async function executePlaywrightSetup(r){r.signal.throwIfAborted(),y({linuxLibc:h(),target:c()});const e=Object.freeze({...r.ambientEnvironment??process.env}),t=r.configuredRegistry??e.PLAYWRIGHT_BROWSERS_PATH,u=E(new f),s=x({ambientEnvironment:e,configuredValue:t,homeDirectory:g.homedir(),platform:c().platform,productRoot:r.productRoot}),o=()=>(l("playwright.setup.probes"),P({productRoot:r.productRoot,signal:r.signal},{ambientEnvironment:e,configuredRegistry:t}));if(!r.force&&!r.withDeps)try{return await o()}catch(a){if(r.signal.throwIfAborted(),!(a instanceof p))throw a}l("playwright.setup.installations");const n=await d({args:playwrightSetupArguments(u.cli,r),cwd:r.productRoot,displayLabel:"Playwright Chromium setup",env:w(e,s),executable:process.execPath,signal:r.signal,timeoutMs:PLAYWRIGHT_SETUP_TIMEOUT_MS}),i=S(n);if(R(r.signal,n,i),i!==null)throw new PlaywrightSetupError(i.message,{cause:i});return o()}function S(r){if(r.terminationError!==null)return r.terminationError;if(r.error!==null)return r.error;if(r.timedOut)return new Error(`Chromium installer exceeded ${PLAYWRIGHT_SETUP_TIMEOUT_MS.toString()} milliseconds`);if(r.cancelled)return new Error("Chromium installer was cancelled");if(r.outputLimitExceeded)return new Error("Chromium installer exceeded the managed output limit");if(r.exitCode!==0){const e=r.stderr.trim();return new Error(`Chromium installer ${r.exitCode===null?`terminated by ${String(r.exitSignal)}`:`exited with code ${r.exitCode.toString()}`}${e.length===0?"":`: ${e}`}`)}return null}export function setupPlaywright(r){return executePlaywrightSetup({...r,productRoot:m(process.cwd(),r.dir)})}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NormalizedServiceConfig } from "../../config/loader/index.js";
|
|
1
2
|
import { type StatusModel } from "./model.js";
|
|
2
3
|
export type { StatusModel } from "./model.js";
|
|
3
4
|
export interface CollectServiceStatusOptions {
|
|
@@ -5,3 +6,4 @@ export interface CollectServiceStatusOptions {
|
|
|
5
6
|
service?: null | string;
|
|
6
7
|
}
|
|
7
8
|
export declare function collectServiceStatus(options?: Readonly<CollectServiceStatusOptions>): Promise<StatusModel[]>;
|
|
9
|
+
export declare function collectConfiguredServiceStatus(allConfigs: NormalizedServiceConfig[], service?: null | string): StatusModel[];
|
package/dist/app/status/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{loadManagedConfigs as
|
|
1
|
+
import{filterConfigsByService as n,loadManagedConfigs as i}from"../../config/loader/managed-configs.js";import{collectStatusModel as r}from"./model.js";export async function collectServiceStatus(t={}){const{allConfigs:o,configs:e}=await i(t);return e.map(c=>r(c,{allConfigs:o}))}export function collectConfiguredServiceStatus(t,o=null){return n(t,o).map(e=>r(e,{allConfigs:t}))}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { NormalizedConfigContext } from "../../config/loader/index.js";
|
|
2
|
+
import type { RunManifest } from "../../process/runs/manifest.js";
|
|
3
3
|
import { type UnknownRecord } from "../../kernel/unknown-values.js";
|
|
4
4
|
type RunStatusContext = Pick<NormalizedConfigContext, "productDir">;
|
|
5
5
|
export interface BuildRunStatusOptions {
|
package/dist/app/status/runs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import c from"node:fs";import f from"node:path";import{
|
|
1
|
+
import c from"node:fs";import f from"node:path";import{inspectRunManifests as d}from"../../process/runs/inspection.js";import{resolveProductDir as m}from"../../process/files/product-dir.js";import{asUnknownRecord as o,displayString as l,isUnknownRecord as p,optionalString as i}from"../../kernel/unknown-values.js";export async function collectRunStatus(t){if(t.runId){const e=m(process.cwd(),t.dir);if(!c.statSync(e).isDirectory())throw new Error(`Product directory is not a directory: ${e}`);return buildRunStatus({productDir:e},t)}const{loadConfigContext:r}=await import("../../config/loader/index.js"),n=await r({dir:t.dir});return buildRunStatus(n,t)}export function buildRunStatus(t,r={}){const n=d(t.productDir,r.runId||void 0),e=S(f.join(t.productDir,".testkit","results","live.json"));if(r.full)return Promise.resolve({productDir:t.productDir,mode:"full",runs:n,live:e});const s={productDir:t.productDir,mode:"summary",runs:n.map(A),live:b(e)};return s.live&&(s.live.state=describeLiveArtifactState(s)),Promise.resolve(s)}export function latestOperation(t){if(!Array.isArray(t))return null;const r=t.filter(p);return[...r].reverse().find(n=>n.status==="running")??r.at(-1)??null}export function describeLiveArtifactState(t){const r=o(t.live),n=o(r?.run),e=l(n?.id,"unknown"),s=l(n?.status,"unknown"),a=y(t,e);return s==="running"&&!a?{runId:e,status:"stale",reason:"no active run matches this live artifact"}:{runId:e,status:s,reason:i(n?.interruptReason)}}function y(t,r){if(!r||r==="unknown")return!1;for(const n of t.runs??[])if(n.active&&v(r,n.runId))return!0;return!1}function v(t,r){if(!r)return!1;const n=typeof r=="string"?r:typeof r=="number"&&Number.isFinite(r)?r.toString():null;return n?n===t||n.startsWith(`${t}:`):!1}function A(t){const r=g(t.status);return{runId:t.runId,pid:t.pid,status:t.status||"unknown",startedAt:t.startedAt||null,heartbeatAt:t.heartbeatAt||null,interruptReason:t.interruptReason||null,active:t.active,terminal:r,stale:!t.active&&!r,session:h(t.session)}}function g(t){return typeof t=="string"&&["finished","failed","interrupted"].includes(t)}function h(t){const r=o(t);return r?{kind:i(r.kind),status:i(r.status),stage:i(r.stage)}:null}function b(t){const r=o(t);if(!r)return null;const e=o(r.timings)?.operations??[],s=Array.isArray(e)?e:[],a=o(r.product),u=o(r.run);return{present:!0,schemaVersion:k(r.schemaVersion),generatedAt:i(r.generatedAt),product:a?{name:i(a.name),directory:i(a.directory)}:null,run:u?{id:i(u.id),status:i(u.status),startedAt:i(u.startedAt),finishedAt:i(u.finishedAt),durationMs:w(u.durationMs),interruptReason:i(u.interruptReason),testkitVersion:i(u.testkitVersion)}:null,summary:r.summary??null,operation:latestOperation(s),operationCount:s.length,artifactRef:".testkit/results/live.json",truncated:!0}}function w(t){const r=Number(t);return Number.isFinite(r)?r:null}function k(t){return typeof t=="number"&&Number.isFinite(t)?t:null}function S(t){try{return JSON.parse(c.readFileSync(t,"utf8"))}catch{return null}}
|
package/dist/app/stop/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import s from"node:fs";import{resolveProductDir as a}from"../../process/files/product-dir.js";import{createEnvironmentMaintenance as i}from"../../environment/maintenance.js";import{normalizeOperationReason as o}from"../../domain/operation-reason.js";export async function stopRuns(n,r={}){return i(n).stop(r.runId?{kind:"run",runId:r.runId}:r.all?{kind:"all"}:{kind:"latest-active"},o(r.reason,"run stopped"))}export async function stopProductRuns(n,r){const t=a(process.cwd(),n.dir);if(!s.statSync(t).isDirectory())throw new Error(`Product directory is not a directory: ${t}`);const e=o(n.reason,r),c=await stopRuns(t,{...n,reason:e});return{productDir:t,stoppedRuns:c}}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EngineId } from "../../domain/identifiers.js";
|
|
2
|
+
import { type NormalizedConfigContext, type DiscoveryResult } from "../../config/loader/index.js";
|
|
2
3
|
export interface TestkitTypecheckOptions {
|
|
3
4
|
dir?: null | string;
|
|
4
5
|
signal?: AbortSignal;
|
|
@@ -56,6 +57,7 @@ export interface NextRuntimeTypePathsInput {
|
|
|
56
57
|
tsconfigPath: string;
|
|
57
58
|
}
|
|
58
59
|
export declare function runTestkitTypecheck(options?: Readonly<TestkitTypecheckOptions>): Promise<TestkitTypecheckResult>;
|
|
60
|
+
export declare function runTestkitTypecheckForContext(context: NormalizedConfigContext, discovery: DiscoveryResult, options?: Readonly<TestkitTypecheckOptions>): Promise<TestkitTypecheckResult>;
|
|
59
61
|
export declare function requireManagedPublicPackageIdentity(manifest: unknown, packageName: string, expectedVersion: string): void;
|
|
60
62
|
export declare function buildTypecheckCompilerOptions({ engineId, tsconfigPath, extendsPath, rootDir, extraPaths, libs, types, }: Readonly<BuildTypecheckCompilerOptionsInput>): TypecheckCompilerOptions;
|
|
61
63
|
export declare function nextRuntimeTypePaths({ tsconfigPath, serviceDir, }: Readonly<NextRuntimeTypePathsInput>): TypeScriptPathMappings;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{attachErrorDiagnostics as
|
|
2
|
-
`),s}function
|
|
3
|
-
`),c}function
|
|
4
|
-
`)}`)},
|
|
1
|
+
import{attachErrorDiagnostics as w}from"../../kernel/error-serialization.js";import{isPathWithin as T}from"../../process/files/paths.js";import d from"fs";import i from"path";import{createHash as j}from"crypto";import{createRequire as _}from"module";import{fileURLToPath as O}from"url";import{NODE_VITEST_PACKAGE_REQUIREMENTS as C}from"../../engines/installations/package-requirements.js";import{PLAYWRIGHT_PACKAGE_FAMILY_REQUIREMENTS as A}from"../../engines/installations/package-requirements.js";import{hasDiscoveryErrors as $,buildDiscoveryErrorFromDiagnostics as R,detectNextApp as M,loadDiscoveredConfigContext as I,resolveServiceCwd as F}from"../../config/loader/index.js";import{K6_POSTGRES_DRIVER_SPECIFIER as N,K6_POSTGRES_DRIVER_TYPE_FILE as D,K6_RUNTIME_GLOBAL_TYPE_FILE as L}from"../../engines/k6/authoring-type-paths.js";import{managedHostProcessInfrastructureError as G,runManagedHostProcess as U}from"../../process/index.js";import{CONFIGURED_STEP_TESTKIT_MODULE_DIST_PATHS as K}from"../../steps/configured-step-testkit-modules.js";import{findPlaywrightConfigFile as z}from"../../engines/playwright/runtime/config-file.js";const J=600*1e3,h=i.resolve(i.dirname(O(import.meta.url)),"..","..",".."),k=_(i.join(h,"package.json")),x=()=>k("typescript"),q={...B(["@gfdelarue/testkit","@gfdelarue/testkit/config","@gfdelarue/testkit/config/build-assets","@gfdelarue/testkit/config/next-runtime-tsconfig","@gfdelarue/testkit/config/database-steps","@gfdelarue/testkit/drizzle","@gfdelarue/testkit/env","@gfdelarue/testkit/tooling/vitest"]),"@gfdelarue/testkit/k6":["engines","k6","runtime","index.d.ts"],"@gfdelarue/testkit/playwright":["engines","playwright","runtime","index.d.ts"],"@gfdelarue/testkit/node-vitest/data":["engines","node-vitest","runtime","data","index.d.ts"],"@gfdelarue/testkit/node-vitest/contract":["engines","node-vitest","runtime","contract","index.d.ts"],"@gfdelarue/testkit/workspace":["app","inspect","index.d.ts"]};function B(e){return Object.fromEntries(e.map(n=>{const t=K[n],s=t[t.length-1];return[n,[...t.slice(0,-1),`${s}.d.ts`]]}))}export class TestkitTypecheckError extends Error{result;results;constructor(n,t,s){super(`testkit typecheck failed for ${n}`),this.result=t,this.results=s,w(this,{result:t,results:s})}}export async function runTestkitTypecheck(e={}){v(e.timeoutMs);const{context:n,discovery:t}=await I({dir:e.dir});return runTestkitTypecheckForContext(n,t,e)}export async function runTestkitTypecheckForContext(e,n,t={}){const s=v(t.timeoutMs);if($(e.discovery.diagnostics))throw R(e.discovery.diagnostics);const o=e.productDir,r=i.join(o,".testkit","_typecheck");d.mkdirSync(r,{recursive:!0});const a=[];if(e.configFile){const u=H({productDir:o,configFile:e.configFile,outputDir:r});a.push({label:"config",cwd:o,tsconfigPath:u})}const c=n.files.map(u=>({engineId:u.engineId,path:u.path})),p=new Set(n.files.filter(u=>u.engineId==="playwright").map(u=>u.service));for(const u of e.configs){if(!p.has(u.name))continue;const y=z(F(o,u.testkit.local?.cwd));y&&c.push({engineId:"playwright",path:y})}const f=W(o,c);for(const u of f){const y=Y({...u,outputDir:r,productDir:o});a.push({label:X(u.engineId,u.scopeName),cwd:u.cwd,tsconfigPath:y})}const g=[];for(const u of a){const y=await ne(u,s,t.signal);if(g.push(y),!y.ok)throw new TestkitTypecheckError(u.label,y,g)}return{ok:!0,productDir:o,results:g}}function H({productDir:e,configFile:n,outputDir:t}){const s=i.join(t,"config.tsconfig.json"),o=P(e),r={...o?{extends:l(s,o)}:{},compilerOptions:buildTypecheckCompilerOptions({tsconfigPath:s,extendsPath:o,libs:["ES2022"],rootDir:l(s,e),types:["node"]}),files:[l(s,n)],include:[],exclude:[m(s,e,"node_modules"),m(s,e,"dist"),m(s,e,".testkit")]};return d.writeFileSync(s,`${JSON.stringify(r,null,2)}
|
|
2
|
+
`),s}function Y({productDir:e,cwd:n,engineId:t,extendsPath:s,files:o,outputDir:r,scopeName:a}){const c=i.join(r,`${t}.${te(a)}.tsconfig.json`),p=t==="playwright",f=p&&M(n),g={...s?{extends:l(c,s)}:{},compilerOptions:{...buildTypecheckCompilerOptions({engineId:t,tsconfigPath:c,extendsPath:s,libs:Z(t),...f?{extraPaths:nextRuntimeTypePaths({tsconfigPath:c,serviceDir:n})}:{},types:Q(t)}),...p?{allowJs:!0,checkJs:!1}:{}},files:[...ae(t,c),...o.map(u=>l(c,u))],include:f?[...d.existsSync(i.join(n,"next-env.d.ts"))?[l(c,i.join(n,"next-env.d.ts"))]:[],m(c,n,"tests/playwright-fixtures/**/*.ts"),m(c,n,"tests/playwright-fixtures/**/*.tsx"),m(c,n,"tests/playwright-fixtures/**/*.mts"),m(c,n,".next/types/**/*.ts"),m(c,n,".next/dev/types/**/*.ts"),m(c,e,".next-testkit/**/dist/types/**/*.ts"),m(c,e,".next-testkit/**/dist/dev/types/**/*.ts")]:[],exclude:["node_modules",".next/cache",".next/dev"]};return d.writeFileSync(c,`${JSON.stringify(g,null,2)}
|
|
3
|
+
`),c}function W(e,n){const t=new Map,s=new Map;for(const o of n){const r=i.resolve(e,o.path),a=[o.engineId,r].join("\0");s.set(a,{...o,path:r})}for(const o of s.values()){const r=o.path,a=o.engineId==="playwright"?S(i.dirname(r),e,["tsconfig.playwright.json"]):null,c=a??S(i.dirname(r),e),p=c?i.dirname(c):e,f=ee(e,p),g=`${o.engineId}\0${c??""}\0${f}`,u=t.get(g);if(u){u.files.push(r);continue}t.set(g,{cwd:p,engineId:o.engineId,extendsPath:c,files:[r,...a?V(a):[]],scopeName:f})}return[...t.values()].map(o=>({...o,files:[...new Set(o.files)].sort((r,a)=>r.localeCompare(a))})).sort((o,r)=>b(o.engineId)-b(r.engineId)||o.scopeName.localeCompare(r.scopeName))}function V(e){const n=x(),t=o=>{throw new Error(`${e}: ${n.flattenDiagnosticMessageText(o.messageText,`
|
|
4
|
+
`)}`)},s=n.getParsedCommandLineOfConfigFile(e,{},{...n.sys,onUnRecoverableConfigFileDiagnostic:t});for(const o of s?.errors??[])o.code!==18002&&o.code!==18003&&t(o);return s?.fileNames??[]}function Q(e){switch(e){case"k6":return["k6","xk6-sql"];case"node-vitest":case"playwright":return["node"]}}function Z(e){return e==="playwright"?["ES2022","DOM","DOM.Iterable"]:["ES2022"]}function b(e){return["k6","node-vitest","playwright"].indexOf(e)}function X(e,n){return n==="repo"?e:`${e}:${n}`}function ee(e,n){const t=i.relative(e,n);return t===""?"repo":t.split(i.sep).join("/")}function te(e){const n=e.replace(/[^a-zA-Z0-9_-]+/g,"-");if(n===e)return e;const t=j("sha256").update(e).digest("hex").slice(0,12);return`${n}-${t}`}function ne({label:e,cwd:n,tsconfigPath:t},s,o){const r=se(n);return U({args:[...r.args,"--noEmit","-p",t],cwd:n,displayLabel:`Testkit typecheck ${e}`,env:process.env,executable:r.command,signal:o,timeoutMs:s}).then(a=>{const c=G(`Testkit typecheck ${e}`,a);if(c)throw c;return{label:e,tsconfigPath:t,ok:a.exitCode===0,exitCode:a.exitCode,stdout:a.stdout,stderr:a.stderr}})}function v(e){const n=e??J;if(!Number.isSafeInteger(n)||n<=0)throw new Error("Testkit typecheck timeout must be a positive safe integer in milliseconds");return n}function se(e){let n=e;for(;;){const s=i.join(n,"node_modules","typescript","bin","tsc");if(d.existsSync(s))return{command:process.execPath,args:[s]};const o=i.dirname(n);if(o===n)break;n=o}const t=i.join(h,"node_modules","typescript","bin","tsc");if(d.existsSync(t))return{command:process.execPath,args:[t]};throw new Error(`TypeScript compiler not found from ${e} upward`)}function P(e,n=["tsconfig.json","tsconfig.build.json"]){for(const t of n){const s=i.join(e,t);if(d.existsSync(s))return s}return null}function S(e,n,t){const s=i.resolve(n);let o=i.resolve(e);for(;;){if(!T(s,o))return null;const r=P(o,t);if(r)return r;if(o===s)return null;const a=i.dirname(o);if(a===o)return null;o=a}}function l(e,n){const t=i.relative(i.dirname(e),n).split(i.sep).join("/");return t.startsWith("../")||i.isAbsolute(t)?t:`./${t}`}function m(e,n,t){return i.join(l(e,n),t).split(i.sep).join("/")}function oe(e){const n={};for(const[t,s]of Object.entries(q))n[t]=[l(e,i.join(h,"dist",...s))];return n}const re=Object.freeze({packages:Object.freeze(C.filter(({authoringResolution:e})=>e==="package-exports").map(({packageName:e})=>Object.freeze({name:e}))),supportedExports:"exact-manifest-exports"}),ie=Object.freeze({packages:Object.freeze(A.map(({packageName:e})=>Object.freeze({name:e}))),supportedExports:"exact-manifest-exports"});function ce(e,n){const t=pe(e);if(t===null)return{};const s={},o=l(n,i.join(h,"dist","app","typecheck","unavailable.d.ts"));for(const r of t.packages){const a=le(r.name),c=fe(r.name,a);s[`${r.name}/*`]=[o];for(const p of Object.keys(c).sort()){if(p!=="."&&!p.startsWith("./")||p.includes("*"))continue;const f=p==="."?r.name:`${r.name}${p.slice(1)}`,g=ge(f,r.name,a);s[f]=[g===null?o:l(n,g)]}}return s}function ae(e,n){return e==="k6"?[l(n,i.join(h,"dist",...L))]:[]}function ue(e,n){return e==="k6"?{[N]:[l(n,i.join(h,"dist",...D))]}:{}}function pe(e){switch(e){case"node-vitest":return re;case"playwright":return ie;case"k6":case void 0:return null}}function le(e){const n=de(e);let t;try{t=k.resolve(`${e}/package.json`)}catch(r){throw new Error(`Testkit typecheck requires managed public package ${e}`,{cause:r})}const s=d.realpathSync.native(i.dirname(t)),o=JSON.parse(d.readFileSync(i.join(s,"package.json"),"utf8"));return requireManagedPublicPackageIdentity(o,e,n),s}export function requireManagedPublicPackageIdentity(e,n,t){if(!E(e)||e.name!==n||e.version!==t)throw new Error(`Testkit managed public package has invalid identity: expected ${n}@${t}`)}function de(e){const n=JSON.parse(d.readFileSync(i.join(h,"package.json"),"utf8"));if(!E(n)||!E(n.dependencies))throw new Error("Testkit package has invalid dependency metadata");const t=n.dependencies[e];if(typeof t!="string"||!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(t))throw new Error(`Testkit managed public package ${e} must be an exact root dependency`);return t}function E(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function fe(e,n){const t=JSON.parse(d.readFileSync(i.join(n,"package.json"),"utf8"));if(typeof t!="object"||t===null||Array.isArray(t)||!("name"in t)||t.name!==e||!("exports"in t)||typeof t.exports!="object"||t.exports===null||Array.isArray(t.exports))throw new Error(`Testkit managed public package has invalid exports: ${e}`);return t.exports}function ge(e,n,t){const s=x(),o=s.resolveModuleName(e,i.join(h,"__testkit_managed_public_types__.ts"),{module:s.ModuleKind.ESNext,moduleResolution:s.ModuleResolutionKind.Bundler,resolveJsonModule:!0,target:s.ScriptTarget.ES2022},s.sys).resolvedModule;if(o===void 0)return null;const r=d.realpathSync.native(o.resolvedFileName);if(!T(t,r))throw new Error(`Managed public package type ${e} resolved outside ${n}`);return r}export function buildTypecheckCompilerOptions({engineId:e,tsconfigPath:n,extendsPath:t,rootDir:s,extraPaths:o={},libs:r,types:a}){const p={...t?ye({tsconfigPath:n,extendsPath:t}):{},...o,...oe(n),...ce(e,n),...ue(e,n)};return{baseUrl:null,lib:[...r],module:"ESNext",moduleResolution:"Bundler",noEmit:!0,...s?{rootDir:s}:{},target:"ES2022",typeRoots:me(a,n),types:[...a],paths:p}}function me(e,n){const t=new Set;for(const s of e){if(s==="k6"||s==="xk6-sql"){const c=i.join(h,"dist","engines","k6","runtime","ambient-types");if(!d.existsSync(i.join(c,s,"index.d.ts")))throw new Error(`Testkit typecheck requires bundled ${s} authoring types; rebuild the installed Testkit package`);t.add(c);continue}const o=he(s);let r;try{r=k.resolve(`${o}/package.json`)}catch(c){throw new Error(`Testkit typecheck requires installed type package ${o}`,{cause:c})}const a=i.dirname(r);t.add(i.dirname(a))}return[...t].map(s=>l(n,s))}function he(e){return e==="node"||e==="k6"?`@types/${e}`:e}function ye({tsconfigPath:e,extendsPath:n}){const t=x(),s=t.getParsedCommandLineOfConfigFile(n,{},{...t.sys,onUnRecoverableConfigFileDiagnostic:()=>{}}),o=s?.options.paths;if(!o||Object.keys(o).length===0)return{};const r=s.options.baseUrl||(typeof s.options.pathsBasePath=="string"?s.options.pathsBasePath:i.dirname(n)),a={};for(const[c,p]of Object.entries(o))a[c]=p.map(f=>l(e,i.resolve(r,f)));return a}export function nextRuntimeTypePaths({tsconfigPath:e,serviceDir:n}){const t=ke(n,[i.join("next","types.d.ts"),i.join("next","types.js")]);if(!t)return{};const s=l(e,t);return{"next/types":[s],"next/types.js":[s]}}function ke(e,n){let t=i.resolve(e);for(;;){for(const o of n){const r=i.join(t,"node_modules",o);if(d.existsSync(r))return r}const s=i.dirname(t);if(s===t)return null;t=s}}
|