@gfdelarue/testkit 0.5.0 → 0.6.1
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 +590 -165
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/bugs/batch.js +1 -0
- package/dist/app/bugs/ci-report.js +2 -2
- package/dist/app/bugs/ci-summary.js +3 -3
- package/dist/app/bugs/draft.js +2 -2
- package/dist/app/bugs/file.js +1 -1
- package/dist/app/bugs/guide.js +1 -1
- package/dist/app/bugs/record.js +2 -2
- package/dist/app/bugs/run-reading.js +1 -1
- package/dist/app/bugs/store.js +1 -1
- package/dist/app/campaigns/index.js +2 -2
- package/dist/app/campaigns/summary.js +1 -1
- package/dist/app/cleanup/index.js +1 -1
- package/dist/app/db-schema/prepare.js +1 -1
- package/dist/app/db-schema/refresh.js +1 -1
- package/dist/app/db-schema/verify.js +1 -1
- package/dist/app/destroy/index.js +1 -1
- package/dist/app/discover/index.js +2 -2
- package/dist/app/environments/config-driven.js +1 -1
- package/dist/app/environments/file-setup.js +1 -1
- package/dist/app/environments/routing.js +1 -1
- package/dist/app/environments/static-values.js +1 -1
- package/dist/app/environments/supplied.js +1 -1
- package/dist/app/inspect/index.js +1 -1
- package/dist/app/lint/native.js +1 -1
- package/dist/app/lint/options.js +1 -1
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/run/engine-orchestration.js +1 -1
- package/dist/app/run/environment.js +1 -1
- package/dist/app/run/live-run.js +1 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/planning.js +1 -1
- package/dist/app/run/request.js +1 -1
- package/dist/app/run/results.js +1 -1
- package/dist/app/run/run-finalization.js +1 -1
- package/dist/app/run/run-finisher.js +1 -0
- package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
- package/dist/app/run/setup/runtime-node-setup.js +2 -2
- package/dist/app/run/task-batches.js +1 -1
- package/dist/app/run/worker-loop.js +1 -1
- package/dist/app/status/model.js +1 -1
- package/dist/app/status/runs.js +1 -1
- package/dist/app/stop/index.js +1 -1
- package/dist/app/tracker/index.js +1 -1
- package/dist/app/tracker/token.js +2 -2
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-flags.js +1 -1
- package/dist/cli/commands/artifacts.js +2 -2
- package/dist/cli/commands/bug/file.js +3 -2
- package/dist/cli/commands/discover.js +1 -1
- package/dist/cli/commands/guard/unsafe-entrypoint.js +1 -1
- package/dist/cli/commands/run.js +2 -2
- package/dist/cli/commands/typecheck.js +1 -1
- package/dist/cli/commands/workspace.js +1 -1
- package/dist/cli/framework/help.js +5 -5
- package/dist/cli/framework/parser.js +1 -1
- package/dist/cli/json-errors.js +1 -1
- package/dist/cli/renderers/bug/text.js +1 -1
- package/dist/cli/renderers/run/formatting.js +1 -1
- package/dist/config/authoring/database-fixture-discovery.js +1 -0
- package/dist/config/authoring/index.js +1 -1
- package/dist/config/loader/config-loader.js +1 -1
- package/dist/config/loader/decode-discovery.js +1 -1
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery-result.js +1 -0
- package/dist/config/loader/discovery.js +2 -2
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/groups.js +2 -1
- package/dist/config/loader/index.js +1 -1
- package/dist/config/loader/load.js +1 -1
- package/dist/config/loader/managed-configs.js +1 -1
- package/dist/config/loader/owned-discovery.js +1 -1
- package/dist/config/loader/runtime-isolation.js +1 -1
- package/dist/config/loader/skip-config.js +1 -1
- package/dist/config/loader/test-selection.js +1 -1
- package/dist/database/admin.js +5 -2
- package/dist/database/call-caps.js +1 -0
- package/dist/database/cleanup.js +1 -1
- package/dist/database/configured-steps.js +1 -1
- package/dist/database/constants.js +1 -1
- package/dist/database/docker-endpoint.js +1 -0
- package/dist/database/docker-metadata.js +2 -2
- package/dist/database/docker.js +5 -5
- package/dist/database/index.js +2 -1
- package/dist/database/lifecycle-cleanup.js +1 -1
- package/dist/database/local-postgres.js +4 -1
- package/dist/database/naming.js +1 -1
- package/dist/database/operation-deadline.js +1 -0
- package/dist/database/postgres/connection-check.js +1 -1
- package/dist/database/postgres/index.js +2 -2
- package/dist/database/postgres/owned-exec.js +27 -0
- package/dist/database/postgres/protocol.js +2 -2
- package/dist/database/provider.js +1 -0
- package/dist/database/roles.js +2 -2
- package/dist/database/schema-operations.js +5 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/steps.js +1 -1
- package/dist/database/template-steps.js +4 -4
- package/dist/domain/identifiers.js +1 -1
- package/dist/domain/retired-test-types.js +1 -1
- package/dist/domain/suite-selection.js +1 -1
- package/dist/domain/test-types.js +1 -1
- package/dist/engines/adapters/evidence-files.js +1 -1
- package/dist/engines/adapters/execution.js +1 -1
- package/dist/engines/catalog.js +1 -1
- package/dist/engines/definitions.js +1 -1
- package/dist/engines/environment/index.js +1 -1
- package/dist/engines/installations/node-vitest.js +1 -1
- package/dist/engines/installations/playwright.js +1 -1
- package/dist/engines/managed-module-resolution.js +6 -2
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/driver.js +2 -2
- package/dist/engines/node-vitest/adapter/evidence.js +2 -2
- package/dist/engines/node-vitest/adapter/workspace.js +8 -24
- package/dist/engines/node-vitest/inventory/index.js +2 -2
- package/dist/engines/node-vitest/inventory/list-files.js +3 -3
- package/dist/engines/node-vitest/runtime/data/index.js +1 -1
- package/dist/engines/node-vitest/runtime/data/report.js +1 -1
- package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/browser-network-evidence.js +4 -0
- package/dist/engines/playwright/adapter/config.js +16 -6
- package/dist/engines/playwright/adapter/evidence.js +2 -1
- package/dist/engines/playwright/adapter/failure-details.js +2 -2
- package/dist/engines/playwright/adapter/reporter.js +1 -1
- package/dist/engines/playwright/runtime/browser-network.js +1 -0
- package/dist/engines/playwright/runtime/browser-traffic.js +1 -0
- package/dist/engines/playwright/runtime/chromium-environment.js +1 -0
- package/dist/engines/playwright/runtime/emulation.js +1 -0
- package/dist/engines/playwright/runtime/index.js +1 -1
- package/dist/engines/playwright/runtime/managed-test.js +1 -0
- package/dist/engines/playwright/runtime/runtime-input.js +2 -2
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/maintenance.js +1 -1
- package/dist/environment/run-environment.js +1 -1
- package/dist/environment/run-stopping.js +1 -1
- package/dist/environment/runtime-manager.js +1 -1
- package/dist/evidence/artifact-config.js +1 -1
- package/dist/evidence/artifacts.js +5 -2
- package/dist/evidence/bundles.js +2 -1
- package/dist/evidence/compressed-artifacts.js +1 -0
- package/dist/evidence/environment-record.js +1 -1
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/live-run-summary.js +1 -0
- package/dist/evidence/local-paths.js +1 -0
- package/dist/evidence/maintenance.js +1 -0
- package/dist/evidence/operation-timings.js +1 -1
- package/dist/evidence/read-run.js +1 -1
- package/dist/evidence/redaction.js +4 -4
- package/dist/evidence/run-evidence.js +1 -0
- package/dist/evidence/setup-progress.js +1 -1
- package/dist/evidence/storage-inventory.js +1 -1
- package/dist/evidence/storage.js +2 -2
- package/dist/kernel/known-keys.js +1 -0
- package/dist/kernel/redaction-targets.js +1 -1
- package/dist/kernel/redaction.js +7 -4
- package/dist/process/engine-environment-keys.js +1 -0
- package/dist/process/environment-overlay.js +1 -1
- package/dist/process/environment.js +1 -1
- package/dist/process/execa-bundle.json +3 -3
- package/dist/process/files/evidence-writes.js +1 -0
- package/dist/process/files/product-dir.js +1 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/index.js +1 -1
- package/dist/process/lifecycle-fence.js +1 -1
- package/dist/process/lock-progress.js +2 -0
- package/dist/process/locks.js +4 -4
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.js +35 -35
- package/dist/process/recovery.js +1 -1
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/runs/ownership.js +1 -1
- package/dist/process/runs/stop.js +1 -1
- package/dist/process/shutdown.js +1 -1
- package/dist/process/windows-native.js +1 -1
- package/dist/repo/fingerprint-policy.js +1 -1
- package/dist/repo/history.js +7 -4
- package/dist/repo/managed-git.js +1 -1
- package/dist/repo/state.js +4 -4
- package/dist/steps/configured-step-testkit-modules.js +1 -1
- package/dist/steps/environment.js +1 -1
- package/dist/toolchains/index.js +2 -2
- package/dist/toolchains/node-provisioning.js +2 -2
- package/dist/toolchains/vitest/index.js +1 -1
- package/dist/tracker/gitea.js +1 -1
- package/dist/tracker/github.js +1 -1
- package/dist/tracker/http.js +1 -1
- package/dist/tracker/ownership.js +1 -0
- package/dist/tracker/redaction.js +1 -1
- package/dist/tracker/responses.js +1 -1
- package/dist/types/config.d.ts +7 -86
- package/dist/types/environment.d.ts +6 -0
- package/dist/types/node-vitest-data.d.ts +2 -218
- package/dist/types/playwright.d.ts +21 -0
- package/dist/types/workspace.d.ts +30 -14
- package/docs/bug-records.md +19 -18
- package/docs/engines.md +115 -21
- package/docs/release-notes.md +2846 -0
- package/docs/test-types.md +19 -69
- package/package.json +15 -6
- package/testkit-config-schema.md +101 -170
- package/dist/app/lint/rules/data.js +0 -1
- package/dist/config/authoring/runtime.js +0 -1
- package/dist/config/loader/task-policy.js +0 -1
- package/dist/engines/node-vitest/runtime/contract/index.js +0 -1
- package/dist/engines/node-vitest/runtime/data/http.js +0 -1
- package/dist/engines/node-vitest/runtime/data/runner.js +0 -1
- package/dist/environment/lifecycle-artifacts.js +0 -1
- package/dist/types/node-vitest-contract.d.ts +0 -75
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{ownedRuntimeInputModuleUrl as
|
|
1
|
+
import{ownedRuntimeInputModuleUrl as w}from"../../runtime-input-file.js";import b from"node:crypto";import s from"node:fs/promises";import o from"node:path";import{fileURLToPath as P,pathToFileURL as y}from"node:url";import{createManagedNodeModuleResolution as j}from"../../managed-module-resolution.js";import{materializePlaywrightRuntimeInput as v,playwrightRuntimeInputBootstrapSource as T}from"../runtime/runtime-input.js";import{nativeReporterFile as O}from"../../adapters/native-report.js";import{requireConsumerFile as S}from"../../adapters/workspace.js";export class PlaywrightConsumerConfigurationError extends Error{constructor(r){const t=r instanceof Error?r:new Error(String(r));super(t.message,{cause:t}),this.name="PlaywrightConsumerConfigurationError"}}export async function writePlaywrightTaskWorkspace(e){let r;try{F(e.inputs.supportModules),r=await k(e.rootDirectory,e.inputs)}catch(m){throw new PlaywrightConsumerConfigurationError(m)}await s.mkdir(e.taskDirectory,{recursive:!0}),await s.mkdir(e.artifactDirectory,{recursive:!0});const t=b.randomUUID(),n=o.join(e.taskDirectory,`playwright-${t}.config.mjs`),a=o.join(e.taskDirectory,`playwright-${t}.input-bootstrap.mjs`),l=o.join(e.taskDirectory,`playwright-${t}.resolution-bootstrap.cjs`),g=o.join(e.taskDirectory,"testkit-playwright-report.json"),d=o.join(e.taskDirectory,"testkit-playwright-reporters-finished"),p=o.join(e.artifactDirectory,"playwright-output");await s.mkdir(p);const c=o.join(e.artifactDirectory,"playwright-attachments");await s.mkdir(c,{mode:448,recursive:!1});const u=await v({baseUrl:e.baseUrl,databases:e.databases,directory:e.taskDirectory,endpoints:e.endpoints,handles:e.handles,inputs:e.inputs,invocationId:e.invocationId,orchestration:e.orchestration,taskId:e.taskId}),f=O("playwright");return await h(l,playwrightResolutionBootstrapSource(e.managedModuleEntrypoints,{consumerRoots:[e.rootDirectory],trustedImporterFiles:[n,a],trustedInternalEntrypoints:[e.moduleFile,f,P(w)]})),await h(a,T({digest:u.digest,file:u.file,sizeBytes:u.sizeBytes,invocationId:e.invocationId,taskId:e.taskId})),await h(n,x({baseConfigFile:r,baseUrl:e.baseUrl,browserFile:e.browserFile,inputBootstrapFile:a,outputDirectory:p,reporterFile:f,report:{attachmentDirectory:c,identity:{engineId:"playwright",invocationId:e.invocationId,taskId:e.taskId,sourceFile:e.sourceFile},reportFile:g,reportersFinishedFile:d,workspaceDirectory:e.taskDirectory},rootDirectory:e.rootDirectory,sourceFile:e.sourceFile})),{attachmentDirectory:c,configFile:n,inputBootstrapFile:a,outputDirectory:p,reportFile:g,reportersFinishedFile:d,resolutionBootstrapFile:l}}export function playwrightResolutionBootstrapSource(e,r){const t={authoring:i(e.authoring,"authoring"),cli:i(e.cli,"cli"),environment:i(e.environment,"environment"),managedTest:i(e.managedTest,"managedTest"),playwright:i(e.playwright,"playwright"),playwrightCore:i(e.playwrightCore,"playwrightCore"),playwrightTest:i(e.playwrightTest,"playwrightTest"),root:i(e.root,"root"),rootPackageJson:i(e.rootPackageJson,"rootPackageJson")},n={"@playwright/test":t.playwrightTest,playwright:t.playwright,"playwright-core":t.playwrightCore};return j({consumerRoots:r.consumerRoots,direct:{"@gfdelarue/testkit":t.root,"@gfdelarue/testkit/environment":t.environment,"@gfdelarue/testkit/package.json":t.rootPackageJson,"@gfdelarue/testkit/playwright":t.authoring,"@playwright/test":t.managedTest,"playwright/test":t.managedTest},directBridgeRoots:[o.dirname(t.root)],engineLabel:"Playwright",forbiddenConsumerPackagePrefixes:[],packageAnchors:n,reservedPackageNames:["@gfdelarue/testkit"],stateSymbol:"@gfdelarue/testkit/playwright-resolution-hooks/v1",trustedImporterFiles:r.trustedImporterFiles,trustedInternalEntrypoints:r.trustedInternalEntrypoints,verifiedParentlessPackageFiles:{playwright:["lib/common/process.js"]},verifiedProcessEntrypoints:[t.cli]}).bootstrapSource}function i(e,r){if(!o.isAbsolute(e))throw new Error(`Resolved Playwright installation ${r} entrypoint must be absolute`);return e}function x(e){return`
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
|
|
5
|
-
const OPTIONS = ${JSON.stringify({baseConfigUrl:e.baseConfigFile===null?null:
|
|
5
|
+
const OPTIONS = ${JSON.stringify({baseConfigUrl:e.baseConfigFile===null?null:y(e.baseConfigFile).href,baseUrl:e.baseUrl,browserFile:e.browserFile,exactTaskMatchSource:playwrightExactFileFilter(e.sourceFile),originalConfigDirectory:e.baseConfigFile===null?e.rootDirectory:o.dirname(e.baseConfigFile),originalConfigFile:e.baseConfigFile??o.join(e.rootDirectory,"__testkit_playwright_config__.mjs"),inputBootstrapUrl:y(e.inputBootstrapFile).href,outputDirectory:e.outputDirectory,report:e.report,reporterFile:e.reporterFile,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile})};
|
|
6
6
|
const inputBootstrap = await import(OPTIONS.inputBootstrapUrl);
|
|
7
7
|
if (typeof inputBootstrap.installPlaywrightRuntimeInput !== "function")
|
|
8
8
|
throw new Error("Managed Playwright runtime-input bootstrap is invalid");
|
|
@@ -11,6 +11,8 @@ const imported = OPTIONS.baseConfigUrl === null ? { default: {} } : await import
|
|
|
11
11
|
const base = imported.default ?? imported;
|
|
12
12
|
if (!base || typeof base !== "object" || Array.isArray(base))
|
|
13
13
|
throw new Error("supportModules.playwrightConfig must default-export a Playwright config object");
|
|
14
|
+
if (process.env.PW_TEST_REPORTER !== undefined)
|
|
15
|
+
throw new Error("supportModules.playwrightConfig must not set PW_TEST_REPORTER, because Playwright runs that reporter after Testkit's; list it in the reporter option instead");
|
|
14
16
|
const resolver = createRequire(OPTIONS.originalConfigFile);
|
|
15
17
|
const builtInReporters = new Set(["list", "line", "dot", "json", "junit", "null", "github", "html", "blob"]);
|
|
16
18
|
const exactTaskMatch = new RegExp(OPTIONS.exactTaskMatchSource, "u");
|
|
@@ -52,8 +54,9 @@ function normalizeExpect(expect) {
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function normalizeUse(use, label) {
|
|
57
|
+
function normalizeUse(use, label, inheritedSandbox) {
|
|
56
58
|
const value = use && typeof use === "object" && !Array.isArray(use) ? use : {};
|
|
59
|
+
const launchOptions = value.launchOptions && typeof value.launchOptions === "object" ? value.launchOptions : {};
|
|
57
60
|
if (value.browserName !== undefined && value.browserName !== "chromium")
|
|
58
61
|
throw new Error(label + ".browserName must be chromium for the managed Playwright engine");
|
|
59
62
|
if (value.channel !== undefined)
|
|
@@ -65,7 +68,8 @@ function normalizeUse(use, label) {
|
|
|
65
68
|
browserName: "chromium",
|
|
66
69
|
...(OPTIONS.baseUrl === null ? {} : { baseURL: OPTIONS.baseUrl }),
|
|
67
70
|
launchOptions: {
|
|
68
|
-
...
|
|
71
|
+
...launchOptions,
|
|
72
|
+
chromiumSandbox: launchOptions.chromiumSandbox ?? inheritedSandbox ?? true,
|
|
69
73
|
executablePath: OPTIONS.browserFile,
|
|
70
74
|
},
|
|
71
75
|
};
|
|
@@ -133,12 +137,18 @@ function normalizeProject(project, index, supportNames) {
|
|
|
133
137
|
}),
|
|
134
138
|
expect: normalizeExpect(project.expect),
|
|
135
139
|
...(isSupportProject ? {} : { testMatch: exactTaskMatch }),
|
|
136
|
-
use: normalizeUse(project.use, "Playwright config.projects[" + index + "].use"),
|
|
140
|
+
use: normalizeUse(project.use, "Playwright config.projects[" + index + "].use", baseSandbox),
|
|
137
141
|
outputDir: OPTIONS.outputDirectory,
|
|
138
142
|
};
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
const projects = Array.isArray(base.projects) ? base.projects : null;
|
|
146
|
+
const baseSandbox = base.use?.launchOptions?.chromiumSandbox;
|
|
147
|
+
const managedPlaywright = resolver("playwright-core");
|
|
148
|
+
Reflect.set(managedPlaywright, "_defaultLaunchOptions", {
|
|
149
|
+
...managedPlaywright._defaultLaunchOptions,
|
|
150
|
+
chromiumSandbox: baseSandbox ?? true,
|
|
151
|
+
});
|
|
142
152
|
const supportNames = projects === null ? new Set() : supportProjectNames(projects);
|
|
143
153
|
|
|
144
154
|
export default {
|
|
@@ -164,4 +174,4 @@ export default {
|
|
|
164
174
|
fullyParallel: false,
|
|
165
175
|
workers: 1,
|
|
166
176
|
};
|
|
167
|
-
`}export function playwrightExactFileFilter(e){return`^${e.split(/[\\/]+/u).map(r=>r.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")).join("[\\\\/]")}$`}async function
|
|
177
|
+
`}export function playwrightExactFileFilter(e){return`^${e.split(/[\\/]+/u).map(r=>r.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")).join("[\\\\/]")}$`}export function playwrightFileSuiteGrep(e){const r=e.split(/[\\/]+/u).filter(a=>a!=="").map(a=>a.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")),t=r.pop()??"";return`(?:^| )(?:\\.\\.[\\\\/])*${r.reduce((a,l)=>`(?:${a}${l}[\\\\/])?`,"")}${t}(?: |$)`}async function k(e,r){if(!Object.hasOwn(r.supportModules,"playwrightConfig"))return null;const t=r.supportModules.playwrightConfig;if(!o.isAbsolute(t))throw new Error("supportModules.playwrightConfig must be an absolute path");return S(e,t,"supportModules.playwrightConfig")}function F(e){const r=Object.keys(e).filter(t=>t!=="playwrightConfig");if(r.length>0)throw new Error(`Playwright engine does not support support module(s): ${r.join(", ")}`)}async function h(e,r){const t=await s.open(e,"wx",384);try{await t.writeFile(r,"utf8")}finally{await t.close()}}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{toError as ot}from"../../../kernel/error-serialization.js";import{isPathWithin as N}from"../../../process/files/paths.js";import v from"node:fs/promises";import g from"node:path";import{evidenceArtifact as O,evidenceFileAccess as lt,readStableJsonFile as ct,snapshotStableEvidenceFile as U,stableEvidenceFromBuffer as X,writeOwnedEvidenceFile as V,isMissingFileError as ut}from"../../adapters/evidence-files.js";import{materializePlaywrightFailureDetails as pt}from"./failure-details.js";import{nonNegativeInteger as T,nonNegativeNumber as J,optionalNonEmptyString as R,optionalNonNegativeInteger as K,portableRelativePath as Q,portableReportPath as Z,requireNonEmptyString as k,requireRecord as P}from"./evidence-validation.js";import{readNativeReport as dt}from"../../adapters/native-report.js";import{environmentValueMissingFailure as ht,parseEnvironmentValueMissingFailure as q,readEnvironmentValueMissingFailure as ft}from"../../contracts/environment-values.js";import{attachmentLimitError as mt,attachmentIoError as gt,MAX_ATTACHMENT_BYTES as tt,MAX_TOTAL_ATTACHMENT_BYTES as yt}from"./attachments.js";const wt=64*1024*1024,et=1e4,I=512*1024*1024,vt=2*1024*1024,it=128,Pt=".testkit-playwright-evidence",Et=/(?:^|\n)(?:Error:\s*)?No tests found(?:\.|\n|$)/u,Tt=["failed","interrupted","passed","timedout"];class L extends Error{limit;constructor(e){super(Mt(e)),this.limit=e,this.name="PlaywrightOutputArtifactLimitError"}}export async function collectPlaywrightEvidence(t,e=lt){await v.mkdir(t.artifactDirectory,{recursive:!0});const i=await v.realpath(t.artifactDirectory),s=await v.realpath(t.rootDirectory),n=g.join(i,Pt);await v.mkdir(n,{mode:448,recursive:!1});const r=[],o=[];for(const l of["stdout","stderr"]){const y=t[l];if(y.length===0)continue;r.push({stream:l,text:y});const F=X(Buffer.from(y,"utf8")),f=await V(n,`${l}.log`,F.contents);o.push(O(f,F,"text/plain; charset=utf-8","engine.process-output",`playwright-${l}`,`${F.sizeBytes.toString()} bytes`))}let c=null,p=null,m=new Set,h=[];try{const l=await ct(t.reportFile,wt,"Testkit native Playwright report",e);if(l.file===null)c=new Error("Playwright did not produce its Testkit native report");else{p=St(dt(l.value,t.identity).result,t.sourceFile);const y=p.carriesEnvironmentFailures?X(Buffer.from(kt(l.value))):l.file,F=await V(n,"testkit-playwright-report.json",y.contents);o.push(O(F,y,"application/json","engine.testkit-playwright-report","testkit-playwright-report","Testkit native Playwright report")),r.push(...p.logs);const f=await pt(n,p.failureDetails);f!==null&&o.push(f);const u=await At(i,n,s,p.attachments,e);o.push(...u.artifacts),m=u.sourcePaths,h=u.failures}}catch(l){c=ot(l)}let a={artifacts:[],bytes:0,entries:0},d=null;try{a=await _t(i,n,t.outputDirectory,m,e),o.push(...a.artifacts)}catch(l){if(!(l instanceof L))throw l;d=l.limit,r.push({stream:"engine",text:l.message})}const w=bt(p,s,t.sourceFile,h.length),E=d??a;return{attachmentFailures:h,evidence:{artifacts:o,logs:r,metrics:[...Object.entries(p?.stats??{}).map(([l,y])=>({name:l==="duration"?"playwright.duration":`playwright.tests.${l}`,unit:l==="duration"?"milliseconds":"tests",value:y})),...["bytes","entries"].map(l=>({name:`playwright.output.${l}`,unit:l,value:E[l]}))],...p===null?{}:{native:{playwright:{attachmentFailures:h.map(l=>({...l})),specs:p.specs,stats:p.stats}}}},outputLimit:d,report:w,reportError:c}}function bt(t,e,i,s){if(t===null)return{failure:null,status:"failed"};const n=t.environmentFailures.at(0);if(n!==void 0)return{failure:ht(t.environmentFailures.flatMap(c=>ft(c)??[]),n.message),status:"failed"};const r=t.specs.filter((c,p)=>rt(i,t.sourceFiles[p])),o=t.failureMessages.length>0&&t.failureMessages.every(c=>Et.test(c));return t.stats.unexpected>0||t.failureMessages.length>0&&(!o||r.length>0)?{failure:{...t.failureMessages.some(c=>/browserType\.launch(?:PersistentContext)?:/u.test(c)&&/Executable doesn't exist|Host system is missing dependencies|error while loading shared libraries|Failed to launch/u.test(c))?{classification:"infra"}:{},code:"PLAYWRIGHT_SPECS_FAILED",details:{attachmentFailures:s,failOnFlakyTests:t.failOnFlakyTests,reportErrors:t.reportErrors,runStatus:t.runStatus,stats:t.stats,specs:t.specs.slice(0,50)},message:t.failureMessages.at(0)??`${t.stats.unexpected.toString()} Playwright test(s) failed`},status:"failed"}:r.length>0?{failure:null,status:r.every(c=>c.status==="skipped")?"skipped":"passed"}:{failure:{code:"PLAYWRIGHT_NO_TESTS",details:{file:Q(e,i),reportedSpecs:t.specs.slice(0,50),stats:t.stats},message:"Selected Playwright task did not execute any tests"},status:"failed"}}function rt(t,e){return g.relative(g.normalize(t),e)===""}function St(t,e){const i=P(t,"Playwright native report result"),s=Tt.find(f=>f===i.status);if(s===void 0)throw new Error("Playwright native report status is invalid");if(typeof i.failOnFlakyTests!="boolean")throw new Error("Playwright native report failOnFlakyTests must be a boolean");const n=i.failOnFlakyTests,r=Ot(i.stats),o=j(i.errors,"errors"),c=o.length,p=q(i.environmentFailure,"Playwright native report environment failure"),m=p===null?[]:[p];let h=p!==null;const a=[],d=[],w=[],E=new Set,l={expected:0,flaky:0,skipped:0,unexpected:0};for(const f of x(i.specs,"specs")){const u=P(f,"Playwright native report spec"),B=k(u.id,"Playwright native spec.id");if(E.has(B))throw new Error("Playwright native report contains a duplicate test identity");E.add(B);const A=k(u.title,"Playwright native spec.title"),b=j(u.parents,"spec.parents");b.length>0&&(b[0]=Z(b[0]));const D=Z(k(u.file,"Playwright native spec.file")),_=k(u.sourceFile,"Playwright native spec.sourceFile");if(!g.isAbsolute(_)&&!g.win32.isAbsolute(_))throw new Error("Playwright native spec.sourceFile must be absolute");const st=M(u.projectName,"spec.projectName"),S=u.status;if(S!=="expected"&&S!=="flaky"&&S!=="skipped"&&S!=="unexpected")throw new Error("Playwright native spec.status is invalid");l[S]+=1;const C=j(u.failureMessages,"spec.failureMessages"),Y=u.failureStack===null?null:M(u.failureStack,"spec.failureStack"),H=K(u.line,"Playwright native spec.line"),W=K(u.column,"Playwright native spec.column"),G=rt(e,_),$=q(u.environmentFailure,"Playwright native spec environment failure");$!==null&&(h=!0),G&&$!==null&&m.push($),S==="unexpected"&&(o.push(...C),w.push({...$===null?{}:{classification:"infra"},kind:G?"playwright-spec":"playwright-other-file-spec",key:[...b.slice(1),A].filter(Boolean).join(" "),title:A,count:1,message:C[0]??`Playwright reported "${A}" as an unexpected failure`,...b.length===0?{}:{suitePath:b},location:{path:D,...H===null?{}:{line:H},...W===null?{}:{column:W}},...Y===null?{}:{stack:Y}})),d.push(_),a.push({durationMs:J(u.durationMs,"Playwright native spec.durationMs"),retries:T(u.retries,"Playwright native spec.retries"),file:D,projectName:st,status:S,title:[...b,A].filter(Boolean).join(" > ")})}for(const f of["expected","flaky","skipped","unexpected"])if(r[f]!==l[f])throw new Error("Playwright native report stats do not match its cases");const y=x(i.logs,"logs").map(f=>{const u=P(f,"Playwright native report log");if(u.stream!=="stdout"&&u.stream!=="stderr")throw new Error("Playwright native report log stream is invalid");return{stream:u.stream,text:M(u.text,"log.text")}});return{attachments:x(i.attachments,"attachments").map((f,u)=>Ft(f,u,E)),carriesEnvironmentFailures:h,environmentFailures:m,failOnFlakyTests:n,failureDetails:w,failureMessages:o,logs:Rt(y),reportErrors:c,runStatus:s,sourceFiles:d,specs:a,stats:r}}function kt(t){const e=P(t,"Testkit native Playwright report"),i=nt(P(e.result,"Playwright native report result")),s=x(i.specs,"specs").map(n=>nt(P(n,"Playwright native report spec")));return JSON.stringify({...e,result:{...i,specs:s}})}function nt(t){return Object.fromEntries(Object.entries(t).filter(([e])=>e!=="environmentFailure"))}function x(t,e){if(!Array.isArray(t))throw new Error(`Playwright native report ${e} must be an array`);return t}function j(t,e){return x(t,e).map(i=>M(i,e))}function M(t,e){if(typeof t!="string")throw new Error(`Playwright native report ${e} must be a string`);return t}function Ft(t,e,i){const s=P(t,`Playwright attachment[${e.toString()}]`),n=k(s.specId,"Playwright attachment.specId");if(!i.has(n))throw new Error("Playwright native attachment references an unknown test identity");const r=R(s.path),o=s.error===void 0?void 0:xt(s.error);if(s.body!==void 0||r===null&&o===void 0)throw new Error("Playwright attachment must contain a path or an error");return{contentType:R(s.contentType)??"application/octet-stream",name:R(s.name)??`attachment-${e.toString()}`,specId:n,...r===null?{}:{path:r},...o===void 0?{}:{error:o},retry:T(s.retry,"Playwright attachment.retry"),specTitle:k(s.specTitle,"Playwright attachment.specTitle")}}function xt(t){const e=P(t,"Playwright attachment.error");if(e.kind==="io")return{kind:"io",message:k(e.message,"Playwright attachment.error.message")};const i=e.kind;if(i!=="individual"&&i!=="total")throw new Error("Playwright attachment.error has an invalid kind");const s=i==="individual"?tt:yt,n=T(e.observed,"Playwright attachment.error.observed");if(e.limit!==s||n<=s)throw new Error("Playwright attachment.error has an invalid limit");return{kind:i,limit:s,observed:n}}function z(t,e){const{name:i,specId:s,specTitle:n,retry:r}=t,o=e.kind==="io"?e.message:`exceeds its ${e.limit.toString()} byte ${e.kind} limit (observed ${e.observed.toString()} bytes)`;return{code:e.kind==="io"?"PLAYWRIGHT_ATTACHMENT_FAILED":"PLAYWRIGHT_ATTACHMENT_LIMIT",...e.kind==="io"?{classification:"infra"}:{},details:{name:i,specId:s,specTitle:n,retry:r,...e},message:`Playwright attachment "${i}" (${n}, retry ${r.toString()}): ${o}`}}async function At(t,e,i,s,n){const r=[],o=[],c=new Set;let p=0;const m=g.join(e,"attachments");s.length>0&&await v.mkdir(m,{mode:448,recursive:!1});for(const[h,a]of s.entries())try{if(a.error!==void 0){a.path!==void 0&&c.add(g.resolve(i,a.path)),o.push(z(a,a.error));continue}const d=await Nt(a.path,i,t,n);c.add(d.file);const w=T(Number(d.identity.size),"Playwright attachment size"),E=mt(w,p+w);if(E!==null){o.push(z(a,E));continue}const l=await U(d.file,tt,"Playwright attachment",n,d.identity,m,`${h.toString().padStart(4,"0")}-${at(a.name)}${It(a)}`);p+=l.identity.sizeBytes,r.push({path:l.filePath,contentType:a.contentType,kind:"engine.playwright-attachment",name:a.name,summary:`${a.specTitle} retry ${a.retry.toString()}`,metadata:{...l.identity,specId:a.specId,specTitle:a.specTitle,retry:a.retry}})}catch(d){o.push(z(a,gt(d)))}return{artifacts:r,failures:o,sourcePaths:c}}async function _t(t,e,i,s,n){try{const r=await n.lstat(i);if(!r.isDirectory()||r.isSymbolicLink())throw new Error("Playwright output directory must be a non-symbolic-link directory");const o=await v.realpath(i);if(!N(t,o))throw new Error("Playwright output directory escaped the artifact root");const c=await $t(o,s,n),p=[],m=g.join(e,"output");c.candidates.length>0&&await v.mkdir(m,{mode:448,recursive:!1});for(const[h,a]of c.candidates.entries()){const d=await U(a.file,I,"Playwright output file",n,a.identity,m,`${h.toString().padStart(5,"0")}-${at(g.basename(a.file))}${jt(a.file)}`);p.push(O(d.filePath,d.identity,Lt(a.file),"engine.playwright-output",a.relativePath,"Playwright output"))}return{artifacts:p,bytes:c.bytes,entries:c.entries}}catch(r){if(ut(r))return{artifacts:[],bytes:0,entries:0};throw r}}async function $t(t,e,i){const s=[];let n=0,r=0;const o=[{depth:0,directory:t}];for(;o.length>0;){const c=o.pop();if(c===void 0)continue;if(c.depth>it)throw new Error(`Playwright output exceeds the ${it.toString()} level directory depth limit`);const p=c.directory,m=await v.readdir(p,{withFileTypes:!0});m.sort((h,a)=>h.name.localeCompare(a.name));for(const h of m){if(r+=1,r>et)throw new L({bytes:n,entries:r,kind:"entries",limit:et,observed:r});const a=g.join(p,h.name),d=await i.lstat(a);if(d.isSymbolicLink())throw new Error("Playwright output must not contain symbolic links");if(d.isDirectory()){o.push({depth:c.depth+1,directory:a});continue}if(!d.isFile())continue;if(d.size<0n||d.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error("Playwright output file has an invalid size");const w=Number(d.size);if(n+=w,!Number.isSafeInteger(n)||n>I)throw new L({bytes:n,entries:r,kind:"bytes",limit:I,observed:n});e.has(a)||s.push({file:a,identity:d,relativePath:Q(t,a)})}}return{bytes:n,candidates:s,entries:r}}function Mt(t){return t.kind==="bytes"?`Playwright output exceeds its ${t.limit.toString()} byte aggregate limit (observed ${t.observed.toString()} bytes)`:`Playwright output exceeds its ${t.limit.toString()} entry limit (observed ${t.observed.toString()} entries)`}async function Nt(t,e,i,s){if(t===void 0)throw new Error("Playwright path attachment did not declare a path");const n=g.resolve(e,t),r=await s.lstat(n);if(!r.isFile()||r.isSymbolicLink())throw new Error("Playwright attachment path must be a regular non-symbolic-link file");const o=await v.realpath(n);if(!N(e,o)&&!N(i,o))throw new Error("Playwright attachment path must stay within the consumer or artifact root");return{file:o,identity:r}}function Ot(t){const e=P(t,"Playwright report.stats");return{expected:T(e.expected,"Playwright stats.expected"),unexpected:T(e.unexpected,"Playwright stats.unexpected"),flaky:T(e.flaky,"Playwright stats.flaky"),skipped:T(e.skipped,"Playwright stats.skipped"),duration:J(e.duration,"Playwright stats.duration")}}function Rt(t){let e=vt;const i=[];for(const s of t){if(e<=0)break;const r=Buffer.from(s.text,"utf8").subarray(0,e).toString("utf8");r.length>0&&i.push({...s,text:r}),e-=Buffer.byteLength(r)}return e<=0&&i.push({stream:"engine",text:"Playwright report log evidence was truncated at its bounded limit"}),i}function It(t){if(t.path!==void 0){const e=g.extname(t.path);if(/^\.[a-z0-9]{1,12}$/iu.test(e))return e}return t.contentType==="application/json"?".json":t.contentType.startsWith("text/")?".txt":t.contentType==="image/png"?".png":t.contentType==="image/jpeg"?".jpg":".bin"}function Lt(t){const e=g.extname(t).toLowerCase();return e===".json"?"application/json":e===".zip"?"application/zip":e===".png"?"image/png":e===".jpg"||e===".jpeg"?"image/jpeg":e===".webm"?"video/webm":[".log",".txt",".md"].includes(e)?"text/plain; charset=utf-8":"application/octet-stream"}function jt(t){const e=g.extname(t).toLowerCase();return/^\.[a-z0-9]{1,12}$/u.test(e)?e:".bin"}function at(t){const e=t.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"").slice(0,80);return e.length>0?e:"attachment"}
|
|
1
|
+
import{toError as ht}from"../../../kernel/error-serialization.js";import{isPathWithin as D}from"../../../process/files/paths.js";import T from"node:fs/promises";import w from"node:path";import{evidenceArtifact as I,evidenceFileAccess as mt,readStableEvidenceFile as yt,readStableJsonFile as wt,snapshotStableEvidenceFile as Z,stableEvidenceFromBuffer as z,writeOwnedEvidenceFile as H,isMissingFileError as gt}from"../../adapters/evidence-files.js";import{materializePlaywrightFailureDetails as Et}from"./failure-details.js";import{nonNegativeInteger as b,nonNegativeNumber as q,optionalNonEmptyString as W,optionalNonNegativeInteger as tt,portableRelativePath as U,portableReportPath as et,requireNonEmptyString as x,requireRecord as S}from"./evidence-validation.js";import{readNativeReport as vt}from"../../adapters/native-report.js";import{environmentValueMissingFailure as Pt,parseEnvironmentValueMissingFailure as rt,readEnvironmentValueMissingFailure as Tt}from"../../contracts/environment-values.js";import{BROWSER_NETWORK_ATTACHMENT as St}from"../runtime/browser-network.js";import{BROWSER_NETWORK_ARTIFACT_KIND as kt,BROWSER_NETWORK_REPORT_KIND as bt,MAX_BROWSER_NETWORK_RECORD_BYTES as Ft,readBrowserNetworkRecord as xt,renderBrowserNetwork as _t}from"./browser-network-evidence.js";import{attachmentLimitError as Rt,attachmentIoError as it,MAX_ATTACHMENT_BYTES as nt,MAX_TOTAL_ATTACHMENT_BYTES as At}from"./attachments.js";const Ot=64*1024*1024,st=1e4,Y=512*1024*1024,Nt=2*1024*1024,at=128,Mt=".testkit-playwright-evidence",$t=/(?:^|\n)(?:Error:\s*)?No tests found(?:\.|\n|$)/u,It=/browserType\.launch(?:PersistentContext)?:/u,Bt=/browserType\.launch(?:PersistentContext|Server)?:|Failed to launch browser/u,Lt=/Chromium sandboxing failed!|No usable sandbox!|crbug\.com\/(?:357670|638180)/u,jt="Testkit starts managed Chromium with its sandbox on, and this host can't start it. To run without the sandbox here, set use.launchOptions.chromiumSandbox to false in playwright.config.ts, or in one file with test.use.",Ct=["failed","interrupted","passed","timedout"],Dt=["failed","interrupted","passed","skipped","timedOut"],zt={expected:"passed",flaky:"flaky",skipped:"skipped",unexpected:"failed"};class X extends Error{limit;constructor(e){super(Zt(e)),this.limit=e,this.name="PlaywrightOutputArtifactLimitError"}}export async function collectPlaywrightEvidence(t,e=mt){await T.mkdir(t.artifactDirectory,{recursive:!0});const r=await T.realpath(t.artifactDirectory),a=await T.realpath(t.rootDirectory),n=w.join(r,Mt);await T.mkdir(n,{mode:448,recursive:!1});const s=[],o=[];for(const c of["stdout","stderr"]){const g=t[c];if(g.length===0)continue;s.push({stream:c,text:g});const F=z(Buffer.from(g,"utf8")),_=await H(n,`${c}.log`,F.contents);o.push(I(_,F,"text/plain; charset=utf-8","engine.process-output",`playwright-${c}`,`${F.sizeBytes.toString()} bytes`))}let h=null,u=null,p=new Set,f=[];try{const c=await wt(t.reportFile,Ot,"Testkit native Playwright report",e);if(c.file===null)h=new Error("Playwright did not produce its Testkit native report");else{u=Ut(vt(c.value,t.identity).result,t.sourceFile);const g=u.carriesEnvironmentFailures?z(Buffer.from(Yt(c.value))):c.file,F=await H(n,"testkit-playwright-report.json",g.contents);o.push(I(F,g,"application/json","engine.testkit-playwright-report","testkit-playwright-report","Testkit native Playwright report")),s.push(...u.logs);const _=await Et(n,u.failureDetails);_!==null&&o.push(_);const y=await Kt(r,n,a,U(a,t.sourceFile),u,e),d=await Vt(r,n,a,u.attachments.filter(R=>!lt(R)),e);o.push(...y.artifacts,...d.artifacts),p=new Set([...y.sourcePaths,...d.sourcePaths]),f=[...y.failures,...d.failures]}}catch(c){h=ht(c)}let i={artifacts:[],bytes:0,entries:0},l=null;try{i=await Jt(r,n,t.outputDirectory,p,e),o.push(...i.artifacts)}catch(c){if(!(c instanceof X))throw c;l=c.limit,s.push({stream:"engine",text:c.message})}const m=Ht(u,a,t.sourceFile,f.length,await Wt(t.reportersFinishedFile)),E=l??i;return{attachmentFailures:f,evidence:{artifacts:o,logs:s,metrics:[...Object.entries(u?.stats??{}).map(([c,g])=>({name:c==="duration"?"playwright.duration":`playwright.tests.${c}`,unit:c==="duration"?"milliseconds":"tests",value:g})),...["bytes","entries"].map(c=>({name:`playwright.output.${c}`,unit:c,value:E[c]}))],...u===null?{}:{native:{playwright:{attachmentFailures:f.map(c=>({...c})),specs:u.specs,stats:u.stats}}}},outputLimit:l,report:m,reportError:h}}function Ht(t,e,r,a,n){if(t===null)return{failure:null,status:"failed"};const s=t.environmentFailures.at(0);if(s!==void 0)return{failure:Pt(t.environmentFailures.flatMap(p=>Tt(p)??[]),s.message),status:"failed"};const o=t.specs.filter((p,f)=>ot(r,t.sourceFiles[f])),h=t.failureMessages.length>0&&t.failureMessages.every(p=>$t.test(p)),u=t.failOnFlakyTests?t.stats.flaky:0;if(t.stats.unexpected+u>0||t.failureMessages.length>0&&(!h||o.length>0)){const p=t.failureMessages.find(f=>Bt.test(f)&&Lt.test(f));return{failure:{...p!==void 0||t.failureMessages.some(f=>It.test(f)&&/Executable doesn't exist|Host system is missing dependencies|error while loading shared libraries|Failed to launch/u.test(f))?{classification:"infra"}:{},code:"PLAYWRIGHT_SPECS_FAILED",details:{attachmentFailures:a,failOnFlakyTests:t.failOnFlakyTests,reportErrors:t.reportErrors,reportersFinished:n,runStatus:t.runStatus,stats:t.stats,specs:t.specs.slice(0,50)},message:p===void 0?t.failureMessages.at(0)??`${(t.stats.unexpected+u).toString()} Playwright test(s) failed`:`${jt}
|
|
2
|
+
${p}`},status:"failed"}}return o.length>0?{failure:null,status:o.every(p=>p.status==="skipped")?"skipped":"passed"}:{failure:{code:"PLAYWRIGHT_NO_TESTS",details:{file:U(e,r),reportedSpecs:t.specs.slice(0,50),stats:t.stats},message:"Selected Playwright task did not execute any tests"},status:"failed"}}async function Wt(t){try{return(await T.lstat(t)).isFile()}catch{return!1}}function ot(t,e){return w.relative(w.normalize(t),e)===""}function Ut(t,e){const r=S(t,"Playwright native report result"),a=Ct.find(y=>y===r.status);if(a===void 0)throw new Error("Playwright native report status is invalid");if(typeof r.failOnFlakyTests!="boolean")throw new Error("Playwright native report failOnFlakyTests must be a boolean");const n=r.failOnFlakyTests,s=qt(r.stats),o=G(r.errors,"errors"),h=o.length,u=rt(r.environmentFailure,"Playwright native report environment failure"),p=u===null?[]:[u];let f=u!==null;const i=[],l=[],m=[],E=new Set,c=new Map,g={expected:0,flaky:0,skipped:0,unexpected:0};for(const y of A(r.specs,"specs")){const d=S(y,"Playwright native report spec"),R=x(d.id,"Playwright native spec.id");if(E.has(R))throw new Error("Playwright native report contains a duplicate test identity");E.add(R);const k=x(d.title,"Playwright native spec.title"),v=G(d.parents,"spec.parents");v.length>0&&(v[0]=et(v[0]));const j=et(x(d.file,"Playwright native spec.file")),O=x(d.sourceFile,"Playwright native spec.sourceFile");if(!w.isAbsolute(O)&&!w.win32.isAbsolute(O))throw new Error("Playwright native spec.sourceFile must be absolute");const dt=B(d.projectName,"spec.projectName"),P=d.status;if(P!=="expected"&&P!=="flaky"&&P!=="skipped"&&P!=="unexpected")throw new Error("Playwright native spec.status is invalid");const K=Dt.find(ft=>ft===d.expectedStatus);if(K===void 0)throw new Error("Playwright native spec.expectedStatus is invalid");g[P]+=1;const V=G(d.failureMessages,"spec.failureMessages"),J=d.failureStack===null?null:B(d.failureStack,"spec.failureStack"),N=tt(d.line,"Playwright native spec.line"),M=tt(d.column,"Playwright native spec.column"),C=ot(e,O),$=rt(d.environmentFailure,"Playwright native spec environment failure");$!==null&&(f=!0),C&&$!==null&&p.push($),P==="unexpected"||P==="flaky"&&n?(o.push(...V),m.push({...$===null?{}:{classification:"infra"},kind:C?"playwright-spec":"playwright-other-file-spec",key:[...v.slice(1),k].filter(Boolean).join(" "),title:k,count:1,message:V[0]??`Playwright reported "${k}" as an unexpected failure`,...v.length===0?{}:{suitePath:v},location:{path:j,...N===null?{}:{line:N},...M===null?{}:{column:M}},...J===null?{}:{stack:J}})):C&&P==="skipped"&&K!=="skipped"&&m.push({kind:"playwright-not-run",key:[...v.slice(1),k].filter(Boolean).join(" "),title:k,message:`Playwright did not run "${k}", which was expected to run`,...v.length===0?{}:{suitePath:v},location:{path:j,...N===null?{}:{line:N},...M===null?{}:{column:M}}}),l.push(O);const Q=b(d.retries,"Playwright native spec.retries");c.set(R,{retry:Q,status:zt[P],title:[...v.slice(1),k].join(" \u203A ")}),i.push({durationMs:q(d.durationMs,"Playwright native spec.durationMs"),retries:Q,file:j,projectName:dt,status:P,title:[...v,k].filter(Boolean).join(" > ")})}for(const y of["expected","flaky","skipped","unexpected"])if(s[y]!==g[y])throw new Error("Playwright native report stats do not match its cases");const F=A(r.logs,"logs").map(y=>{const d=S(y,"Playwright native report log");if(d.stream!=="stdout"&&d.stream!=="stderr")throw new Error("Playwright native report log stream is invalid");return{stream:d.stream,text:B(d.text,"log.text")}});return{attachments:A(r.attachments,"attachments").map((y,d)=>Xt(y,d,E)),carriesEnvironmentFailures:f,environmentFailures:p,failOnFlakyTests:n,failureDetails:m,failureMessages:o,logs:te(F),reportErrors:h,runStatus:a,sourceFiles:l,specs:i,stats:s,tests:c}}function Yt(t){const e=S(t,"Testkit native Playwright report"),r=ct(S(e.result,"Playwright native report result")),a=A(r.specs,"specs").map(n=>ct(S(n,"Playwright native report spec")));return JSON.stringify({...e,result:{...r,specs:a}})}function ct(t){return Object.fromEntries(Object.entries(t).filter(([e])=>e!=="environmentFailure"))}function A(t,e){if(!Array.isArray(t))throw new Error(`Playwright native report ${e} must be an array`);return t}function G(t,e){return A(t,e).map(r=>B(r,e))}function B(t,e){if(typeof t!="string")throw new Error(`Playwright native report ${e} must be a string`);return t}function Xt(t,e,r){const a=S(t,`Playwright attachment[${e.toString()}]`),n=x(a.specId,"Playwright attachment.specId");if(!r.has(n))throw new Error("Playwright native attachment references an unknown test identity");const s=W(a.path),o=a.error===void 0?void 0:Gt(a.error);if(a.body!==void 0||s===null&&o===void 0)throw new Error("Playwright attachment must contain a path or an error");return{contentType:W(a.contentType)??"application/octet-stream",name:W(a.name)??`attachment-${e.toString()}`,specId:n,...s===null?{}:{path:s},...o===void 0?{}:{error:o},retry:b(a.retry,"Playwright attachment.retry"),specTitle:x(a.specTitle,"Playwright attachment.specTitle")}}function Gt(t){const e=S(t,"Playwright attachment.error");if(e.kind==="io")return{kind:"io",message:x(e.message,"Playwright attachment.error.message")};const r=e.kind;if(r!=="individual"&&r!=="total")throw new Error("Playwright attachment.error has an invalid kind");const a=r==="individual"?nt:At,n=b(e.observed,"Playwright attachment.error.observed");if(e.limit!==a||n<=a)throw new Error("Playwright attachment.error has an invalid limit");return{kind:r,limit:a,observed:n}}function L(t,e){const{name:r,specId:a,specTitle:n,retry:s}=t,o=e.kind==="io"?e.message:`exceeds its ${e.limit.toString()} byte ${e.kind} limit (observed ${e.observed.toString()} bytes)`;return{code:e.kind==="io"?"PLAYWRIGHT_ATTACHMENT_FAILED":"PLAYWRIGHT_ATTACHMENT_LIMIT",...e.kind==="io"?{classification:"infra"}:{},details:{name:r,specId:a,specTitle:n,retry:s,...e},message:`Playwright attachment "${r}" (${n}, retry ${s.toString()}): ${o}`}}function lt(t){return t.name===St&&t.error===void 0}async function Kt(t,e,r,a,n,s){const o=[],h=new Set,u=[];for(const i of n.attachments.filter(lt))try{const l=await ut(i.path,r,t,s);h.add(l.file);const m=n.tests.get(i.specId);if(m===void 0||i.retry!==m.retry)continue;const{contents:E}=await yt(l.file,Ft,"Browser network record",s,l.identity);u.push({title:m.title,status:m.status,retry:m.retry,...xt(E)})}catch(l){o.push(L(i,it(l)))}if(u.length===0)return{artifacts:[],failures:o,sourcePaths:h};const p=_t(a,u),f=[];for(const[i,l,m,E]of[["browser-network",p.json,"application/json",kt],["browser-network-report",p.markdown,"text/markdown",bt]]){const c=z(Buffer.from(l,"utf8")),g=await H(e,`${i}${m==="text/markdown"?".md":".json"}`,c.contents);f.push(I(g,c,m,E,i,p.summary))}return{artifacts:f,failures:o,sourcePaths:h}}async function Vt(t,e,r,a,n){const s=[],o=[],h=new Set;let u=0;const p=w.join(e,"attachments");a.length>0&&await T.mkdir(p,{mode:448,recursive:!1});for(const[f,i]of a.entries())try{if(i.error!==void 0){i.path!==void 0&&h.add(w.resolve(r,i.path)),o.push(L(i,i.error));continue}const l=await ut(i.path,r,t,n);h.add(l.file);const m=b(Number(l.identity.size),"Playwright attachment size"),E=Rt(m,u+m);if(E!==null){o.push(L(i,E));continue}const c=await Z(l.file,nt,"Playwright attachment",n,l.identity,p,`${f.toString().padStart(4,"0")}-${pt(i.name)}${ee(i)}`);u+=c.identity.sizeBytes,s.push({path:c.filePath,contentType:i.contentType,kind:"engine.playwright-attachment",name:i.name,summary:`${i.specTitle} retry ${i.retry.toString()}`,metadata:{...c.identity,specId:i.specId,specTitle:i.specTitle,retry:i.retry}})}catch(l){o.push(L(i,it(l)))}return{artifacts:s,failures:o,sourcePaths:h}}async function Jt(t,e,r,a,n){try{const s=await n.lstat(r);if(!s.isDirectory()||s.isSymbolicLink())throw new Error("Playwright output directory must be a non-symbolic-link directory");const o=await T.realpath(r);if(!D(t,o))throw new Error("Playwright output directory escaped the artifact root");const h=await Qt(o,a,n),u=[],p=w.join(e,"output");h.candidates.length>0&&await T.mkdir(p,{mode:448,recursive:!1});for(const[f,i]of h.candidates.entries()){const l=await Z(i.file,Y,"Playwright output file",n,i.identity,p,`${f.toString().padStart(5,"0")}-${pt(w.basename(i.file))}${ie(i.file)}`);u.push(I(l.filePath,l.identity,re(i.file),"engine.playwright-output",i.relativePath,"Playwright output"))}return{artifacts:u,bytes:h.bytes,entries:h.entries}}catch(s){if(gt(s))return{artifacts:[],bytes:0,entries:0};throw s}}async function Qt(t,e,r){const a=[];let n=0,s=0;const o=[{depth:0,directory:t}];for(;o.length>0;){const h=o.pop();if(h===void 0)continue;if(h.depth>at)throw new Error(`Playwright output exceeds the ${at.toString()} level directory depth limit`);const u=h.directory,p=await T.readdir(u,{withFileTypes:!0});p.sort((f,i)=>f.name.localeCompare(i.name));for(const f of p){if(s+=1,s>st)throw new X({bytes:n,entries:s,kind:"entries",limit:st,observed:s});const i=w.join(u,f.name),l=await r.lstat(i);if(l.isSymbolicLink())throw new Error("Playwright output must not contain symbolic links");if(l.isDirectory()){o.push({depth:h.depth+1,directory:i});continue}if(!l.isFile())continue;if(l.size<0n||l.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error("Playwright output file has an invalid size");const m=Number(l.size);if(n+=m,!Number.isSafeInteger(n)||n>Y)throw new X({bytes:n,entries:s,kind:"bytes",limit:Y,observed:n});e.has(i)||a.push({file:i,identity:l,relativePath:U(t,i)})}}return{bytes:n,candidates:a,entries:s}}function Zt(t){return t.kind==="bytes"?`Playwright output exceeds its ${t.limit.toString()} byte aggregate limit (observed ${t.observed.toString()} bytes)`:`Playwright output exceeds its ${t.limit.toString()} entry limit (observed ${t.observed.toString()} entries)`}async function ut(t,e,r,a){if(t===void 0)throw new Error("Playwright path attachment did not declare a path");const n=w.resolve(e,t),s=await a.lstat(n);if(!s.isFile()||s.isSymbolicLink())throw new Error("Playwright attachment path must be a regular non-symbolic-link file");const o=await T.realpath(n);if(!D(e,o)&&!D(r,o))throw new Error("Playwright attachment path must stay within the consumer or artifact root");return{file:o,identity:s}}function qt(t){const e=S(t,"Playwright report.stats");return{expected:b(e.expected,"Playwright stats.expected"),unexpected:b(e.unexpected,"Playwright stats.unexpected"),flaky:b(e.flaky,"Playwright stats.flaky"),skipped:b(e.skipped,"Playwright stats.skipped"),duration:q(e.duration,"Playwright stats.duration")}}function te(t){let e=Nt;const r=[];for(const a of t){if(e<=0)break;const s=Buffer.from(a.text,"utf8").subarray(0,e).toString("utf8");s.length>0&&r.push({...a,text:s}),e-=Buffer.byteLength(s)}return e<=0&&r.push({stream:"engine",text:"Playwright report log evidence was truncated at its bounded limit"}),r}function ee(t){if(t.path!==void 0){const e=w.extname(t.path);if(/^\.[a-z0-9]{1,12}$/iu.test(e))return e}return t.contentType==="application/json"?".json":t.contentType.startsWith("text/")?".txt":t.contentType==="image/png"?".png":t.contentType==="image/jpeg"?".jpg":".bin"}function re(t){const e=w.extname(t).toLowerCase();return e===".json"?"application/json":e===".zip"?"application/zip":e===".png"?"image/png":e===".jpg"||e===".jpeg"?"image/jpeg":e===".webm"?"video/webm":[".log",".txt",".md"].includes(e)?"text/plain; charset=utf-8":"application/octet-stream"}function ie(t){const e=w.extname(t).toLowerCase();return/^\.[a-z0-9]{1,12}$/u.test(e)?e:".bin"}function pt(t){const e=t.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"").slice(0,80);return e.length>0?e:"attachment"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{evidenceArtifact as a,stableEvidenceFromBuffer as l,writeOwnedEvidenceFile as
|
|
2
|
-
`,"utf8")),r=await
|
|
1
|
+
import{evidenceArtifact as a,stableEvidenceFromBuffer as l,writeOwnedEvidenceFile as o}from"../../adapters/evidence-files.js";export async function materializePlaywrightFailureDetails(i,t){if(t.length===0)return null;const n={phase:"exec",failures:t},e=l(Buffer.from(`${JSON.stringify(n,null,2)}
|
|
2
|
+
`,"utf8")),r=await o(i,"playwright-failure-details.json",e.contents);return{...a(r,e,"application/json","testkit.failure-details","playwright-failure-details",s(t)),data:n}}function s(i){const t=i.filter(r=>r.kind==="playwright-not-run").length,n=i.length-t,e=`${n.toString()} failed Playwright spec${n===1?"":"s"}`;return t===0?e:`${e}, ${t.toString()} not run`}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import f from"node:path";import{statSync as I}from"node:fs";import d from"node:fs/promises";import R from"node:crypto";import{stripVTControlCharacters as U}from"node:util";import{writeNativeReport as L}from"../../adapters/native-report.js";import{writeOwnedEvidenceFile as V}from"../../adapters/evidence-files.js";import{boundEnvironmentValueMissingText as j,environmentValueMissingFailure as C,readEnvironmentValueMissingText as A}from"../../contracts/environment-values.js";import{attachmentIoError as O,attachmentLimitError as z}from"./attachments.js";export default class _{options;config;suite;errors=[];loadedFiles=new Map;missing=[];constructor(t){this.options=t}onBegin(t,r){this.config=t,this.suite=r;for(const i of r.allTests()){const s=S(i)?.title;if(s===void 0||this.loadedFiles.has(s))continue;const a=X(f.resolve(t.rootDir,s));this.loadedFiles.set(s,a),a||this.errors.push(`Playwright's file suite "${s}" names no file resolved against ${t.rootDir}, so Testkit cannot tell which file declared its specs`)}}onError(t){this.missing.push(...M(t,!1)),this.errors.push(g(y(t)))}loadedFile(t){const r=S(t)?.title;return r===void 0||this.loadedFiles.get(r)===!1?t.location.file:f.resolve(this.config.rootDir,r)}async onEnd(t){const r=this.suite.allTests(),i={duration:t.duration,expected:0,flaky:0,skipped:0,unexpected:0},s=[],a=[];let h=0;const w=[];for(const n of r){const v=n.outcome();i[v]+=1;let F=0,m=0,E=[],T=null;const k=[],P=n.parent.project()?.use.baseURL;for(const c of n.results){for(const o of c.errors)k.push(...M(o,!P));F+=c.duration,m=Math.max(m,c.retry);for(const o of["stdout","stderr"])for(const l of c[o])s.push({stream:o,text:typeof l=="string"?l:l.toString("utf8")});for(const o of c.attachments){const l={name:o.name,contentType:o.contentType,...o.path===void 0?{}:{path:o.path},specId:n.id,specTitle:n.title,retry:c.retry};try{let u=o.body?.byteLength;if(u===void 0){if(o.path===void 0)throw new Error("Attachment has no body or path");const p=await d.lstat(o.path);if(!p.isFile()||p.isSymbolicLink())throw new Error("Attachment path must be a regular non-symbolic-link file");u=p.size}const b=z(u,h+u);if(b!==null){a.push({...l,error:b});continue}let x=o.path;if(o.body!==void 0){const p=this.options.attachmentDirectory;if(await d.realpath(p)!==f.resolve(p))throw new Error("Attachment directory must not resolve through symbolic links");x=await V(p,R.randomUUID(),o.body)}a.push({...l,path:x}),h+=u}catch(u){a.push({...l,error:O(u)})}}if(c.errors.length>0){E=c.errors.map(l=>g(y(l)));const o=c.errors.find(l=>l.stack)?.stack;T=o===void 0?null:g(o)}}w.push({id:n.id,title:n.title,parents:q(n,this.config.rootDir),file:N(this.config.rootDir,n.location.file),sourceFile:this.loadedFile(n),projectName:n.parent.project()?.name??"",line:n.location.line,column:n.location.column,status:v,expectedStatus:n.expectedStatus,durationMs:F,retries:m,failureMessages:E,failureStack:T,...D(k)})}await L(this.options,{status:t.status,failOnFlakyTests:this.options.failOnFlakyTests,stats:i,specs:w,logs:s,attachments:a,errors:this.errors,...D(this.missing)},64*1024*1024)}async onExit(){if(await d.realpath(f.dirname(this.options.reportersFinishedFile))!==await d.realpath(this.options.workspaceDirectory))throw new Error("Playwright reporters-finished marker must stay in its invocation workspace");await(await d.open(this.options.reportersFinishedFile,"wx",384)).close()}}const G=8,B=/^(?:\w*Error: )?page\.goto: .*Cannot navigate to invalid URL/u,H=/^\s*- navigating to "([^"]*)"/mu,Z=/^[A-Za-z][A-Za-z\d+.-]*:/u;function M(e,t){const r=[];let i=e;for(let s=0;s<G&&i!==void 0;s+=1){const a=y(i),h=[...A(a),...t?$(a):[]];h.length>0&&r.push({message:a,values:h}),i=i.cause}return r}function $(e){const t=U(e);if(!B.test(t))return[];const r=H.exec(t)?.[1];return r===void 0||Z.test(r)?[]:[{kind:"endpoint",valueName:null}]}function D(e){const t=e.at(0);return t===void 0?{}:{environmentFailure:C(e.flatMap(({values:r})=>r),t.message)}}function g(e){return A(e).length===0?e:j(e)}function X(e){try{return I(e).isFile()}catch{return!1}}function S(e){let t=e.parent;for(;t!==void 0&&t.type!=="file";)t=t.parent;return t}function q(e,t){const r=[];for(let i=e.parent;i&&i.type!=="project"&&i.type!=="root";i=i.parent)r.unshift(i.type==="file"&&i.location?N(t,i.location.file):i.title);return r}function N(e,t){return f.relative(e,t).split(f.sep).join("/")}function y(e){return e.message?.trim()||e.stack?.trim()||e.value?.trim()||"Playwright reported an error"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{redactSensitiveText as g}from"../../../kernel/redaction.js";export const BROWSER_NETWORK_ATTACHMENT="testkit-browser-network",MAX_DETAILED_REQUESTS=200,MAX_ROWS=100,MAX_OTHER_ORIGINS=20,MAX_STATUSES=32;const q=200,E=new Set(["document","fetch","xhr"]),S=new Set(["DELETE","PATCH","POST","PUT"]),M={"ws:":"http:","wss:":"https:"},R=/^(?:\d+|[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}|[\da-f]{16,})$/iu;export function serviceOrigins(s){return Object.entries(s).flatMap(([t,n])=>{const i=URL.parse(n);return i===null?[]:[{name:t,origin:i.origin,basePath:i.pathname.replace(/\/+$/u,"")}]})}export function serviceFor(s,t){const n=M[s.protocol],i=n===void 0?s.origin:`${n}//${s.host}`;let u=null;for(const o of t)o.origin===i&&(o.basePath!==""&&s.pathname!==o.basePath&&!s.pathname.startsWith(`${o.basePath}/`)||(u===null||o.basePath.length>u.basePath.length)&&(u=o));return u?.name??null}export function pathPattern(s){return f(g(s).split("/").map(t=>R.test(t)?":id":t).join("/"))}export function summarizeBrowserNetwork(s){const t=new Map,n=new Map,i=new Map,u=new Map,o=[];let y=0,d=0,T=0;for(const e of s){d+=e.requestBytes,T+=e.responseBytes;const r=t.get(e.resourceType)??{requests:0,responseBytes:0};if(r.requests+=1,r.responseBytes+=e.responseBytes,t.set(e.resourceType,r),e.service===null||!E.has(e.resourceType)){if(e.service===null){const p=new URL(e.url),l=f(p.host||p.protocol),h=i.get(l)??{requests:0,responseBytes:0};h.requests+=1,h.responseBytes+=e.responseBytes,i.set(l,h)}continue}const a=pathPattern(e.path),B=JSON.stringify([e.service,e.method,a]),c=n.get(B)??O(e.service,e.method,a);if(c.requests+=1,c.requestBytes+=e.requestBytes,c.responseBytes+=e.responseBytes,e.status!==null&&c.statuses.add(e.status),e.failure!==null&&(c.failures+=1),n.set(B,c),S.has(e.method)){const p=JSON.stringify([e.service,e.method,_(e.url),e.bodyHash]),l=u.get(p)??{row:{service:e.service,method:e.method,path:a,requests:0},requests:0};l.requests+=1,u.set(p,l)}o.length<MAX_DETAILED_REQUESTS?o.push({service:e.service,method:e.method,path:a,resourceType:e.resourceType,status:e.status,failure:e.failure===null?null:f(g(e.failure)),requestBytes:e.requestBytes,responseBytes:e.responseBytes,startMs:e.startMs,durationMs:e.durationMs}):y+=1}const m=[...n.values()],w=[...i.entries()];return{totals:{requests:s.length,requestBytes:d,responseBytes:T},resourceTypes:[...t.entries()].map(([e,r])=>({type:e,...r})).sort((e,r)=>r.responseBytes-e.responseBytes),rows:m.slice(0,MAX_ROWS).map(e=>({...e,statuses:[...e.statuses].sort((r,a)=>r-a).slice(0,MAX_STATUSES)})),omittedRows:Math.max(0,m.length-MAX_ROWS),otherOrigins:w.slice(0,MAX_OTHER_ORIGINS).map(([e,r])=>({host:e,...r})),omittedOrigins:Math.max(0,w.length-MAX_OTHER_ORIGINS),repeatedWrites:[...u.values()].filter(e=>e.requests>1).slice(0,MAX_ROWS).map(e=>({...e.row,requests:e.requests})),requests:o,omittedRequests:y}}function O(s,t,n){return{service:s,method:t,path:n,requests:0,requestBytes:0,responseBytes:0,statuses:new Set,failures:0}}function f(s){return s.length<=q?s:`${s.slice(0,q-1)}\u2026`}function _(s){const t=new URL(s);return t.hash="",t.href}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import p from"node:crypto";import{BROWSER_NETWORK_ATTACHMENT as y,serviceFor as f,serviceOrigins as w,summarizeBrowserNetwork as m}from"./browser-network.js";import{readPlaywrightContext as g}from"./runtime-input.js";const h=Symbol.for("@gfdelarue/testkit/playwright-browser-network/v1"),T=5e3,B=new Set(["DELETE","PATCH","POST","PUT"]);export async function browserRequests(){const r=Reflect.get(globalThis,h);if(typeof r!="function")throw new Error("browserRequests() works only inside a test that Testkit's Playwright engine runs");return r()}export async function recordBrowserTraffic(r,e,s){const t=new R(w(g()?.endpoints??{})),o=Reflect.get(r,"_instrumentation"),a={runAfterCreateBrowserContext:i=>(t.watch(i),Promise.resolve()),runBeforeCloseBrowserContext:i=>t.finishReading(i)};o.addListener(a),Reflect.set(globalThis,h,()=>t.requests());try{await s()}finally{o.removeListener(a),Reflect.deleteProperty(globalThis,h),await t.stop()}const u=t.observed();u.length>0&&await e.attach(y,{body:JSON.stringify(m(u)),contentType:"application/json"})}class R{services;#e=[];#t=new WeakMap;#s=new Map;#r=Date.now();constructor(e){this.services=e}watch(e){if(this.#s.has(e))return;const s=n=>{this.#o(e,n)},t=n=>{const c=this.#t.get(n.request());c!==void 0&&(c.observed={...c.observed,status:n.status()})},o=n=>{this.#n(n,!1)},a=n=>{this.#n(n,!0)},u=n=>{this.#i(e,n.url())},i=new Set,l=n=>{i.add(n),n.on("websocket",u)};e.pages().forEach(l),e.on("page",l),e.on("request",s),e.on("response",t),e.on("requestfinished",o),e.on("requestfailed",a),this.#s.set(e,()=>{e.off("request",s),e.off("response",t),e.off("requestfinished",o),e.off("requestfailed",a),e.off("page",l);for(const n of i)n.off("websocket",u)})}async requests(){const e=[...this.#e];return await d(e.filter(s=>s.observed.resourceType!=="eventsource").map(s=>s.settled)),e.map(({observed:s})=>({service:s.service,method:s.method,url:s.url,path:s.path,resourceType:s.resourceType,status:s.status,failure:s.failure,requestBytes:s.requestBytes,responseBytes:s.responseBytes,durationMs:s.durationMs}))}async finishReading(e){await d(this.#e.flatMap(s=>s.reading!==null&&(e===void 0||s.context===e)?[s.reading]:[]))}async stop(){for(const e of this.#s.values())e();this.#s.clear(),await this.finishReading()}observed(){return this.#e.map(e=>e.observed)}#o(e,s){const t=new URL(s.url()),o=s.method();let a;const u=new Promise(l=>{a=l}),i={context:e,observed:{service:f(t,this.services),method:o,url:t.href,path:t.pathname,resourceType:s.resourceType(),status:null,failure:null,requestBytes:0,responseBytes:0,durationMs:null,bodyHash:B.has(o)?b(s):null,startMs:Date.now()-this.#r},reading:null,settled:u,settle:a};this.#e.push(i),this.#t.set(s,i)}#i(e,s){const t=new URL(s);this.#e.push({context:e,observed:{service:f(t,this.services),method:"GET",url:t.href,path:t.pathname,resourceType:"websocket",status:null,failure:null,requestBytes:0,responseBytes:0,durationMs:null,bodyHash:null,startMs:Date.now()-this.#r},reading:null,settled:Promise.resolve(),settle:()=>{}})}#n(e,s){const t=this.#t.get(e);t===void 0||t.reading!==null||(t.reading=v(e,s).then(o=>{t.observed={...t.observed,...o,status:o.status??t.observed.status},t.settle()}))}}function b(r){return p.createHash("sha256").update(r.postDataBuffer()??"").digest("hex")}async function d(r){if(r.length===0)return;let e;await Promise.race([Promise.all(r),new Promise(s=>{e=setTimeout(s,T)})]),clearTimeout(e)}async function v(r,e){const[s,t]=await Promise.all([e?null:r.sizes().catch(()=>null),r.response().catch(()=>null)]),o=r.timing().responseEnd;return{status:t?.status()??null,failure:e?r.failure()?.errorText??"failed":null,requestBytes:Math.max(0,s?.requestBodySize??r.postDataBuffer()?.byteLength??0),responseBytes:Math.max(0,s?.responseBodySize??0),durationMs:o>=0?Math.round(o):null}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CHROMIUM_STARTUP_ENVIRONMENT=Object.freeze({DBUS_SESSION_BUS_ADDRESS:"disabled:",FC_FONTATIONS:"1"});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const r=Object.freeze({"3g":Object.freeze({latencyMs:2e3,downloadKbps:400,uploadKbps:400}),"slow-4g":Object.freeze({latencyMs:562.5,downloadKbps:1440,uploadKbps:675}),"fast-4g":Object.freeze({latencyMs:165,downloadKbps:8100,uploadKbps:1350})}),a=Object.keys(r).map(e=>`"${e}"`),c=["latencyMs","downloadKbps","uploadKbps"],l=100,i=new WeakMap;export async function emulateNetwork(e,t){const o=networkThrottle(t),n=await w(e);await n.send("Network.emulateNetworkConditionsByRule",{offline:!1,matchedNetworkConditions:[{urlPattern:"",...o}]}),await n.send("Network.overrideNetworkState",{offline:!1,...o})}export async function emulateCpu(e,t){const o=cpuThrottlingRate(t);await(await w(e)).send("Emulation.setCPUThrottlingRate",{rate:o})}export function networkThrottle(e){const t=typeof e=="string"?d(e):f(e);return{latency:t.latencyMs,downloadThroughput:p(t.downloadKbps),uploadThroughput:p(t.uploadKbps)}}export function cpuThrottlingRate(e){const t=s(e,"emulateCpu slowdown");if(t<1||t>l)throw new RangeError(`emulateCpu slowdown must be from 1 to ${String(l)}, received ${String(t)}`);return t}function d(e){if(!Object.hasOwn(r,e))throw new RangeError(`emulateNetwork has no profile ${JSON.stringify(e)}. Use ${a.join(", ")}, or custom { latencyMs, downloadKbps, uploadKbps }`);return r[e]}function f(e){if(typeof e!="object"||e===null||Array.isArray(e))throw new TypeError(`emulateNetwork takes a profile name (${a.join(", ")}) or { latencyMs, downloadKbps, uploadKbps }`);const t=Object.keys(e).filter(n=>!c.includes(n));if(t.length>0)throw new TypeError(`emulateNetwork has no field ${t.map(n=>JSON.stringify(n)).join(", ")}. Custom conditions take latencyMs, downloadKbps, and uploadKbps`);const o=s(Reflect.get(e,"latencyMs"),"emulateNetwork latencyMs");if(o<0)throw new RangeError(`emulateNetwork latencyMs must be 0 or more, received ${String(o)}`);return{latencyMs:o,downloadKbps:u(Reflect.get(e,"downloadKbps"),"downloadKbps"),uploadKbps:u(Reflect.get(e,"uploadKbps"),"uploadKbps")}}function u(e,t){const o=s(e,`emulateNetwork ${t}`);if(o<=0)throw new RangeError(`emulateNetwork ${t} must be more than 0, received ${String(o)}`);return o}function s(e,t){if(typeof e!="number")throw new TypeError(`${t} must be a number, received ${e===void 0?"nothing":typeof e}`);if(!Number.isFinite(e))throw new RangeError(`${t} must be finite, received ${String(e)}`);return e}function p(e){return e*1e3/8}function w(e){let t=i.get(e);return t===void 0&&(t=e.context().newCDPSession(e),i.set(e,t)),t}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"@playwright/test";export{readPlaywrightContext}from"./runtime-input.js";export{defineTestkitConfig}from"./config.js";export{assertSafeUiTarget,assertSafeUiTargets,createAuthedApiClient,createSandboxId,createUiSandbox,resolveBackendBaseUrl,resolveFrontendBaseUrl,waitForAnimationFrames,waitForElementStable,waitForImagesLoaded,waitForNetworkIdle,waitForUiSettled}from"./sandbox.js";
|
|
1
|
+
export*from"@playwright/test";export{readPlaywrightContext}from"./runtime-input.js";export{defineTestkitConfig}from"./config.js";export{emulateCpu,emulateNetwork}from"./emulation.js";export{browserRequests}from"./browser-traffic.js";export{assertSafeUiTarget,assertSafeUiTargets,createAuthedApiClient,createSandboxId,createUiSandbox,resolveBackendBaseUrl,resolveFrontendBaseUrl,waitForAnimationFrames,waitForElementStable,waitForImagesLoaded,waitForNetworkIdle,waitForUiSettled}from"./sandbox.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import c,{test as u}from"playwright/test";import{recordBrowserTraffic as s}from"./browser-traffic.js";import{CHROMIUM_STARTUP_ENVIRONMENT as i}from"./chromium-environment.js";export*from"playwright/test";const o=u.extend({_testkitBrowserTraffic:[async({playwright:e},t,a)=>{await s(e,a,()=>t(void 0))},{auto:!0,box:!0}],_browserOptions:[async({_browserOptions:e,playwright:t},a,r)=>{const n={...e,...e.env===void 0?{}:{env:{...i,...e.env}},chromiumSandbox:e.chromiumSandbox??r.project.use.launchOptions?.chromiumSandbox??!0,executablePath:f(r)};Reflect.set(t,"_defaultLaunchOptions",n),await a(n)},{auto:!0,box:!0,scope:"worker"}]});for(const[e,t]of Object.entries(c))Object.hasOwn(o,e)||Reflect.set(o,e,t);Reflect.set(o,"test",o);export{o as test,o as"module.exports"};export default o;function f(e){const t=e.project.use.launchOptions?.executablePath;if(typeof t!="string"||t.length===0)throw new Error("Managed Playwright config names no browser executable");return t}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import l from"node:fs/promises";import c from"node:path";import{writeOwnedRuntimeInput as m,ownedRuntimeInputModuleUrl as f}from"../../runtime-input-file.js";import{requireBoundedJsonValue as y,RUNTIME_INPUT_JSON_BOUNDS as h}from"../../adapters/json-bounds.js";import{ENGINE_PROCESS_RESERVED_ENVIRONMENT_KEYS as w}from"../../../process/environment.js";import{
|
|
1
|
+
import l from"node:fs/promises";import c from"node:path";import{writeOwnedRuntimeInput as m,ownedRuntimeInputModuleUrl as f}from"../../runtime-input-file.js";import{requireBoundedJsonValue as y,RUNTIME_INPUT_JSON_BOUNDS as h}from"../../adapters/json-bounds.js";import{ENGINE_PROCESS_RESERVED_ENVIRONMENT_KEYS as w}from"../../../process/engine-environment-keys.js";import{CHROMIUM_STARTUP_ENVIRONMENT as g}from"./chromium-environment.js";import{managedProcessEnvironmentOverlayInstallerSource as E}from"../../../process/environment-overlay.js";import{isUnknownRecord as u}from"../../../kernel/unknown-values.js";export const PLAYWRIGHT_RUNTIME_INPUT_MAX_BYTES=4*1024*1024,PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES=2*1024*1024,PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES=512;const d=new Set(["BASE_URL","FORCE_COLOR","NODE_OPTIONS","NO_COLOR","PLAYWRIGHT_BROWSERS_PATH","PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD","TESTKIT_ACTIVE","TESTKIT_INTERNAL_FILE_DEADLINE_MS","TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS","TESTKIT_INVOCATION_ID","TESTKIT_RUNTIME_ID","TESTKIT_TEST_FILE","TESTKIT_TEST_ID",...Object.keys(g),...w].map(e=>e.toUpperCase())),p={filename:"runtime-input.json",label:"Playwright runtime input",maxSizeBytes:PLAYWRIGHT_RUNTIME_INPUT_MAX_BYTES},T=Symbol.for("@gfdelarue/testkit/playwright-runtime-input/v1");export function readPlaywrightContext(){const e=Reflect.get(globalThis,T);return I(e)?e.context:null}export async function materializePlaywrightRuntimeInput(e){if(!c.isAbsolute(e.directory))throw new Error("Playwright runtime-input directory must be absolute");await l.mkdir(e.directory,{recursive:!0,mode:448}),process.platform!=="win32"&&await l.chmod(e.directory,448);const t=v(e.inputs.environment),i={baseUrl:e.baseUrl===null?null:b(e.baseUrl),databases:e.databases,endpoints:e.endpoints,engineId:"playwright",environment:t,fixtures:e.inputs.fixtures,handles:e.handles,invocationId:e.invocationId,managed:!0,schemaVersion:1,taskId:e.taskId,uiConfig:e.inputs.configuration??{},...e.inputs.setupData===void 0?{}:{setupData:e.inputs.setupData}},o=await m(e.directory,i,p);return{digest:o.sha256,file:o.file,sizeBytes:o.sizeBytes}}export function playwrightRuntimeInputBootstrapSource(e){const t=JSON.stringify({digest:e.digest,file:e.file,ownedInput:{directory:c.dirname(e.file),file:e.file,sha256:e.digest,sizeBytes:e.sizeBytes},protocol:p,invocationId:e.invocationId,jsonBounds:h,maximumEnvironmentBytes:PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES,maximumEnvironmentEntries:PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES,reservedEnvironmentKeys:[...d],taskId:e.taskId}),n=E(),i=`const requireBoundedJsonValue = (${y.toString()});`;return`
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readVerifiedOwnedFile } from ${JSON.stringify(f)};
|
|
4
4
|
|
|
@@ -171,4 +171,4 @@ function requireRecord(value, label) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
await installPlaywrightRuntimeInput();
|
|
174
|
-
`}function
|
|
174
|
+
`}function I(e){return!u(e)||!u(e.context)||!u(e.input)?!1:typeof e.digest=="string"&&typeof e.file=="string"&&e.input.engineId==="playwright"&&e.input.schemaVersion===1&&Object.isFrozen(e)&&Object.isFrozen(e.context)&&Object.isFrozen(e.input)}function v(e){const t=Object.create(null),n=new Set;let i=0;const o=Object.entries(e);if(o.length>PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES)throw new Error(`Playwright consumer environment exceeds its ${PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES.toString()} entry limit`);for(const[a,s]of o){O(a,s);const r=a.toUpperCase();if(n.has(r))throw new Error(`Playwright consumer environment contains case-insensitive duplicate key ${a}`);if(n.add(r),r.startsWith("TESTKIT_")||r.startsWith("PLAYWRIGHT_")||r.startsWith("PWTEST_")||r.startsWith("PW_TEST_")||d.has(r))throw new Error(`Playwright consumer environment contains reserved key ${a}`);if(i+=Buffer.byteLength(a,"utf8")+Buffer.byteLength(s,"utf8")+2,i>PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES)throw new Error(`Playwright consumer environment exceeds its ${PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES.toString()} byte limit`);t[a]=s}return t}function b(e){if(typeof e!="string"||e.trim().length===0)throw new Error("Playwright runtime input base URL must be non-empty");let t;try{t=new URL(e)}catch(n){throw new Error("Playwright runtime input base URL must be a valid URL",{cause:n})}if(t.protocol!=="http:"&&t.protocol!=="https:")throw new Error("Playwright runtime input base URL must use HTTP or HTTPS");return t.href}function O(e,t){if(e.length===0||e.includes("=")||e.includes("\0")||typeof t!="string")throw new Error("Playwright consumer environment contains an invalid key");if(t.includes("\0"))throw new Error(`Playwright consumer environment contains an invalid value for ${e}`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import N from"crypto";import{isTerminalRunStatus as z}from"../process/runs/schema.js";import{observeProcessCreationIdentity as x}from"../process/process-identity.js";import{protectTestkitStateRoot as W}from"../process/files/product-dir.js";export{isRunManifestActive}from"../process/runs/inspection.js";import{normalizeOperationReason as U}from"../domain/operation-reason.js";import{captureProcessCreationIdentity as V}from"../process/service-processes.js";import{requireProductLifecycleFenceProductDir as B,withProductLifecycleFence as Y}from"../process/lifecycle-fence.js";import{createRunManifest as J,listRunManifests as K,readRunMaintenanceStopRequest as Q,removeRunManifestFile as _,requireSafeRunId as X,RUN_MANIFEST_SCHEMA_VERSION as Z,runManifestPath as ee,writeRunManifest as te}from"../process/runs/manifest.js";const ne=1e3,re=String,k=new WeakMap;export function createUnpublishedRunLifecycle(n,r){const s=r.runId===void 0?ue():X(r.runId,"Testkit run id");W(n,!0);const u=ee(n,s),o=new AbortController,I=new AbortController,d=r.pid===void 0?process.pid:r.pid;if(!Number.isSafeInteger(d)||d<0)throw new Error("Testkit run pid must be a non-negative safe integer");const w=d>0?V(d):null;if(d>0&&w===null)throw new Error(`Could not capture the Testkit run process identity for pid ${d.toString()}`);let f={incarnationId:N.randomUUID(),schemaVersion:Z,runId:s,pid:d,processIdentity:typeof w=="string"?w:null,controller:se(r.controller),status:"starting",startedAt:new Date().toISOString(),heartbeatAt:r.heartbeat===!1?null:new Date().toISOString(),interruptReason:null,services:[],runtimeDirs:[]};const O=[];let S,C=null;const y=new Set,b=new Set,M=r.keepAlive===!0?setInterval(()=>{},ne):null;let R=null,g=null,v=null,L=!1;function $(e){const t=k.get(h);if(!t)throw new Error("Testkit run lifecycle ownership is unavailable");if(!t.published){J(n,e),t.published=!0;return}te(n,e)}function A(){const e=Q(n,s);return e===null||e.targetIncarnationId!==f.incarnationId||e.targetPid!==d||e.targetProcessIdentity!==f.processIdentity||e.coordinatorPid===C?.coordinatorPid&&e.coordinatorProcessIdentity===C.coordinatorProcessIdentity?null:x(e.coordinatorPid,e.coordinatorProcessIdentity)==="different"?(C=e,null):e}function m(e){E("mutate run state");const t=ie(f);e(t),t.heartbeatAt!==null&&(t.heartbeatAt=new Date().toISOString()),$(t),f=t}function E(e){if(g!==null)throw new Error(`Cannot ${e} after Testkit run ${s} finalization is committed`)}function G(){R||(R=setInterval(()=>{},1e3))}function P(){R&&(clearInterval(R),R=null)}function q(){for(const e of b)clearInterval(e);b.clear()}function F(e="controller process exited before Testkit run finalized"){if(L||z(f.status))return!1;const t=U(e,"controller process exited before Testkit run finalized"),i=new Date,a=f.status==="interrupting"||o.signal.aborted?"interrupted":"failed",l=a==="interrupted"?f.interruptReason??v??t:t;return m(p=>{p.status=a,p.finishedAt=i.toISOString(),p.interruptReason=l,p.heartbeatAt!==null&&(p.heartbeatAt=i.toISOString())}),r.evidence.controllerExited(n,{runId:s,reason:l,status:a,finishedAt:i}),!0}const H=()=>{F()};process.on("exit",H);const h={runId:s,manifestPath:u,maintenanceStopSignal:I.signal,signal:o.signal,commitFinalization(e="finished"){if(g!==null)return g;const t=A(),i=o.signal.aborted||t!==null||f.status==="interrupting",a=i?f.interruptReason??v??t?.reason??"run interrupted":null,l=Object.freeze({interruptReason:a,interrupted:i});return m(p=>{p.status=i?"interrupted":e,p.finishedAt=new Date().toISOString(),p.interruptReason=a}),g=l,q(),l},getInterruptReason(){return f.interruptReason??v},isMaintenanceStopCoordinated(){return A()!==null},isStopRequested(){return o.signal.aborted||g?.interrupted===!0},markRunning(){m(e=>{e.status="running"})},markFinished(e="finished"){m(t=>{t.status=e,t.finishedAt=new Date().toISOString()})},heartbeat(e={}){m(t=>{t.heartbeatAt=new Date().toISOString(),e&&typeof e=="object"&&!Array.isArray(e)&&(t.session={...t.session||{},...e})})},startHeartbeat(e=5e3,t=null){E("start a run heartbeat");const i=Number.isFinite(Number(e))&&Number(e)>0?Math.floor(Number(e)):5e3,a=setInterval(()=>{let l;try{l=typeof t=="function"?t():{}}catch{l={}}h.heartbeat(l)},i);return a.unref(),b.add(a),()=>{clearInterval(a),b.delete(a)}},requestStop(e="interrupted"){if(g!==null||z(f.status))return;const t=A(),i=t?t.reason:U(e,"interrupted"),a=!o.signal.aborted;a&&(v=i),G();let l=null;if(a)try{m(c=>{c.status="interrupting",c.interruptReason=i})}catch(c){l=c instanceof Error?c:new Error("Failed to persist the Testkit stop request",{cause:c})}t!==null&&!I.signal.aborted&&I.abort(new Error(`Testkit run ${s} is being stopped by external maintenance`));const p=[...y].map(({supervisor:c})=>c.stop(t===null?void 0:Date.parse(t.requestedAt)+c.shutdownTimeoutMs));if(Promise.allSettled(p).then(P),o.signal.aborted||o.abort(new Error(`testkit run interrupted (${i})`)),!!a&&l!==null)throw l},trackGraphContext(e){m(t=>{ae(t.runtimeDirs,e.runtimeDir)})},registerService(e,t,i,a){if(E("register application service ownership"),i.pid!==t.pid||(process.platform==="win32"?i.recovery.processGroupId!==null:i.recovery.processGroupId!==i.pid))throw new Error("Application service supervisor returned an invalid ownership handoff");const l={supervisor:t,unsubscribe:()=>{}};y.add(l);const p=oe(e);m(c=>{c.services=c.services.filter(D=>D.pid!==i.pid),c.services.push({serviceName:e.name,runtimeLabel:e.runtimeLabel,command:e.testkit.local.start||null,cwd:a,pid:i.pid,processGroupId:i.recovery.processGroupId,processIdentity:i.recovery.creationIdentity,ports:p,startedAt:new Date().toISOString(),shutdownTimeoutMs:t.shutdownTimeoutMs,shutdownDeadlineAt:t.shutdownDeadlineAt})}),l.unsubscribe=t.onShutdownStarted(c=>{m(D=>{const T=D.services.find(j=>j.pid===i.pid);T&&(T.shutdownDeadlineAt=T.shutdownDeadlineAt==null?c:Math.min(T.shutdownDeadlineAt,c))})})},unregisterService(e){E("unregister application service ownership");for(const t of y)t.supervisor.pid===e&&(t.unsubscribe(),y.delete(t));m(t=>{t.services=t.services.filter(i=>i.pid!==e)})},installSignalHandlers(){if(!(O.length>0)&&!(d!==process.pid||x(d,f.processIdentity)!=="matches")){for(const e of["SIGINT","SIGTERM","SIGHUP"]){const t=h.requestStop.bind(h,e);process.on(e,t),O.push([e,t])}process.platform==="win32"&&(S=setInterval(()=>{const e=A();e!==null&&(clearInterval(S),h.requestStop(e.reason))},100),S.unref(),b.add(S))}},removeSignalHandlers(){S&&(clearInterval(S),b.delete(S),S=void 0);for(const[e,t]of O.splice(0))process.off(e,t)},async removeManifest(){await Y(n,e=>cleanupOwnedRunLifecycleWithinLifecycleFence(e,h))},finalizeForControllerExit:F,dispose(){L=!0;for(const e of y)e.unsubscribe();process.off("exit",H),h.removeSignalHandlers(),M&&clearInterval(M),P(),q()}};return k.set(h,{incarnationId:f.incarnationId,pid:d,processIdentity:f.processIdentity,productDir:n,published:!1,runId:s}),h}function ie(n){const r={...n,controller:n.controller===null?null:{...n.controller},runtimeDirs:[...n.runtimeDirs],services:n.services.map(s=>({...s,ports:s.ports.map(u=>({...u}))}))};return n.session!==void 0&&(r.session=n.session===null?null:{...n.session}),r}function se(n){return!n||typeof n!="object"||Array.isArray(n)?null:Object.fromEntries(Object.entries(n).filter(([,r])=>r!==void 0).map(([r,s])=>[r,s===null?null:re(s)]))}export function cleanupOwnedRunLifecycleWithinLifecycleFence(n,r){const{manifest:s,ownership:u,productDir:o}=readOwnedRunLifecycleWithinLifecycleFence(n,r);if(!s)return!1;if(s.services.length>0)throw new Error(`Cannot clean Testkit run ${r.runId} service ownership while holding the lifecycle fence`);return _(o,r.runId),u.published=!1,!0}export function readOwnedRunLifecycleWithinLifecycleFence(n,r){const s=B(n),u=k.get(r);if(!u||u.productDir!==s)throw new Error("Cannot clean a Testkit run without matching lifecycle ownership");if(u.runId!==r.runId)throw new Error("Testkit run lifecycle identity has been modified");const o=K(s).find(I=>I.runId===r.runId);if(o!==void 0&&(o.incarnationId!==u.incarnationId||o.pid!==u.pid||o.processIdentity!==u.processIdentity))throw new Error(`Testkit run ${r.runId} ownership no longer matches its manifest`);return{manifest:o??null,ownership:u,productDir:s}}function oe(n){const r=new Set,s=[];for(const u of[n.testkit.local.baseUrl,n.testkit.local.readyUrl])if(u)try{const o=new URL(u),I=le(o.hostname),d=Number(o.port||(o.protocol==="https:"?443:80)),w=`${I}:${d.toString()}`;if(r.has(w))continue;r.add(w),s.push({host:I,port:d})}catch{}return s}function ae(n,r){r&&(n.includes(r)||n.push(r))}function ue(){return`${Date.now().toString()}-${process.pid.toString()}-${N.randomBytes(4).toString("hex")}`}function le(n){return n?n==="::1"?"127.0.0.1":n:"127.0.0.1"}export function disposeFailedRunLifecycle(n){const r=k.get(n);n.dispose(),n.removeSignalHandlers(),r?.published===!0&&_(r.productDir,n.runId)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{formatRunSummary as
|
|
1
|
+
import{formatRunSummary as R}from"../process/runs/manifest.js";import{cleanupOrphanedLocalInfrastructure as y}from"../database/lifecycle-cleanup.js";import{requireProductLifecycleFenceProductDir as d,withProductLifecycleFence as l}from"../process/lifecycle-fence.js";import{listRunManifests as p,removeRunManifestFile as h,requireSafeRunId as w}from"../process/runs/manifest.js";import{stopRunManifest as S}from"./run-stopping.js";import{createUnpublishedRunLifecycle as A,disposeFailedRunLifecycle as I,isRunManifestActive as u}from"./lifecycle-controller.js";class v extends Error{constructor(e){super(`Stale Testkit run ${e} still owns application services and requires phased cleanup`),this.name="StaleRunServiceCleanupRequiredError"}}export async function startRunLifecycle(t,e){for(;;){await cleanupStaleRuns(t,e);try{return await l(t,n=>startRunLifecycleWithinLifecycleFence(n,e))}catch(n){if(!(n instanceof v))throw n}}}export async function startRunLifecycleWithinLifecycleFence(t,e){const n=d(t);m(n),await cleanupRunsWithinLifecycleFence(t,{evidence:e.evidence,localInfrastructure:!1});const o=p(n).find(c=>!u(c)&&c.services.length>0);if(o!==void 0)throw new v(o.runId);m(n);const i=A(n,e);try{return i.installSignalHandlers(),e.evidence.runStarting(n),i.markRunning(),i}catch(c){throw I(i),c}}function m(t){const e=p(t).find(u);if(e)throw new Error(`Active Testkit run ${R(e)} already owns this product`)}export async function cleanupRuns(t,{evidence:e,includeActive:n=!1,localInfrastructure:o}){const i=[],c=[];for(;;){const r=await l(t,()=>p(t).flatMap(a=>{const f=u(a);return n&&f||!f&&a.services.length>0?[{active:f,manifest:a}]:[]}));if(r.length===0)break;for(const a of r)await S(t,a.manifest,{evidence:e,reason:a.active?"cleanup":"stale run process not active"}),a.active?i.push(a.manifest):c.push(a.manifest)}const s=await l(t,r=>cleanupRunsWithinLifecycleFence(r,{evidence:e,localInfrastructure:o}));return{...s,cleaned:[...i,...c,...s.cleaned],stoppedActive:i}}export async function cleanupRunsWithinLifecycleFence(t,{evidence:e,protectedLiveRunIds:n=new Set,localInfrastructure:o=!0}){const i=d(t),c=p(i),s={cleaned:[],skippedActive:[],stoppedActive:[]};for(const r of c){if(u(r)||r.services.length>0){s.skippedActive.push(r);continue}typeof r.runId=="string"&&e.runStopped(i,{runId:r.runId,reason:"stale run process not active"}),h(i,w(r.runId,"Testkit run manifest runId")),s.cleaned.push(r)}return e.activeRunsListed(i,new Set([...s.skippedActive.map(r=>r.runId).filter(r=>typeof r=="string"),...n])),s.resourceCleanup=o?await y(i):{kept:[],removed:[],skipped:[],stopped:[],targets:[]},{cleaned:s.cleaned,resourceCleanup:s.resourceCleanup,skippedActive:s.skippedActive,stoppedActive:s.stoppedActive}}export async function cleanupStaleRuns(t,{evidence:e}){const n=[];for(;;){const o=await cleanupRuns(t,{evidence:e,localInfrastructure:!1});if(n.push(...o.cleaned),!o.skippedActive.some(c=>!u(c)&&c.services.length>0))return n}}export{listRunManifests,RunManifestInventoryError}from"../process/runs/manifest.js";export{stopRunById,stopRunManifest}from"./run-stopping.js";export{cleanupOwnedRunLifecycleWithinLifecycleFence,isRunManifestActive}from"./lifecycle-controller.js";export{isPidRunning}from"../process/runs/stop.js";export{formatRunSummary}from"../process/runs/manifest.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{inspectRunManifests as G}from"../process/runs/inspection.js";import
|
|
1
|
+
import{inspectRunManifests as G}from"../process/runs/inspection.js";import f from"fs";import p from"path";import{cleanupOwnedLocalDatabaseResources as L,cleanupOrphanedLocalInfrastructure as x,destroyOwnedLocalDatabaseResources as I,destroyRuntimeDatabase as R,destroyServiceDatabaseCache as F,isDatabaseStateDir as S}from"../database/index.js";import{cleanupRuns as C,cleanupRunsWithinLifecycleFence as g,isRunManifestActive as k,listRunManifests as A,stopRunManifest as N}from"./lifecycle.js";import{findGraphDirsForService as T,findRuntimeStateDirs as M}from"./state.js";import{collectRuntimeGraphStatus as E,collectRunStatus as O}from"./status-runtime.js";import{withProductLifecycleFence as d}from"../process/lifecycle-fence.js";import{pruneEmptyDir as $}from"../process/files/directory-status.js";export function createEnvironmentMaintenance(e){async function t(n,a){if(n.kind==="destroy")return j(e,n);if(a===void 0)throw new Error("Cleanup requires a cache maintenance plan");if(n.dryRun)return P(e,n,a);const i=await C(e,{evidence:n.evidence,includeActive:!0});return d(e,s=>P(e,n,a,s,i))}return{inspect(n,a=null){return{runtimeGraphs:E(e,{desiredGraphs:n,serviceName:a}),runs:O(e)}},inspectRuns:n=>G(e,n),async stop(n,a,i){const s=await d(e,()=>{const r=A(e);return n.kind==="run"?r.filter(o=>o.runId===n.runId):n.kind==="all"?r:r.filter(k).slice(-1)}),u=[];for(const r of s)u.push(await N(e,r,{evidence:i,reason:a}));return u},maintain:t}}async function j(e,t){const{configs:n,evidence:a}=t;B(e,n),await C(e,{evidence:a,includeActive:!0}),await d(e,async i=>{const s=await g(i,{evidence:a});b(s,"destroy");const u=new Set(n.flatMap(r=>[r.stateDir,...T(e,r.name)]));for(const r of u){if(!f.existsSync(r))continue;const o=M(r,S);for(const l of o)await R({productDir:e,stateDir:l});f.rmSync(r,{recursive:!0,force:!0})}for(const r of n)await F(e,r.name);t.destroyOwnedResources&&await I(i),await x(e)})}async function P(e,t,n,a=null,i=null){const{dryRun:s}=t,u=t.serviceName||null,r=t.resources;let o,l=[];if(s)o=W(e);else{if(a===null)throw new Error("Destructive cleanup requires the product lifecycle fence");const c=await g(a,{evidence:t.evidence});b(c,"cleanup"),o={...c,cleaned:[...i?.cleaned??[],...c.cleaned],stoppedActive:[...i?.stoppedActive??[]]},l=[...i===null?[]:[i.resourceCleanup],c.resourceCleanup]}const v=E(e,{desiredGraphs:t.desiredGraphs,serviceName:u}).flatMap(c=>c.staleRuntimeDirs.filter(m=>!m.active).map(m=>({graph:c.name,runtimeId:m.id,path:m.path}))),w=n(),h=[];let y={targets:[],removed:[],kept:[],skipped:[],stopped:[]};if(!s){for(const c of v)await U(e,c.path),h.push(c);await w.prune(),$(p.join(e,".testkit","_graphs"))}return r&&(y=await L(e,{dryRun:s})),{environment:{...o,dryRun:s,targets:v,runtimeCleaned:h,resourceCleanup:y,automaticResourceCleanup:l},otherCaches:w.result}}function b(e,t){if(e.skippedActive.length!==0)throw new Error(`A Testkit run started while ${t} was acquiring exclusive product ownership; retry after the run finishes`)}function B(e,t){const n=f.realpathSync.native(p.resolve(e));for(const a of t)if(f.realpathSync.native(p.resolve(a.productDir))!==n)throw new Error(`Cannot destroy service ${a.name} from a different Testkit product`)}async function U(e,t){if(!t||!f.existsSync(t))return;const n=M(t,S);for(const a of n)await R({productDir:e,stateDir:a});f.rmSync(t,{recursive:!0,force:!0})}function W(e){const t={cleaned:[],skippedActive:[],stoppedActive:[]};for(const n of A(e))k(n)?t.skippedActive.push(n):t.cleaned.push(n);return t}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{cleanupOwnedRunLifecycleWithinLifecycleFence as h,cleanupRunsWithinLifecycleFence as g,listRunManifests as w,startRunLifecycle as y}from"./lifecycle.js";import{createRuntimeManager as R}from"./runtime-manager.js";import{withProductLifecycleFence as v}from"../process/lifecycle-fence.js";import{formatError as q}from"../kernel/error-serialization.js";export function createLocalRunEnvironment(i){return{async start(l){const n=await y(i,l),s=n.startHeartbeat(2e3,()=>({kind:"controller",status:n.isStopRequested()?"interrupting":"running"}));let a=null;const d=new Map,o=()=>{if(a===null)throw new Error("Run runtimes have not been provisioned");return a};return{runId:n.runId,signal:n.signal,isStopRequested:()=>n.isStopRequested(),getInterruptReason:()=>n.getInterruptReason(),requestStop:e=>{n.requestStop(e)},beginFinalization:s,dispose(){s(),n.removeSignalHandlers(),n.dispose()},provision(e,r){if(a!==null)throw new Error("Run runtimes are already provisioned");a=R({productDir:i,graphs:e,lifecycle:n,runtimeOptions:r})},async acquire(e,r){const t=await o().acquire(e,r);d.set(t.leaseId,t);const{leaseId:c,runtimeId:p,graphKey:m,targetNames:u,services:f}=t;return{leaseId:c,runtimeId:p,graphKey:m,targetNames:u,services:f}},canAcquire:e=>o().canAcquire(e),async release(e,r){const t=d.get(e);t!==void 0&&(await o().release(t,r),d.delete(e))},async cleanupAll(){await a?.cleanupAll()},getStats:()=>a?.getStats()??[],getGraph(e){const r=a?.getGraph(e);return r?{key:r.key,targetNames:[...r.targetNames]}:null},getGraphFailure:e=>a?.getGraphFailure(e)??null,markGraphFailed:(e,r)=>{o().markGraphFailed(e,r)},async finalize(e,r){return s(),v(i,async t=>{const c=w(i).find(u=>u.runId===n.runId);if(c!==void 0&&c.services.length>0)throw new Error(e==="normal"?"Cannot commit a run while application service ownership remains published":"Cannot publish terminal failure while application service ownership remains published");let p=!0;const m=()=>{if(!p)throw new Error("Run finalization steps must run under the product lifecycle fence")};try{const u=await r({commit(f){return m(),n.commitFinalization(f)},async maintain(){return m(),F(t,n,l.evidence)}});return h(t,n),u}finally{p=!1}})}}}}}async function F(i,l,n){try{return await g(i,{evidence:n,localInfrastructure:!1,protectedLiveRunIds:new Set([l.runId])}),{status:"passed",error:null}}catch(s){return{status:"failed",error:q(s)}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{waitForPreparedRunStop as
|
|
1
|
+
import{waitForPreparedRunStop as S,drainRunServices as b,requestProcessStop as C,runManifestServiceKey as _,sameRunManifestIncarnation as g}from"../process/runs/stop.js";import{observeProcessCreationIdentity as m,captureProcessCreationIdentity as D}from"../process/index.js";import{normalizeOperationReason as P}from"../domain/operation-reason.js";import{RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION as N}from"../process/runs/schema.js";import{requireProductLifecycleFenceProductDir as v,withProductLifecycleFence as y}from"../process/lifecycle-fence.js";import{listRunManifests as q,readRunMaintenanceStopRequest as E,removeRunMaintenanceStopRequest as R,removeRunManifestFile as F,requireSafeRunId as T,writeRunMaintenanceStopRequest as L,persistRunShutdownDeadlines as O}from"../process/runs/manifest.js";export async function stopRunById(r,t,n){const o=T(t,"Testkit run id"),e=await y(r,i=>A(i,o,n));if(e===null)return!1;const s=await S(e);return await M(r,e,s,n.evidence),!0}export async function stopRunManifest(r,t,n){const o=T(t.runId,"Testkit run manifest runId"),e=await y(r,i=>A(i,o,n,t));if(e===null)return{runId:o,pid:t.pid,reason:P(n.reason,"stopped"),stopped:!0};const s=await S(e);return M(r,e,s,n.evidence)}function A(r,t,n,o=null){const e=v(r);let s=P(n.reason,"stopped");const i=q(e).find(a=>a.runId===t);if(i===void 0){if(o!==null&&o.pid>0&&o.processIdentity===null&&m(o.pid,o.processIdentity)==="unknown")throw new Error(`Cannot establish the creation identity of Testkit run process ${o.pid.toString()}; refusing unsafe stop`);return o!==null&&o.pid===0&&(n.evidence.runStopped(e,{runId:t,reason:s}),R(e,t)),null}if(o!==null&&!g(i,o))throw new Error(`Testkit run ${t} ownership changed before the stop request could be published`);const u=i.pid,f=u>0?m(u,i.processIdentity):"different";if(u===process.pid&&f==="matches")throw new Error(`Cannot stop active Testkit run ${t} from inside its own controller process`);if(f==="unknown")throw new Error(`Cannot establish the creation identity of Testkit run process ${u.toString()}; refusing unsafe stop`);let d=null,p=new Date().toISOString(),l=null;const I=E(e,t),c=I!==null&&I.targetIncarnationId===i.incarnationId&&I.targetPid===u&&I.targetProcessIdentity===i.processIdentity?I:null;c!==null&&(s=c.reason,p=c.requestedAt);let h=null;const w=()=>{if(h!==null)return h;const a=V(i,p);return f==="different"&&O(e,a),h=a,a};if(f==="matches"){if(l=i.processIdentity,l===null)throw new Error(`Testkit run process ${u.toString()} has no creation identity; refusing unsafe stop`);if(c!==null&&m(c.coordinatorPid,c.coordinatorProcessIdentity)!=="different")return d=c.coordinatorProcessIdentity,{controllerActive:!0,coordinatorProcessIdentity:d,graceMs:n.graceMs||2e3,manifest:w(),requestedAt:p,pid:u,productDir:e,reason:s,runId:t,targetProcessIdentity:l};if(d=D(process.pid),d===null)throw new Error("Could not capture the maintenance coordinator process identity");L(e,{coordinatorPid:process.pid,coordinatorProcessIdentity:d,reason:s,requestedAt:p,runId:t,schemaVersion:N,targetIncarnationId:i.incarnationId,targetPid:u,targetProcessIdentity:l}),w();try{process.platform!=="win32"&&C(u,l,"SIGINT")}catch(a){throw R(e,t),a}}return{controllerActive:f==="matches",coordinatorProcessIdentity:d,graceMs:n.graceMs||2e3,manifest:w(),requestedAt:p,pid:u,productDir:e,reason:s,runId:t,targetProcessIdentity:l}}async function M(r,t,n,o){for(;;){const e=await y(r,s=>$(s,t,n,o));if(e.result!==null)return e.result;await b(e.services,n)}}function $(r,t,n,o){const e=v(r),s=q(e).find(u=>u.runId===t.runId);if(s===void 0||!g(s,t.manifest))return U(e,t),{result:k(t),services:null};const i=s.services.filter(u=>!n.has(_(u)));return i.length>0?{result:null,services:i}:(o.runStopped(e,{runId:t.runId,reason:t.reason}),F(e,t.runId),{result:k(t),services:null})}function k(r){return{runId:r.runId,pid:r.pid,stopped:!0,reason:r.reason}}function U(r,t){const n=E(r,t.runId);n===null||n.coordinatorPid!==process.pid||n.coordinatorProcessIdentity!==t.coordinatorProcessIdentity||n.targetIncarnationId!==t.manifest.incarnationId||n.targetPid!==t.pid||n.targetProcessIdentity!==t.targetProcessIdentity||R(r,t.runId)}function V(r,t){return{...r,services:r.services.map(n=>n.shutdownTimeoutMs===void 0?n:{...n,shutdownDeadlineAt:Math.min(n.shutdownDeadlineAt??Number.MAX_VALUE,Date.parse(t)+n.shutdownTimeoutMs)})}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildRuntimeIds as
|
|
1
|
+
import{buildRuntimeIds as M}from"../domain/runtime-identity.js";import{formatError as h}from"../kernel/error-serialization.js";import{cleanupRuntimeInstanceContext as S,createRuntimeInstanceContext as U,ensureRuntimeInstanceReady as N}from"./runtime-contexts.js";import{timeOperation as A}from"../evidence/operation-timings.js";import{createReadyEnvironment as K}from"./ready-environment.js";import{buildRuntimeLabel as T}from"./template.js";export function createRuntimeManager({productDir:e,graphs:n,lifecycle:a,runtimeOptions:r={}}){const i=new Map(n.map(t=>[t.key,t])),u=new Map,o=B(n),m=new Map,d={...r,provisionedSharedDatabases:new Set};let R=!1;return{canAcquire(t){if(m.has(t.graphKey))return!1;const s=C(u,i,t);return q(s,t,o)!==null},async acquire(t,s={}){const l=C(u,i,t);if(a.isStopRequested())throw a.signal.reason||new Error("testkit run interrupted");const g=t.leaseId,c=await F(r,s,"testkit.runtime.slot_claim",{summary:"claim runtime slot",scope:{graphKey:t.graphKey,taskId:t.id,serviceName:t.serviceName||null,targetName:t.targetName||null},metadata:{decision:"claim",resourceCost:v(t).toString()}},()=>{const p=$(l,t,o);if(!p)throw new Error(`Task ${t.id.toString()} was claimed before runtime capacity was available`);return p});R||=c.graph.runtimeConfigs.some(p=>p.testkit.database);try{const p=T(c.graph.dirName,c.runtimeId);s.onRuntimeAssigned?.({runtimeId:c.runtimeId,services:c.graph.runtimeConfigs.map(({name:L})=>({serviceName:L,runtimeLabel:p}))});const y=await E(c,e,t,a,d,s);return a.signal.throwIfAborted(),{...K(y,g),resourceCost:v(t),slot:c,context:y}}catch(p){throw x(c,v(t)),c.context||I(c,o),await f(c,a,o),a.isStopRequested()||b(m,t.graphKey,h(p)),p}},async release(t,s={}){t?.slot&&(s.invalidate&&(t.slot.draining=!0),t.released||(x(t.slot,t.resourceCost),t.released=!0),t.slot.draining&&t.slot.activeLeaseCount===0&&await f(t.slot,a,o))},async cleanupAll(){const t=[];for(const g of u.values())for(const c of g.slots){c.draining=!0;try{await f(c,a,o)}catch(p){t.push({slot:c,error:p})}}const s=t.length>0?new AggregateError(t.map(g=>g.error),`Failed to clean up ${t.length.toString()} runtime ${t.length===1?"slot":"slots"}: ${t.map(g=>`${g.slot.runtimeId}: ${h(g.error)}`).join("; ")}`):null,l=R?await G(e):null;if(s!==null&&l!==null)throw new AggregateError([s,l.error],`${s.message}; ${h(l.error)}`);if(s!==null)throw s;if(l!==null)throw l.error},getStats(){return[...u.values()].map(t=>({graphKey:t.slots[0]?.graph.key||null,targetNames:[...t.slots.at(0)?.graph.targetNames??[]],maxConcurrentTasks:Number.isFinite(t.slots[0]?.graph.maxConcurrentTasks)?t.slots[0]?.graph.maxConcurrentTasks??null:null,runtimeCount:t.slots.length,runtimes:t.slots.map(s=>({runtimeId:s.runtimeId,peakLeaseCount:s.peakLeaseCount,peakResourceUnits:s.peakResourceUnits,resourceReservations:s.resourceReservations.map(l=>({...l}))})),resourceBudgets:[...o.values()].filter(s=>t.slots.some(l=>l.graph.resourceReservations.some(g=>g.key===s.key))).map(s=>({key:s.key,capacity:s.capacity,reserved:s.reserved,available:s.available,active:s.active,peakActive:s.peakActive}))})).sort((t,s)=>(t.graphKey||"").localeCompare(s.graphKey||""))},getGraphFailure(t){return m.get(t)??null},getGraph(t){return i.get(t)||null},markGraphFailed(t,s){b(m,t,s)}}}function C(e,n,a){const r=e.get(a.graphKey);if(r)return r;const i=n.get(a.graphKey);if(!i)throw new Error(`Unknown graph "${a.graphKey}"`);const o={slots:M(i.instanceCount).map(m=>({graph:i,runtimeId:m,context:null,activeLeaseCount:0,activeResourceUnits:0,resourceReservations:[],peakLeaseCount:0,peakResourceUnits:0,draining:!1}))};return e.set(a.graphKey,o),o}function $(e,n,a){const r=v(n),i=e.slots.filter(o=>!o.draining);if(i.length===0)return null;const u=[...i].filter(o=>w(o,r)&&k(o,a)).sort((o,m)=>o.activeResourceUnits-m.activeResourceUnits||o.activeLeaseCount-m.activeLeaseCount||o.runtimeId.localeCompare(m.runtimeId)).at(0);return u?(P(u,a),u.activeLeaseCount+=1,u.activeResourceUnits+=r,u.peakLeaseCount=Math.max(u.peakLeaseCount,u.activeLeaseCount),u.peakResourceUnits=Math.max(u.peakResourceUnits,u.activeResourceUnits),u):null}function x(e,n){e.activeLeaseCount=Math.max(0,e.activeLeaseCount-1),e.activeResourceUnits=Math.max(0,e.activeResourceUnits-n)}async function E(e,n,a,r,i,u={}){e.context||(e.context=U(e.runtimeId,e.graph,n),r.trackGraphContext(e.context));const o=i.operationTimingRegistry||null,m=o?.start({name:"testkit.runtime.ensure",kind:"runtime",summary:"ensure runtime instance ready",parentId:u.operationTimingParentId||null,scope:{graphKey:e.graph.key,runtimeId:e.runtimeId,serviceName:a.serviceName,targetName:a.targetName}})??null;try{await N(e.context,r,i),o?.finish(m,{status:"passed"})}catch(d){throw o?.finish(m,{status:"failed",error:h(d)}),d}return e.context}function F(e,n,a,r,i){return A(e.operationTimingRegistry||null,{name:a,kind:"runtime",summary:r.summary||null,parentId:n.operationTimingParentId||null,scope:r.scope||null,metadata:r.metadata||null},i)}async function G(e){try{const{stopRetainedLocalPostgres:n}=await import("../database/index.js");return await n(e),null}catch(n){return{error:n}}}async function f(e,n,a){!e.context||e.activeLeaseCount>0||(await S(e.context,n),I(e,a),e.context=null,e.draining=!1)}function q(e,n,a){const r=v(n);return e.slots.find(i=>!i.draining&&w(i,r)&&k(i,a))||null}function w(e,n){return e.activeResourceUnits+n<=e.graph.maxConcurrentTasks}function B(e){const n=new Map;for(const a of e)for(const r of a.resourceReservations){const i=n.get(r.key)||{key:r.key,capacity:r.capacity||0,reserved:r.reserved||0,available:r.available||Math.max(0,(r.capacity||0)-(r.reserved||0)),active:0,peakActive:0};i.capacity=Math.max(i.capacity,r.capacity||0),i.reserved=Math.max(i.reserved,r.reserved||0),i.available=Math.max(0,i.capacity-i.reserved),n.set(r.key,i)}return n}function k(e,n){return e.resourceReservations.length>0?!0:e.graph.resourceReservations.every(a=>{const r=n.get(a.key);return r?r.active+a.perRuntime<=r.available:!0})}function P(e,n){if(e.resourceReservations.length>0)return;const a=[];for(const r of e.graph.resourceReservations){const i=n.get(r.key);i&&(i.active+=r.perRuntime,i.peakActive=Math.max(i.peakActive,i.active),a.push({key:r.key,units:r.perRuntime,services:r.services}))}e.resourceReservations=a}function I(e,n){for(const a of e.resourceReservations){const r=n.get(a.key);r&&(r.active=Math.max(0,r.active-a.units))}e.resourceReservations=[]}function b(e,n,a){!n||e.has(n)||e.set(n,a)}function v(e){return e.resourceCost??1}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isUnknownArray as
|
|
1
|
+
import{rejectUnknownKeys as e}from"../kernel/known-keys.js";import{isUnknownArray as o,isUnknownRecord as s}from"../kernel/unknown-values.js";export function normalizeArtifactArchiveConfig(r){const t=normalizeOptionalRecord(r,"artifacts"),i=normalizeOptionalRecord(t?.retention,"artifacts.retention"),n=normalizeOptionalRecord(t?.publication,"artifacts.publication");return t&&e(t,["publication","retention"],"artifacts"),i&&e(i,["maxAgeDays","maxRuns","preserveSuites"],"artifacts.retention"),n&&e(n,["directory","required"],"artifacts.publication"),{retention:{maxRuns:a(i?.maxRuns,"artifacts.retention.maxRuns"),maxAgeDays:f(i?.maxAgeDays,"artifacts.retention.maxAgeDays"),preserveSuites:u(i?.preserveSuites,"artifacts.retention.preserveSuites")},publication:n?{directory:c(n.directory,"artifacts.publication.directory"),required:m(n.required,"artifacts.publication.required")}:null}}function a(r,t){if(r==null)return null;if(typeof r!="number"||!Number.isInteger(r)||r<=0)throw new Error(`${t} must be a positive integer`);return r}function f(r,t){if(r==null)return null;if(typeof r!="number"||!Number.isFinite(r)||r<=0)throw new Error(`${t} must be a positive number`);return r}function u(r,t){if(r==null)return[];if(!o(r))throw new Error(`${t} must be an array of non-empty strings`);const i=r.map(n=>{if(typeof n!="string"||n.trim().length===0)throw new Error(`${t} must be an array of non-empty strings`);return n.trim()});return[...new Set(i)].sort()}function c(r,t){if(typeof r!="string"||r.trim().length===0)throw new Error(`${t} is required`);return r.trim()}function m(r,t){if(r===void 0)return!1;if(typeof r!="boolean")throw new Error(`${t} must be a boolean`);return r}export function normalizeOptionalRecord(r,t){if(r!=null){if(!s(r))throw new Error(`${t} must be an object`);return r}}
|