@guanlin4924/devflow-pi 0.1.94 → 0.1.95
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 +4 -4
- package/node_modules/@playwright/test/LICENSE +202 -0
- package/node_modules/@playwright/test/NOTICE +5 -0
- package/node_modules/@playwright/test/README.md +318 -0
- package/node_modules/@playwright/test/cli.js +19 -0
- package/node_modules/@playwright/test/index.d.ts +18 -0
- package/node_modules/@playwright/test/index.js +17 -0
- package/node_modules/@playwright/test/index.mjs +18 -0
- package/node_modules/@playwright/test/package.json +35 -0
- package/node_modules/@playwright/test/reporter.d.ts +17 -0
- package/node_modules/@playwright/test/reporter.js +17 -0
- package/node_modules/@playwright/test/reporter.mjs +17 -0
- package/node_modules/playwright/LICENSE +202 -0
- package/node_modules/playwright/NOTICE +5 -0
- package/node_modules/playwright/README.md +318 -0
- package/node_modules/playwright/ThirdPartyNotices.txt +14 -0
- package/node_modules/playwright/cli.js +19 -0
- package/node_modules/playwright/index.d.ts +17 -0
- package/node_modules/playwright/index.js +17 -0
- package/node_modules/playwright/index.mjs +18 -0
- package/node_modules/playwright/jsx-runtime.js +42 -0
- package/node_modules/playwright/jsx-runtime.mjs +21 -0
- package/node_modules/playwright/lib/agents/agentParser.js +89 -0
- package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +34 -0
- package/node_modules/playwright/lib/agents/generateAgents.js +395 -0
- package/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +88 -0
- package/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/node_modules/playwright/lib/agents/playwright-test-healer.agent.md +56 -0
- package/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/node_modules/playwright/lib/agents/playwright-test-planner.agent.md +74 -0
- package/node_modules/playwright/lib/cli/reportActions.js +78 -0
- package/node_modules/playwright/lib/cli/testActions.js +214 -0
- package/node_modules/playwright/lib/common/index.js +2923 -0
- package/node_modules/playwright/lib/common/index.js.txt +34 -0
- package/node_modules/playwright/lib/errorContext.js +129 -0
- package/node_modules/playwright/lib/globals.js +58 -0
- package/node_modules/playwright/lib/index.js +815 -0
- package/node_modules/playwright/lib/isomorphic.js +260 -0
- package/node_modules/playwright/lib/isomorphic.js.txt +9 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js +34 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js.txt +9 -0
- package/node_modules/playwright/lib/matchers/expect.js +13490 -0
- package/node_modules/playwright/lib/matchers/expect.js.LICENSE +640 -0
- package/node_modules/playwright/lib/matchers/expect.js.txt +67 -0
- package/node_modules/playwright/lib/mcp/test/browserBackend.js +135 -0
- package/node_modules/playwright/lib/mcp/test/generatorTools.js +122 -0
- package/node_modules/playwright/lib/mcp/test/plannerTools.js +150 -0
- package/node_modules/playwright/lib/mcp/test/seed.js +82 -0
- package/node_modules/playwright/lib/mcp/test/streams.js +44 -0
- package/node_modules/playwright/lib/mcp/test/testBackend.js +99 -0
- package/node_modules/playwright/lib/mcp/test/testContext.js +306 -0
- package/node_modules/playwright/lib/mcp/test/testTool.js +30 -0
- package/node_modules/playwright/lib/mcp/test/testTools.js +98 -0
- package/node_modules/playwright/lib/package.js +47 -0
- package/node_modules/playwright/lib/program.js +241 -0
- package/node_modules/playwright/lib/runner/index.js +8527 -0
- package/node_modules/playwright/lib/runner/index.js.txt +61 -0
- package/node_modules/playwright/lib/transform/babelBundle.js +72274 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +2384 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +325 -0
- package/node_modules/playwright/lib/transform/esmLoader.js +8054 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +355 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +60 -0
- package/node_modules/playwright/lib/util.js +373 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +1861 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +19 -0
- package/node_modules/playwright/package.json +55 -0
- package/node_modules/playwright/test.d.ts +18 -0
- package/node_modules/playwright/test.js +24 -0
- package/node_modules/playwright/test.mjs +35 -0
- package/node_modules/playwright/types/test.d.ts +11074 -0
- package/node_modules/playwright/types/testReporter.d.ts +950 -0
- package/node_modules/playwright-core/LICENSE +202 -0
- package/node_modules/playwright-core/NOTICE +5 -0
- package/node_modules/playwright-core/README.md +3 -0
- package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
- package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
- package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/browsers.json +52 -0
- package/node_modules/playwright-core/cli.js +21 -0
- package/node_modules/playwright-core/index.d.ts +17 -0
- package/node_modules/playwright-core/index.js +17 -0
- package/node_modules/playwright-core/index.mjs +28 -0
- package/node_modules/playwright-core/lib/bootstrap.js +88 -0
- package/node_modules/playwright-core/lib/coreBundle.js +75813 -0
- package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
- package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
- package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
- package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
- package/node_modules/playwright-core/lib/package.js +50 -0
- package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/server/electron/loader.js +116 -0
- package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
- package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
- package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
- package/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +721 -0
- package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
- package/node_modules/playwright-core/lib/tools/cli-client/output.js +351 -0
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +418 -0
- package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +259 -0
- package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +425 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +174 -0
- package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
- package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
- package/node_modules/playwright-core/lib/utilsBundle.js +85357 -0
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/node_modules/playwright-core/lib/xdg-open +1267 -0
- package/node_modules/playwright-core/package.json +34 -0
- package/node_modules/playwright-core/types/protocol.d.ts +24645 -0
- package/node_modules/playwright-core/types/structs.d.ts +52 -0
- package/node_modules/playwright-core/types/types.d.ts +26632 -0
- package/package.json +12 -2
- package/plugins/devflow-core/extensions/devflow-core.ts +431 -7
- package/plugins/devflow-core/runtime/hooks/hook_common.py +269 -0
- package/plugins/devflow-core/runtime/hooks/index_gate.py +29 -16
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +407 -71
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_pins.py +433 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_store.py +137 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +87 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +318 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +134 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_surface.py +138 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +515 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tc_hash.py +132 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +102 -8
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +13 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +13 -34
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -1
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +68 -140
- package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization_database.py +1 -1
- package/plugins/devflow-implementation/skills/workflow-execute-phase/references/execute-phase.md +3 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +23 -4
- package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +872 -0
- package/plugins/devflow-quality/runtime/playwright/playwright.config.mjs +46 -0
- package/plugins/devflow-quality/runtime/playwright/reporter.mjs +311 -0
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +1308 -0
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +11 -0
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +71 -58
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/api-mainflow-rules.md +20 -15
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/codebase-discovery.md +3 -1
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +115 -192
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/mainflow-selection.md +27 -23
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +78 -153
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +36 -83
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +497 -556
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/export_legacy_testagent.py +647 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/normalize_case_payloads.py +10 -3
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/validate_case_payloads.py +165 -275
- package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +61 -0
- package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +4 -4
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +15 -9
- package/src/approval-store.ts +340 -0
- package/src/constants.ts +1 -1
- package/src/doctor.ts +259 -2
- package/src/initialization-store.ts +48 -1
- package/src/paths.ts +12 -1
- package/src/project-context.ts +24 -1
- package/templates/omp/hook-config.json +6 -0
|
@@ -0,0 +1,1308 @@
|
|
|
1
|
+
// plugins/devflow-quality/runtime/playwright/src/runner.spec.ts
|
|
2
|
+
import path4 from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { test } from "@playwright/test";
|
|
5
|
+
|
|
6
|
+
// plugins/devflow-quality/runtime/playwright/src/execute.ts
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import fs2 from "node:fs";
|
|
9
|
+
import path3 from "node:path";
|
|
10
|
+
|
|
11
|
+
// plugins/devflow-quality/runtime/playwright/src/schema-validate.ts
|
|
12
|
+
var SUPPORTED = new Set([
|
|
13
|
+
"$schema",
|
|
14
|
+
"$id",
|
|
15
|
+
"$defs",
|
|
16
|
+
"$ref",
|
|
17
|
+
"title",
|
|
18
|
+
"description",
|
|
19
|
+
"type",
|
|
20
|
+
"const",
|
|
21
|
+
"enum",
|
|
22
|
+
"pattern",
|
|
23
|
+
"minLength",
|
|
24
|
+
"maxLength",
|
|
25
|
+
"minimum",
|
|
26
|
+
"maximum",
|
|
27
|
+
"properties",
|
|
28
|
+
"patternProperties",
|
|
29
|
+
"additionalProperties",
|
|
30
|
+
"required",
|
|
31
|
+
"minProperties",
|
|
32
|
+
"maxProperties",
|
|
33
|
+
"items",
|
|
34
|
+
"minItems",
|
|
35
|
+
"maxItems",
|
|
36
|
+
"uniqueItems",
|
|
37
|
+
"oneOf",
|
|
38
|
+
"anyOf"
|
|
39
|
+
]);
|
|
40
|
+
function typeOf(value) {
|
|
41
|
+
if (value === null)
|
|
42
|
+
return "null";
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return "array";
|
|
45
|
+
if (typeof value === "number")
|
|
46
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
47
|
+
return typeof value;
|
|
48
|
+
}
|
|
49
|
+
function typeMatches(value, type) {
|
|
50
|
+
const actual = typeOf(value);
|
|
51
|
+
if (type === "number")
|
|
52
|
+
return actual === "number" || actual === "integer";
|
|
53
|
+
return actual === type;
|
|
54
|
+
}
|
|
55
|
+
function deepEqual(a, b) {
|
|
56
|
+
if (a === b)
|
|
57
|
+
return true;
|
|
58
|
+
if (typeof a !== typeof b || a === null || b === null)
|
|
59
|
+
return false;
|
|
60
|
+
if (Array.isArray(a)) {
|
|
61
|
+
if (!Array.isArray(b) || a.length !== b.length)
|
|
62
|
+
return false;
|
|
63
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
64
|
+
}
|
|
65
|
+
if (typeof a === "object") {
|
|
66
|
+
if (Array.isArray(b))
|
|
67
|
+
return false;
|
|
68
|
+
const ak = Object.keys(a).sort();
|
|
69
|
+
const bk = Object.keys(b).sort();
|
|
70
|
+
if (!deepEqual(ak, bk))
|
|
71
|
+
return false;
|
|
72
|
+
return ak.every((k) => deepEqual(a[k], b[k]));
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
var regexCache = new Map;
|
|
77
|
+
function regex(pattern) {
|
|
78
|
+
let re = regexCache.get(pattern);
|
|
79
|
+
if (!re) {
|
|
80
|
+
re = new RegExp(pattern, "u");
|
|
81
|
+
regexCache.set(pattern, re);
|
|
82
|
+
}
|
|
83
|
+
return re;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class SchemaValidator {
|
|
87
|
+
root;
|
|
88
|
+
constructor(root) {
|
|
89
|
+
this.root = root;
|
|
90
|
+
}
|
|
91
|
+
validate(value) {
|
|
92
|
+
const errors = [];
|
|
93
|
+
this.check(this.root, value, "", errors);
|
|
94
|
+
return errors;
|
|
95
|
+
}
|
|
96
|
+
resolveRef(ref) {
|
|
97
|
+
if (!ref.startsWith("#/"))
|
|
98
|
+
throw new Error(`unsupported $ref ${ref}`);
|
|
99
|
+
let node = this.root;
|
|
100
|
+
for (const part of ref.slice(2).split("/")) {
|
|
101
|
+
node = node?.[part.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
102
|
+
}
|
|
103
|
+
if (node === undefined)
|
|
104
|
+
throw new Error(`unresolved $ref ${ref}`);
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
check(schema, value, at, errors) {
|
|
108
|
+
if (schema === true)
|
|
109
|
+
return;
|
|
110
|
+
if (schema === false) {
|
|
111
|
+
errors.push({ path: at, message: "not allowed" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(schema)) {
|
|
115
|
+
if (!SUPPORTED.has(key)) {
|
|
116
|
+
errors.push({ path: at, message: `schema keyword ${key} unsupported by runtime validator` });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const s = schema;
|
|
121
|
+
if (s.$ref) {
|
|
122
|
+
this.check(this.resolveRef(s.$ref), value, at, errors);
|
|
123
|
+
}
|
|
124
|
+
if (s.type !== undefined) {
|
|
125
|
+
const types = Array.isArray(s.type) ? s.type : [s.type];
|
|
126
|
+
if (!types.some((t) => typeMatches(value, t))) {
|
|
127
|
+
errors.push({ path: at, message: `expected ${types.join("|")}, got ${typeOf(value)}` });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if ("const" in s && !deepEqual(value, s.const))
|
|
132
|
+
errors.push({ path: at, message: `must equal ${JSON.stringify(s.const)}` });
|
|
133
|
+
if (s.enum && !s.enum.some((e) => deepEqual(e, value))) {
|
|
134
|
+
errors.push({ path: at, message: `must be one of ${JSON.stringify(s.enum)}` });
|
|
135
|
+
}
|
|
136
|
+
if (typeof value === "string") {
|
|
137
|
+
const length = [...value].length;
|
|
138
|
+
if (s.minLength !== undefined && length < s.minLength)
|
|
139
|
+
errors.push({ path: at, message: `shorter than ${s.minLength}` });
|
|
140
|
+
if (s.maxLength !== undefined && length > s.maxLength)
|
|
141
|
+
errors.push({ path: at, message: `longer than ${s.maxLength}` });
|
|
142
|
+
if (s.pattern !== undefined && !regex(s.pattern).test(value))
|
|
143
|
+
errors.push({ path: at, message: `does not match ${s.pattern}` });
|
|
144
|
+
}
|
|
145
|
+
if (typeof value === "number") {
|
|
146
|
+
if (s.minimum !== undefined && value < s.minimum)
|
|
147
|
+
errors.push({ path: at, message: `below minimum ${s.minimum}` });
|
|
148
|
+
if (s.maximum !== undefined && value > s.maximum)
|
|
149
|
+
errors.push({ path: at, message: `above maximum ${s.maximum}` });
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
if (s.minItems !== undefined && value.length < s.minItems)
|
|
153
|
+
errors.push({ path: at, message: `fewer than ${s.minItems} items` });
|
|
154
|
+
if (s.maxItems !== undefined && value.length > s.maxItems)
|
|
155
|
+
errors.push({ path: at, message: `more than ${s.maxItems} items` });
|
|
156
|
+
if (s.uniqueItems) {
|
|
157
|
+
for (let i = 0;i < value.length; i++) {
|
|
158
|
+
for (let j = i + 1;j < value.length; j++) {
|
|
159
|
+
if (deepEqual(value[i], value[j]))
|
|
160
|
+
errors.push({ path: at, message: "items not unique" });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (s.items !== undefined)
|
|
165
|
+
value.forEach((item, i) => this.check(s.items, item, `${at}/${i}`, errors));
|
|
166
|
+
}
|
|
167
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
168
|
+
const obj = value;
|
|
169
|
+
const keys = Object.keys(obj);
|
|
170
|
+
if (s.minProperties !== undefined && keys.length < s.minProperties)
|
|
171
|
+
errors.push({ path: at, message: `fewer than ${s.minProperties} properties` });
|
|
172
|
+
if (s.maxProperties !== undefined && keys.length > s.maxProperties)
|
|
173
|
+
errors.push({ path: at, message: `more than ${s.maxProperties} properties` });
|
|
174
|
+
for (const req of s.required ?? [])
|
|
175
|
+
if (!(req in obj))
|
|
176
|
+
errors.push({ path: at, message: `missing required ${req}` });
|
|
177
|
+
for (const key of keys) {
|
|
178
|
+
let matched = false;
|
|
179
|
+
if (s.properties && Object.prototype.hasOwnProperty.call(s.properties, key)) {
|
|
180
|
+
matched = true;
|
|
181
|
+
this.check(s.properties[key], obj[key], `${at}/${key}`, errors);
|
|
182
|
+
}
|
|
183
|
+
for (const [pattern, sub] of Object.entries(s.patternProperties ?? {})) {
|
|
184
|
+
if (regex(pattern).test(key)) {
|
|
185
|
+
matched = true;
|
|
186
|
+
this.check(sub, obj[key], `${at}/${key}`, errors);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!matched && s.additionalProperties !== undefined) {
|
|
190
|
+
this.check(s.additionalProperties, obj[key], `${at}/${key}`, errors);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (s.oneOf) {
|
|
195
|
+
const passing = s.oneOf.filter((sub) => this.probe(sub, value));
|
|
196
|
+
if (passing.length !== 1)
|
|
197
|
+
errors.push({ path: at, message: `must match exactly one alternative (matched ${passing.length})` });
|
|
198
|
+
}
|
|
199
|
+
if (s.anyOf && !s.anyOf.some((sub) => this.probe(sub, value))) {
|
|
200
|
+
errors.push({ path: at, message: "must match at least one alternative" });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
probe(schema, value) {
|
|
204
|
+
const errors = [];
|
|
205
|
+
this.check(schema, value, "", errors);
|
|
206
|
+
return errors.length === 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// plugins/devflow-quality/runtime/playwright/src/dsl.ts
|
|
211
|
+
var UI_ACTIONS = new Set([
|
|
212
|
+
"goto",
|
|
213
|
+
"click",
|
|
214
|
+
"fill",
|
|
215
|
+
"select",
|
|
216
|
+
"check",
|
|
217
|
+
"uncheck",
|
|
218
|
+
"wait_for",
|
|
219
|
+
"assert_visible",
|
|
220
|
+
"assert_text",
|
|
221
|
+
"assert_url",
|
|
222
|
+
"screenshot"
|
|
223
|
+
]);
|
|
224
|
+
var API_ACTIONS = new Set(["request"]);
|
|
225
|
+
var BUILTIN_VARIABLES = new Set(["execution_id", "namespace"]);
|
|
226
|
+
var VAR_REF = /\$\{([^}]*)\}/g;
|
|
227
|
+
var VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
228
|
+
function collectRefs(value, out = new Set) {
|
|
229
|
+
if (typeof value === "string") {
|
|
230
|
+
for (const m of value.matchAll(VAR_REF))
|
|
231
|
+
out.add(m[1]);
|
|
232
|
+
} else if (Array.isArray(value)) {
|
|
233
|
+
value.forEach((v) => collectRefs(v, out));
|
|
234
|
+
} else if (value && typeof value === "object") {
|
|
235
|
+
Object.values(value).forEach((v) => collectRefs(v, out));
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
function validateDsl(dsl, schema) {
|
|
240
|
+
const issues = new SchemaValidator(schema).validate(dsl);
|
|
241
|
+
if (issues.length)
|
|
242
|
+
return issues;
|
|
243
|
+
return semanticIssues(dsl);
|
|
244
|
+
}
|
|
245
|
+
function semanticIssues(dsl) {
|
|
246
|
+
const issues = [];
|
|
247
|
+
const phases = [
|
|
248
|
+
["data/setup", dsl.data.setup],
|
|
249
|
+
["steps", dsl.steps],
|
|
250
|
+
["data/cleanup", dsl.data.cleanup]
|
|
251
|
+
];
|
|
252
|
+
const ids = new Set;
|
|
253
|
+
const screenshotNames = new Set;
|
|
254
|
+
const defined = new Set([...BUILTIN_VARIABLES, ...Object.keys(dsl.variables)]);
|
|
255
|
+
for (const name of Object.keys(dsl.variables)) {
|
|
256
|
+
if (BUILTIN_VARIABLES.has(name))
|
|
257
|
+
issues.push({ path: `/variables/${name}`, message: "shadows a built-in variable" });
|
|
258
|
+
}
|
|
259
|
+
for (const [name, variable] of Object.entries(dsl.variables)) {
|
|
260
|
+
const refs = collectRefs(variable.value);
|
|
261
|
+
if (refs.size)
|
|
262
|
+
issues.push({ path: `/variables/${name}`, message: "variable values must be literals" });
|
|
263
|
+
}
|
|
264
|
+
for (const ref of collectRefs(dsl.data.namespace)) {
|
|
265
|
+
if (ref !== "execution_id")
|
|
266
|
+
issues.push({ path: "/data/namespace", message: `namespace may only reference \${execution_id}, got \${${ref}}` });
|
|
267
|
+
}
|
|
268
|
+
for (const [base, steps] of phases) {
|
|
269
|
+
steps.forEach((step, index) => {
|
|
270
|
+
const at = `/${base}/${index}`;
|
|
271
|
+
if (ids.has(step.id))
|
|
272
|
+
issues.push({ path: `${at}/id`, message: `duplicate step id ${step.id}` });
|
|
273
|
+
ids.add(step.id);
|
|
274
|
+
const allowed = step.driver === "api" ? API_ACTIONS : UI_ACTIONS;
|
|
275
|
+
if (!allowed.has(step.action))
|
|
276
|
+
issues.push({ path: `${at}/action`, message: `unknown action ${step.action}` });
|
|
277
|
+
for (const ref of collectRefs(step)) {
|
|
278
|
+
if (!VAR_NAME.test(ref))
|
|
279
|
+
issues.push({ path: at, message: `malformed variable reference \${${ref}}` });
|
|
280
|
+
else if (!defined.has(ref))
|
|
281
|
+
issues.push({ path: at, message: `variable \${${ref}} is not defined before use` });
|
|
282
|
+
}
|
|
283
|
+
if (step.action === "screenshot") {
|
|
284
|
+
const file = String(step.name ?? step.id);
|
|
285
|
+
if (screenshotNames.has(file))
|
|
286
|
+
issues.push({ path: at, message: `duplicate screenshot name ${file}` });
|
|
287
|
+
screenshotNames.add(file);
|
|
288
|
+
}
|
|
289
|
+
if (step.driver === "api") {
|
|
290
|
+
const assertions = step.assertions ?? [];
|
|
291
|
+
assertions.forEach((a, i) => {
|
|
292
|
+
const needsValue = a.op !== "exists" && a.op !== "not_exists";
|
|
293
|
+
if (needsValue && !("value" in a))
|
|
294
|
+
issues.push({ path: `${at}/assertions/${i}`, message: `op ${a.op} requires value` });
|
|
295
|
+
if (!needsValue && "value" in a)
|
|
296
|
+
issues.push({ path: `${at}/assertions/${i}`, message: `op ${a.op} takes no value` });
|
|
297
|
+
});
|
|
298
|
+
const method = String(step.method);
|
|
299
|
+
if (step.retry && (method === "POST" || method === "PATCH") && step.idempotent !== true) {
|
|
300
|
+
issues.push({ path: `${at}/retry`, message: `${method} retry requires idempotent=true` });
|
|
301
|
+
}
|
|
302
|
+
for (const name of Object.keys(step.extract ?? {})) {
|
|
303
|
+
if (BUILTIN_VARIABLES.has(name) || name in dsl.variables) {
|
|
304
|
+
issues.push({ path: `${at}/extract/${name}`, message: "extract may not overwrite a declared or built-in variable" });
|
|
305
|
+
}
|
|
306
|
+
defined.add(name);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return issues;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
class UnsetVariableError extends Error {
|
|
315
|
+
variable;
|
|
316
|
+
constructor(variable) {
|
|
317
|
+
super(`variable \${${variable}} has no value`);
|
|
318
|
+
this.variable = variable;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function interpolate(text, vars) {
|
|
322
|
+
return text.replace(VAR_REF, (_, name) => {
|
|
323
|
+
if (!vars.has(name))
|
|
324
|
+
throw new UnsetVariableError(name);
|
|
325
|
+
const v = vars.get(name);
|
|
326
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function interpolateDeep(value, vars) {
|
|
330
|
+
if (typeof value === "string") {
|
|
331
|
+
const whole = /^\$\{([A-Za-z_][A-Za-z0-9_]{0,63})\}$/.exec(value);
|
|
332
|
+
if (whole) {
|
|
333
|
+
if (!vars.has(whole[1]))
|
|
334
|
+
throw new UnsetVariableError(whole[1]);
|
|
335
|
+
return vars.get(whole[1]);
|
|
336
|
+
}
|
|
337
|
+
return interpolate(value, vars);
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(value))
|
|
340
|
+
return value.map((v) => interpolateDeep(v, vars));
|
|
341
|
+
if (value && typeof value === "object") {
|
|
342
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, interpolateDeep(v, vars)]));
|
|
343
|
+
}
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
function jsonPointer(doc, pointer) {
|
|
347
|
+
if (pointer === "")
|
|
348
|
+
return { found: true, value: doc };
|
|
349
|
+
let node = doc;
|
|
350
|
+
for (const raw of pointer.slice(1).split("/")) {
|
|
351
|
+
const token = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
352
|
+
if (Array.isArray(node)) {
|
|
353
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token) || Number(token) >= node.length)
|
|
354
|
+
return { found: false };
|
|
355
|
+
node = node[Number(token)];
|
|
356
|
+
} else if (node && typeof node === "object" && Object.prototype.hasOwnProperty.call(node, token)) {
|
|
357
|
+
node = node[token];
|
|
358
|
+
} else {
|
|
359
|
+
return { found: false };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { found: true, value: node };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// plugins/devflow-quality/runtime/playwright/src/network.ts
|
|
366
|
+
var SUBRESOURCE_POLICIES = new Set(["same-site-plus-declared", "declared-only"]);
|
|
367
|
+
var IMPLICIT_SCHEMES = new Set(["data:", "blob:", "about:"]);
|
|
368
|
+
|
|
369
|
+
class ProfileError extends Error {
|
|
370
|
+
code = "environment_profile_invalid";
|
|
371
|
+
}
|
|
372
|
+
function normalizeOrigin(value, field) {
|
|
373
|
+
let u;
|
|
374
|
+
try {
|
|
375
|
+
u = new URL(value);
|
|
376
|
+
} catch {
|
|
377
|
+
throw new ProfileError(`${field}: not an absolute URL: ${value}`);
|
|
378
|
+
}
|
|
379
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
380
|
+
throw new ProfileError(`${field}: only http(s) origins allowed: ${value}`);
|
|
381
|
+
return u.origin;
|
|
382
|
+
}
|
|
383
|
+
function buildPolicy(profile) {
|
|
384
|
+
if (!profile || typeof profile !== "object")
|
|
385
|
+
throw new ProfileError("profile must be a JSON object");
|
|
386
|
+
if (typeof profile.base_url !== "string")
|
|
387
|
+
throw new ProfileError("base_url is required");
|
|
388
|
+
const listOf = (value, field) => {
|
|
389
|
+
if (value === undefined)
|
|
390
|
+
return [];
|
|
391
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string"))
|
|
392
|
+
throw new ProfileError(`${field} must be an array of strings`);
|
|
393
|
+
return value;
|
|
394
|
+
};
|
|
395
|
+
const navigation = new Set(listOf(profile.navigation_origins, "navigation_origins").map((o) => normalizeOrigin(o, "navigation_origins")));
|
|
396
|
+
const api = new Set(listOf(profile.api_origins, "api_origins").map((o) => normalizeOrigin(o, "api_origins")));
|
|
397
|
+
if (navigation.size === 0)
|
|
398
|
+
throw new ProfileError("navigation_origins must not be empty");
|
|
399
|
+
const policy = profile.subresources?.policy ?? "same-site-plus-declared";
|
|
400
|
+
if (!SUBRESOURCE_POLICIES.has(policy))
|
|
401
|
+
throw new ProfileError(`unknown subresources.policy ${policy}`);
|
|
402
|
+
const subresourceOrigins = new Set(listOf(profile.subresources?.allowed_origins, "subresources.allowed_origins").map((o) => normalizeOrigin(o, "subresources.allowed_origins")));
|
|
403
|
+
const baseUrl = new URL(profile.base_url).toString();
|
|
404
|
+
if (!navigation.has(new URL(baseUrl).origin))
|
|
405
|
+
throw new ProfileError("base_url origin must be listed in navigation_origins");
|
|
406
|
+
const apiBaseUrl = profile.api_base_url ? new URL(profile.api_base_url).toString() : baseUrl;
|
|
407
|
+
return { baseUrl, apiBaseUrl, navigation, api, subresourcePolicy: policy, subresourceOrigins };
|
|
408
|
+
}
|
|
409
|
+
function sameSite(a, b) {
|
|
410
|
+
if (a.protocol !== b.protocol)
|
|
411
|
+
return false;
|
|
412
|
+
const ha = a.hostname;
|
|
413
|
+
const hb = b.hostname;
|
|
414
|
+
return ha === hb || ha.endsWith(`.${hb}`) || hb.endsWith(`.${ha}`);
|
|
415
|
+
}
|
|
416
|
+
function isAllowed(policy, rawUrl, kind) {
|
|
417
|
+
let u;
|
|
418
|
+
try {
|
|
419
|
+
u = new URL(rawUrl);
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
if (IMPLICIT_SCHEMES.has(u.protocol))
|
|
424
|
+
return kind === "subresource";
|
|
425
|
+
if (kind === "navigation")
|
|
426
|
+
return policy.navigation.has(u.origin);
|
|
427
|
+
if (kind === "api")
|
|
428
|
+
return policy.api.has(u.origin);
|
|
429
|
+
if (policy.navigation.has(u.origin) || policy.api.has(u.origin) || policy.subresourceOrigins.has(u.origin))
|
|
430
|
+
return true;
|
|
431
|
+
if (policy.subresourcePolicy === "same-site-plus-declared") {
|
|
432
|
+
for (const origin of policy.navigation)
|
|
433
|
+
if (sameSite(u, new URL(origin)))
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
function isTopLevelNavigation(request) {
|
|
439
|
+
try {
|
|
440
|
+
return request.isNavigationRequest() && request.frame().parentFrame() === null;
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
class NetworkRecorder {
|
|
447
|
+
redactor;
|
|
448
|
+
entries = [];
|
|
449
|
+
violations = [];
|
|
450
|
+
starts = new WeakMap;
|
|
451
|
+
constructor(redactor) {
|
|
452
|
+
this.redactor = redactor;
|
|
453
|
+
}
|
|
454
|
+
api(entry) {
|
|
455
|
+
this.entries.push({ source: "api", ...entry, url: this.redactor.url(entry.url) });
|
|
456
|
+
}
|
|
457
|
+
violation(url, kind) {
|
|
458
|
+
this.violations.push({ url: this.redactor.url(url), kind, at: Date.now() });
|
|
459
|
+
}
|
|
460
|
+
async install(context, policy) {
|
|
461
|
+
await context.route("**/*", async (route) => {
|
|
462
|
+
const request = route.request();
|
|
463
|
+
const topLevel = isTopLevelNavigation(request);
|
|
464
|
+
const kind = topLevel ? "navigation" : "subresource";
|
|
465
|
+
if (isAllowed(policy, request.url(), kind)) {
|
|
466
|
+
await route.continue();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.violation(request.url(), kind);
|
|
470
|
+
this.entries.push({
|
|
471
|
+
source: "page",
|
|
472
|
+
method: request.method(),
|
|
473
|
+
url: this.redactor.url(request.url()),
|
|
474
|
+
status: null,
|
|
475
|
+
duration_ms: null,
|
|
476
|
+
blocked: true,
|
|
477
|
+
resource_type: request.resourceType()
|
|
478
|
+
});
|
|
479
|
+
await route.abort("blockedbyclient");
|
|
480
|
+
});
|
|
481
|
+
context.on("request", (request) => {
|
|
482
|
+
this.starts.set(request, Date.now());
|
|
483
|
+
if (request.redirectedFrom()) {
|
|
484
|
+
const topLevel = isTopLevelNavigation(request);
|
|
485
|
+
const kind = topLevel ? "navigation" : "subresource";
|
|
486
|
+
if (!isAllowed(policy, request.url(), kind))
|
|
487
|
+
this.violation(request.url(), topLevel ? "navigation" : kind);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
const finish = async (request, failure) => {
|
|
491
|
+
const started = this.starts.get(request);
|
|
492
|
+
if (failure === "net::ERR_BLOCKED_BY_CLIENT")
|
|
493
|
+
return;
|
|
494
|
+
let status = null;
|
|
495
|
+
try {
|
|
496
|
+
status = (await request.response())?.status() ?? null;
|
|
497
|
+
} catch {
|
|
498
|
+
status = null;
|
|
499
|
+
}
|
|
500
|
+
this.entries.push({
|
|
501
|
+
source: "page",
|
|
502
|
+
method: request.method(),
|
|
503
|
+
url: this.redactor.url(request.url()),
|
|
504
|
+
status,
|
|
505
|
+
duration_ms: started ? Date.now() - started : null,
|
|
506
|
+
blocked: false,
|
|
507
|
+
resource_type: request.resourceType(),
|
|
508
|
+
...failure ? { failure: this.redactor.text(failure) } : {}
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
context.on("requestfinished", (request) => void finish(request));
|
|
512
|
+
context.on("requestfailed", (request) => void finish(request, request.failure()?.errorText ?? "failed"));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// plugins/devflow-quality/runtime/playwright/src/step.ts
|
|
517
|
+
var DEFAULT_UI_TIMEOUT_MS = 1e4;
|
|
518
|
+
var DEFAULT_NAVIGATION_TIMEOUT_MS = 30000;
|
|
519
|
+
var DEFAULT_API_TIMEOUT_MS = 30000;
|
|
520
|
+
|
|
521
|
+
class StepFailure extends Error {
|
|
522
|
+
status;
|
|
523
|
+
code;
|
|
524
|
+
details;
|
|
525
|
+
constructor(status, code, message, details = {}) {
|
|
526
|
+
super(message);
|
|
527
|
+
this.status = status;
|
|
528
|
+
this.code = code;
|
|
529
|
+
this.details = details;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function isTimeoutError(err) {
|
|
533
|
+
return err instanceof Error && (err.name === "TimeoutError" || /Timeout \d+ms exceeded/.test(err.message));
|
|
534
|
+
}
|
|
535
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
536
|
+
|
|
537
|
+
// plugins/devflow-quality/runtime/playwright/src/api.ts
|
|
538
|
+
var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
539
|
+
var MAX_REDIRECT_HOPS = 5;
|
|
540
|
+
function resolveApiUrl(raw, ctx) {
|
|
541
|
+
const url = interpolate(raw, ctx.vars);
|
|
542
|
+
if (url.startsWith("/"))
|
|
543
|
+
return new URL(url, ctx.policy.apiBaseUrl).toString();
|
|
544
|
+
try {
|
|
545
|
+
return new URL(url).toString();
|
|
546
|
+
} catch {
|
|
547
|
+
throw new StepFailure("blocked", "invalid_url", `api url must be absolute or start with '/': ${ctx.redactor.text(url)}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function assertOrigin(url, ctx) {
|
|
551
|
+
if (!isAllowed(ctx.policy, url, "api")) {
|
|
552
|
+
ctx.recorder.violation(url, "api");
|
|
553
|
+
throw new StepFailure("blocked", "network_policy_denied", `api origin not allowed by environment profile: ${ctx.redactor.url(url)}`, {
|
|
554
|
+
url: ctx.redactor.url(url)
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function send(ctx, method, url, headers, params, body, timeout) {
|
|
559
|
+
let currentMethod = method;
|
|
560
|
+
let currentUrl = url;
|
|
561
|
+
let currentBody = body;
|
|
562
|
+
let currentParams = params;
|
|
563
|
+
for (let hop = 0;; hop++) {
|
|
564
|
+
assertOrigin(currentUrl, ctx);
|
|
565
|
+
const started = Date.now();
|
|
566
|
+
let response;
|
|
567
|
+
try {
|
|
568
|
+
response = await ctx.request.fetch(currentUrl, {
|
|
569
|
+
method: currentMethod,
|
|
570
|
+
headers,
|
|
571
|
+
params: currentParams,
|
|
572
|
+
data: currentBody,
|
|
573
|
+
maxRedirects: 0,
|
|
574
|
+
failOnStatusCode: false,
|
|
575
|
+
timeout
|
|
576
|
+
});
|
|
577
|
+
} catch (err) {
|
|
578
|
+
ctx.recorder.api({ method: currentMethod, url: currentUrl, status: null, duration_ms: Date.now() - started, blocked: false, failure: ctx.redactor.error(err) });
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
ctx.recorder.api({ method: currentMethod, url: response.url(), status: response.status(), duration_ms: Date.now() - started, blocked: false });
|
|
582
|
+
const location = response.headers()["location"];
|
|
583
|
+
if (!REDIRECT_STATUSES.has(response.status()) || !location)
|
|
584
|
+
return { response, url: response.url() };
|
|
585
|
+
if (hop + 1 > MAX_REDIRECT_HOPS) {
|
|
586
|
+
throw new StepFailure("failed", "too_many_redirects", `more than ${MAX_REDIRECT_HOPS} redirects`);
|
|
587
|
+
}
|
|
588
|
+
currentUrl = new URL(location, response.url()).toString();
|
|
589
|
+
currentParams = undefined;
|
|
590
|
+
const status = response.status();
|
|
591
|
+
if (status === 303 || (status === 301 || status === 302) && currentMethod !== "GET") {
|
|
592
|
+
currentMethod = "GET";
|
|
593
|
+
currentBody = undefined;
|
|
594
|
+
}
|
|
595
|
+
await response.dispose();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function describeActual(value) {
|
|
599
|
+
return value.found ? value.value : "<absent>";
|
|
600
|
+
}
|
|
601
|
+
function evaluateAssertion(op, found, expected) {
|
|
602
|
+
switch (op) {
|
|
603
|
+
case "exists":
|
|
604
|
+
return found.found;
|
|
605
|
+
case "not_exists":
|
|
606
|
+
return !found.found;
|
|
607
|
+
case "equals":
|
|
608
|
+
return found.found && deepEqual(found.value, expected);
|
|
609
|
+
case "not_equals":
|
|
610
|
+
return found.found && !deepEqual(found.value, expected);
|
|
611
|
+
case "contains":
|
|
612
|
+
if (!found.found)
|
|
613
|
+
return false;
|
|
614
|
+
if (typeof found.value === "string" && typeof expected === "string")
|
|
615
|
+
return found.value.includes(expected);
|
|
616
|
+
if (Array.isArray(found.value))
|
|
617
|
+
return found.value.some((item) => deepEqual(item, expected));
|
|
618
|
+
return false;
|
|
619
|
+
default:
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function executeApi(step, ctx) {
|
|
624
|
+
const details = {};
|
|
625
|
+
const method = String(step.method);
|
|
626
|
+
const timeout = step.timeout_ms ?? DEFAULT_API_TIMEOUT_MS;
|
|
627
|
+
const url = resolveApiUrl(String(step.url), ctx);
|
|
628
|
+
details.url = ctx.redactor.url(url);
|
|
629
|
+
const headers = {};
|
|
630
|
+
for (const [name, value] of Object.entries(step.headers ?? {})) {
|
|
631
|
+
headers[name] = interpolate(value, ctx.vars);
|
|
632
|
+
}
|
|
633
|
+
const params = step.query ? interpolateDeep(step.query, ctx.vars) : undefined;
|
|
634
|
+
let body;
|
|
635
|
+
if ("json" in step) {
|
|
636
|
+
body = JSON.stringify(interpolateDeep(step.json, ctx.vars));
|
|
637
|
+
if (!Object.keys(headers).some((h) => h.toLowerCase() === "content-type"))
|
|
638
|
+
headers["content-type"] = "application/json";
|
|
639
|
+
}
|
|
640
|
+
const retry = step.retry;
|
|
641
|
+
const maxAttempts = retry?.max_attempts ?? 1;
|
|
642
|
+
const retryOn = new Set(retry?.on ?? []);
|
|
643
|
+
let hop;
|
|
644
|
+
for (let attempt = 1;; attempt++) {
|
|
645
|
+
let condition = null;
|
|
646
|
+
let lastError = null;
|
|
647
|
+
try {
|
|
648
|
+
hop = await send(ctx, method, url, headers, params, body, timeout);
|
|
649
|
+
if (hop.response.status() === 429)
|
|
650
|
+
condition = "rate_limited";
|
|
651
|
+
} catch (err) {
|
|
652
|
+
if (err instanceof StepFailure)
|
|
653
|
+
throw new StepFailure(err.status, err.code, err.message, { ...details, ...err.details });
|
|
654
|
+
condition = isTimeoutError(err) ? "timeout" : "connection_error";
|
|
655
|
+
lastError = err;
|
|
656
|
+
}
|
|
657
|
+
if (condition && retryOn.has(condition) && attempt < maxAttempts) {
|
|
658
|
+
await sleep(200 * attempt);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (lastError) {
|
|
662
|
+
throw new StepFailure("blocked", condition === "timeout" ? "api_timeout" : "api_connection_error", ctx.redactor.error(lastError), details);
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
const response = hop.response;
|
|
667
|
+
const status = response.status();
|
|
668
|
+
details.http_status = status;
|
|
669
|
+
details.url = ctx.redactor.url(hop.url);
|
|
670
|
+
const text = await response.text();
|
|
671
|
+
let json = undefined;
|
|
672
|
+
let parsed = false;
|
|
673
|
+
try {
|
|
674
|
+
json = JSON.parse(text);
|
|
675
|
+
parsed = true;
|
|
676
|
+
} catch {
|
|
677
|
+
parsed = false;
|
|
678
|
+
}
|
|
679
|
+
const expectStatus = step.expect_status;
|
|
680
|
+
if (expectStatus !== undefined ? status !== expectStatus : status < 200 || status > 299) {
|
|
681
|
+
details.assertion = { match: "status", expected: expectStatus ?? "2xx", actual: status };
|
|
682
|
+
throw new StepFailure("failed", "http_status_mismatch", `unexpected HTTP status ${status}`, details);
|
|
683
|
+
}
|
|
684
|
+
const assertions = step.assertions ?? [];
|
|
685
|
+
for (const assertion of assertions) {
|
|
686
|
+
const expected = "value" in assertion ? interpolateDeep(assertion.value, ctx.vars) : undefined;
|
|
687
|
+
const found = parsed ? jsonPointer(json, assertion.pointer) : { found: false };
|
|
688
|
+
details.assertion = {
|
|
689
|
+
match: `${assertion.op} ${assertion.pointer}`,
|
|
690
|
+
expected: ctx.redactor.value(expected ?? null),
|
|
691
|
+
actual: ctx.redactor.value(describeActual(found))
|
|
692
|
+
};
|
|
693
|
+
if (!parsed)
|
|
694
|
+
throw new StepFailure("failed", "response_not_json", "response body is not JSON", details);
|
|
695
|
+
if (!evaluateAssertion(assertion.op, found, expected)) {
|
|
696
|
+
throw new StepFailure("failed", "assertion_failed", `assertion ${assertion.op} ${assertion.pointer} failed`, details);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
for (const [name, spec] of Object.entries(step.extract ?? {})) {
|
|
700
|
+
const found = parsed ? jsonPointer(json, spec.pointer) : { found: false };
|
|
701
|
+
if (!found.found) {
|
|
702
|
+
throw new StepFailure("failed", "extract_missing", `extract ${name}: pointer ${spec.pointer} not found`, details);
|
|
703
|
+
}
|
|
704
|
+
if (spec.sensitive)
|
|
705
|
+
ctx.redactor.add(found.value);
|
|
706
|
+
ctx.vars.set(name, found.value);
|
|
707
|
+
}
|
|
708
|
+
return details;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// plugins/devflow-quality/runtime/playwright/src/ui.ts
|
|
712
|
+
import path from "node:path";
|
|
713
|
+
import fs from "node:fs";
|
|
714
|
+
var POLL_MS = 100;
|
|
715
|
+
function buildLocator(page, target, ctx) {
|
|
716
|
+
const text = (key) => interpolate(String(target[key]), ctx.vars);
|
|
717
|
+
const exact = target.exact === true;
|
|
718
|
+
const q = (s) => `'${ctx.redactor.text(s).replace(/'/g, "\\'")}'`;
|
|
719
|
+
const exactSuffix = exact ? ", { exact: true }" : "";
|
|
720
|
+
if ("testid" in target)
|
|
721
|
+
return { locator: page.getByTestId(text("testid")), description: `getByTestId(${q(text("testid"))})` };
|
|
722
|
+
if ("role" in target) {
|
|
723
|
+
const name = text("name");
|
|
724
|
+
return {
|
|
725
|
+
locator: page.getByRole(target.role, { name, exact }),
|
|
726
|
+
description: `getByRole('${String(target.role)}', { name: ${q(name)}${exact ? ", exact: true" : ""} })`
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if ("label" in target)
|
|
730
|
+
return { locator: page.getByLabel(text("label"), { exact }), description: `getByLabel(${q(text("label"))}${exactSuffix})` };
|
|
731
|
+
if ("placeholder" in target) {
|
|
732
|
+
return { locator: page.getByPlaceholder(text("placeholder"), { exact }), description: `getByPlaceholder(${q(text("placeholder"))}${exactSuffix})` };
|
|
733
|
+
}
|
|
734
|
+
if ("text" in target)
|
|
735
|
+
return { locator: page.getByText(text("text"), { exact }), description: `getByText(${q(text("text"))}${exactSuffix})` };
|
|
736
|
+
if ("css" in target)
|
|
737
|
+
return { locator: page.locator(text("css")), description: `locator(${q(text("css"))})` };
|
|
738
|
+
throw new StepFailure("blocked", "invalid_target", "target has no supported locator form");
|
|
739
|
+
}
|
|
740
|
+
async function resolveStrict(locator, description, deadline) {
|
|
741
|
+
let count = 0;
|
|
742
|
+
for (;; ) {
|
|
743
|
+
count = await locator.count();
|
|
744
|
+
if (count > 0 || Date.now() >= deadline)
|
|
745
|
+
break;
|
|
746
|
+
await sleep(POLL_MS);
|
|
747
|
+
}
|
|
748
|
+
if (count === 0) {
|
|
749
|
+
throw new StepFailure("failed", "element_not_found", `no element matches ${description}`, { locator: description, match_count: 0 });
|
|
750
|
+
}
|
|
751
|
+
if (count > 1) {
|
|
752
|
+
throw new StepFailure("failed", "strict_locator_violation", `${count} elements match ${description}; strict locators require exactly one`, {
|
|
753
|
+
locator: description,
|
|
754
|
+
match_count: count
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return count;
|
|
758
|
+
}
|
|
759
|
+
var remaining = (deadline) => Math.max(1, deadline - Date.now());
|
|
760
|
+
function resolveNavigationUrl(raw, ctx) {
|
|
761
|
+
const url = interpolate(raw, ctx.vars);
|
|
762
|
+
if (url.startsWith("/"))
|
|
763
|
+
return new URL(url, ctx.policy.baseUrl).toString();
|
|
764
|
+
try {
|
|
765
|
+
return new URL(url).toString();
|
|
766
|
+
} catch {
|
|
767
|
+
throw new StepFailure("blocked", "invalid_url", `goto url must be absolute or start with '/': ${ctx.redactor.text(url)}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function normalizeText(value, normalizeWhitespace, caseSensitive) {
|
|
771
|
+
let out = normalizeWhitespace ? value.replace(/\s+/g, " ").trim() : value;
|
|
772
|
+
if (!caseSensitive)
|
|
773
|
+
out = out.toLowerCase();
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
function textMatches(actual, expected, match) {
|
|
777
|
+
return match === "exact" ? actual === expected : actual.includes(expected);
|
|
778
|
+
}
|
|
779
|
+
async function executeUi(step, ctx) {
|
|
780
|
+
const page = ctx.page;
|
|
781
|
+
const defaultTimeout = step.action === "goto" ? DEFAULT_NAVIGATION_TIMEOUT_MS : DEFAULT_UI_TIMEOUT_MS;
|
|
782
|
+
const timeout = step.timeout_ms ?? defaultTimeout;
|
|
783
|
+
const deadline = Date.now() + timeout;
|
|
784
|
+
const details = {};
|
|
785
|
+
let locator = null;
|
|
786
|
+
if (step.target) {
|
|
787
|
+
const built = buildLocator(page, step.target, ctx);
|
|
788
|
+
locator = built.locator;
|
|
789
|
+
details.locator = built.description;
|
|
790
|
+
}
|
|
791
|
+
const withDetails = (err) => {
|
|
792
|
+
if (err instanceof StepFailure)
|
|
793
|
+
throw new StepFailure(err.status, err.code, err.message, { ...details, ...err.details });
|
|
794
|
+
if (isTimeoutError(err))
|
|
795
|
+
throw new StepFailure("failed", "timeout", ctx.redactor.error(err), details);
|
|
796
|
+
if (err instanceof Error && /strict mode violation/i.test(err.message)) {
|
|
797
|
+
throw new StepFailure("failed", "strict_locator_violation", ctx.redactor.error(err), details);
|
|
798
|
+
}
|
|
799
|
+
throw new StepFailure("failed", "step_error", ctx.redactor.error(err), details);
|
|
800
|
+
};
|
|
801
|
+
try {
|
|
802
|
+
switch (step.action) {
|
|
803
|
+
case "goto": {
|
|
804
|
+
const url = resolveNavigationUrl(String(step.url), ctx);
|
|
805
|
+
details.url = ctx.redactor.url(url);
|
|
806
|
+
const response = await page.goto(url, { timeout, waitUntil: "load" });
|
|
807
|
+
details.http_status = response?.status() ?? null;
|
|
808
|
+
if (response && response.status() >= 400) {
|
|
809
|
+
throw new StepFailure("failed", "navigation_http_error", `navigation returned HTTP ${response.status()}`);
|
|
810
|
+
}
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
case "click":
|
|
814
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
815
|
+
await locator.click({ timeout: remaining(deadline) });
|
|
816
|
+
break;
|
|
817
|
+
case "fill":
|
|
818
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
819
|
+
await locator.fill(interpolate(String(step.value), ctx.vars), { timeout: remaining(deadline) });
|
|
820
|
+
break;
|
|
821
|
+
case "select": {
|
|
822
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
823
|
+
const value = Array.isArray(step.value) ? step.value.map((v) => interpolate(v, ctx.vars)) : interpolate(String(step.value), ctx.vars);
|
|
824
|
+
const selected = await locator.selectOption(value, { timeout: remaining(deadline) });
|
|
825
|
+
const wanted = Array.isArray(value) ? value.length : 1;
|
|
826
|
+
if (selected.length !== wanted) {
|
|
827
|
+
throw new StepFailure("failed", "select_mismatch", `selected ${selected.length} option(s), expected ${wanted}`);
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case "check":
|
|
832
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
833
|
+
await locator.check({ timeout: remaining(deadline) });
|
|
834
|
+
break;
|
|
835
|
+
case "uncheck":
|
|
836
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
837
|
+
await locator.uncheck({ timeout: remaining(deadline) });
|
|
838
|
+
break;
|
|
839
|
+
case "wait_for": {
|
|
840
|
+
const state = step.state;
|
|
841
|
+
if (state === "visible" || state === "attached") {
|
|
842
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
843
|
+
}
|
|
844
|
+
await locator.waitFor({ state, timeout: remaining(deadline) });
|
|
845
|
+
if (details.match_count === undefined)
|
|
846
|
+
details.match_count = await locator.count();
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
case "assert_visible":
|
|
850
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
851
|
+
try {
|
|
852
|
+
await locator.waitFor({ state: "visible", timeout: remaining(deadline) });
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (isTimeoutError(err))
|
|
855
|
+
throw new StepFailure("failed", "assertion_failed", `${details.locator} is not visible`);
|
|
856
|
+
throw err;
|
|
857
|
+
}
|
|
858
|
+
break;
|
|
859
|
+
case "assert_text": {
|
|
860
|
+
details.match_count = await resolveStrict(locator, details.locator, deadline);
|
|
861
|
+
const normalizeWhitespace = step.normalize_whitespace !== false;
|
|
862
|
+
const caseSensitive = step.case_sensitive !== false;
|
|
863
|
+
const match = String(step.match);
|
|
864
|
+
const expectedRaw = interpolate(String(step.expected), ctx.vars);
|
|
865
|
+
const expected = normalizeText(expectedRaw, normalizeWhitespace, caseSensitive);
|
|
866
|
+
let actualRaw = "";
|
|
867
|
+
for (;; ) {
|
|
868
|
+
actualRaw = await locator.innerText({ timeout: Math.max(remaining(deadline), 1000) });
|
|
869
|
+
if (textMatches(normalizeText(actualRaw, normalizeWhitespace, caseSensitive), expected, match))
|
|
870
|
+
break;
|
|
871
|
+
if (Date.now() >= deadline) {
|
|
872
|
+
details.assertion = { match, expected: ctx.redactor.text(expectedRaw), actual: ctx.redactor.text(actualRaw) };
|
|
873
|
+
throw new StepFailure("failed", "assertion_failed", `text ${match} assertion failed`);
|
|
874
|
+
}
|
|
875
|
+
await sleep(POLL_MS);
|
|
876
|
+
}
|
|
877
|
+
details.assertion = { match, expected: ctx.redactor.text(expectedRaw), actual: ctx.redactor.text(actualRaw) };
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
case "assert_url": {
|
|
881
|
+
const match = String(step.match);
|
|
882
|
+
const raw = interpolate(String(step.expected), ctx.vars);
|
|
883
|
+
const expected = raw.startsWith("/") && match === "exact" ? new URL(raw, ctx.policy.baseUrl).toString() : raw;
|
|
884
|
+
let actual = page.url();
|
|
885
|
+
while (!textMatches(actual, expected, match)) {
|
|
886
|
+
if (Date.now() >= deadline) {
|
|
887
|
+
details.assertion = { match, expected: ctx.redactor.url(expected), actual: ctx.redactor.url(actual) };
|
|
888
|
+
throw new StepFailure("failed", "assertion_failed", `url ${match} assertion failed`);
|
|
889
|
+
}
|
|
890
|
+
await sleep(POLL_MS);
|
|
891
|
+
actual = page.url();
|
|
892
|
+
}
|
|
893
|
+
details.url = ctx.redactor.url(actual);
|
|
894
|
+
details.assertion = { match, expected: ctx.redactor.url(expected), actual: ctx.redactor.url(actual) };
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
case "screenshot": {
|
|
898
|
+
const file = `${String(step.name ?? step.id)}.png`;
|
|
899
|
+
if (!/^[A-Za-z0-9_-]{1,64}\.png$/.test(file))
|
|
900
|
+
throw new StepFailure("blocked", "invalid_screenshot_name", "invalid screenshot name");
|
|
901
|
+
const dir = path.join(ctx.artifactDir, "screenshots");
|
|
902
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
903
|
+
const target = path.join(dir, file);
|
|
904
|
+
await page.screenshot({ path: target, fullPage: step.full_page === true, timeout: remaining(deadline) });
|
|
905
|
+
ctx.artifactFiles.push({ path: path.relative(ctx.runDir, target), kind: "screenshot" });
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
default:
|
|
909
|
+
throw new StepFailure("blocked", "unknown_action", `unknown ui action ${step.action}`);
|
|
910
|
+
}
|
|
911
|
+
} catch (err) {
|
|
912
|
+
withDetails(err);
|
|
913
|
+
}
|
|
914
|
+
return details;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// plugins/devflow-quality/runtime/playwright/src/data.ts
|
|
918
|
+
function baseResult(step, phase) {
|
|
919
|
+
return {
|
|
920
|
+
id: step.id,
|
|
921
|
+
phase,
|
|
922
|
+
action: step.action,
|
|
923
|
+
status: "skipped",
|
|
924
|
+
duration_ms: 0,
|
|
925
|
+
locator: null,
|
|
926
|
+
match_count: null,
|
|
927
|
+
url: null,
|
|
928
|
+
http_status: null,
|
|
929
|
+
assertion: null,
|
|
930
|
+
covers: step.covers ?? {},
|
|
931
|
+
error: null,
|
|
932
|
+
error_code: null
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
async function runStep(step, phase, ctx) {
|
|
936
|
+
const result = baseResult(step, phase);
|
|
937
|
+
const started = Date.now();
|
|
938
|
+
const violationsBefore = ctx.recorder.violations.filter((v) => v.kind === "navigation").length;
|
|
939
|
+
try {
|
|
940
|
+
const details = step.driver === "api" ? await executeApi(step, ctx) : await executeUi(step, ctx);
|
|
941
|
+
Object.assign(result, details);
|
|
942
|
+
result.status = "passed";
|
|
943
|
+
} catch (err) {
|
|
944
|
+
if (err instanceof StepFailure) {
|
|
945
|
+
Object.assign(result, err.details);
|
|
946
|
+
result.status = err.status;
|
|
947
|
+
result.error_code = err.code;
|
|
948
|
+
result.error = ctx.redactor.text(err.message);
|
|
949
|
+
} else if (err instanceof UnsetVariableError) {
|
|
950
|
+
result.status = "failed";
|
|
951
|
+
result.error_code = "variable_unset";
|
|
952
|
+
result.error = err.message;
|
|
953
|
+
} else {
|
|
954
|
+
result.status = "failed";
|
|
955
|
+
result.error_code = "step_error";
|
|
956
|
+
result.error = ctx.redactor.error(err);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const violationsAfter = ctx.recorder.violations.filter((v) => v.kind === "navigation").length;
|
|
960
|
+
if (violationsAfter > violationsBefore && result.status !== "blocked") {
|
|
961
|
+
result.status = "blocked";
|
|
962
|
+
result.error_code = "network_policy_denied";
|
|
963
|
+
result.error = result.error ?? "top-level navigation to an origin not declared in navigation_origins was blocked";
|
|
964
|
+
}
|
|
965
|
+
result.duration_ms = Date.now() - started;
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
async function runPhase(steps, phase, ctx) {
|
|
969
|
+
const results = [];
|
|
970
|
+
let status = "passed";
|
|
971
|
+
let errorCode = null;
|
|
972
|
+
for (const step of steps) {
|
|
973
|
+
if (status !== "passed") {
|
|
974
|
+
results.push(baseResult(step, phase));
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
const result = await runStep(step, phase, ctx);
|
|
978
|
+
results.push(result);
|
|
979
|
+
if (result.status === "failed" || result.status === "blocked") {
|
|
980
|
+
status = result.status;
|
|
981
|
+
errorCode = result.error_code ?? null;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return { status, error_code: errorCode, results };
|
|
985
|
+
}
|
|
986
|
+
async function runCleanup(steps, ctx) {
|
|
987
|
+
if (steps.length === 0)
|
|
988
|
+
return { summary: { status: "skipped", errors: [] }, results: [] };
|
|
989
|
+
const results = [];
|
|
990
|
+
const errors = [];
|
|
991
|
+
for (const step of steps) {
|
|
992
|
+
const result = await runStep(step, "cleanup", ctx);
|
|
993
|
+
results.push(result);
|
|
994
|
+
if (result.status !== "passed")
|
|
995
|
+
errors.push(`${step.id}: ${result.error_code ?? "error"}: ${result.error ?? ""}`.trim());
|
|
996
|
+
}
|
|
997
|
+
return { summary: { status: errors.length ? "failed" : "passed", errors }, results };
|
|
998
|
+
}
|
|
999
|
+
function synthesize(setup, main, cleanup) {
|
|
1000
|
+
if (setup.status !== "passed")
|
|
1001
|
+
return { status: "blocked", error_code: "setup_failed" };
|
|
1002
|
+
const m = main;
|
|
1003
|
+
if (m.status === "failed")
|
|
1004
|
+
return { status: "failed", error_code: m.error_code };
|
|
1005
|
+
if (m.status === "blocked")
|
|
1006
|
+
return { status: "blocked", error_code: m.error_code };
|
|
1007
|
+
if (cleanup.status === "failed")
|
|
1008
|
+
return { status: "blocked", error_code: "cleanup_error" };
|
|
1009
|
+
return { status: "passed", error_code: null };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// plugins/devflow-quality/runtime/playwright/src/env.ts
|
|
1013
|
+
import path2 from "node:path";
|
|
1014
|
+
var NAMES = {
|
|
1015
|
+
dslFile: "DEVFLOW_DSL_FILE",
|
|
1016
|
+
dslSha256: "DEVFLOW_DSL_SHA256",
|
|
1017
|
+
tcId: "DEVFLOW_TC_ID",
|
|
1018
|
+
tcSha256: "DEVFLOW_TC_SHA256",
|
|
1019
|
+
executionId: "DEVFLOW_EXECUTION_ID",
|
|
1020
|
+
nonce: "DEVFLOW_EXECUTION_NONCE",
|
|
1021
|
+
resultFile: "DEVFLOW_RESULT_FILE",
|
|
1022
|
+
artifactDir: "DEVFLOW_ARTIFACT_DIR",
|
|
1023
|
+
environmentFile: "DEVFLOW_ENVIRONMENT_FILE"
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
class EnvError extends Error {
|
|
1027
|
+
code;
|
|
1028
|
+
constructor(message, code) {
|
|
1029
|
+
super(message);
|
|
1030
|
+
this.code = code;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function rawEnv(env = process.env) {
|
|
1034
|
+
const out = {};
|
|
1035
|
+
for (const [key, name] of Object.entries(NAMES)) {
|
|
1036
|
+
const value = env[name];
|
|
1037
|
+
if (typeof value === "string" && value.length > 0)
|
|
1038
|
+
out[key] = value;
|
|
1039
|
+
}
|
|
1040
|
+
return out;
|
|
1041
|
+
}
|
|
1042
|
+
function readRuntimeEnv(env = process.env) {
|
|
1043
|
+
const raw = rawEnv(env);
|
|
1044
|
+
const missing = Object.entries(NAMES).filter(([key]) => !raw[key]).map(([, name]) => name);
|
|
1045
|
+
if (missing.length)
|
|
1046
|
+
throw new EnvError(`missing reserved environment variables: ${missing.join(", ")}`, "runtime_env_missing");
|
|
1047
|
+
const r = raw;
|
|
1048
|
+
for (const key of ["dslFile", "resultFile", "artifactDir", "environmentFile"]) {
|
|
1049
|
+
if (!path2.isAbsolute(r[key]))
|
|
1050
|
+
throw new EnvError(`${NAMES[key]} must be an absolute path`, "runtime_env_invalid");
|
|
1051
|
+
}
|
|
1052
|
+
if (!/^[0-9a-f]{64}$/.test(r.dslSha256))
|
|
1053
|
+
throw new EnvError("DEVFLOW_DSL_SHA256 must be 64 lowercase hex", "runtime_env_invalid");
|
|
1054
|
+
if (!/^[0-9a-f]{64}$/.test(r.tcSha256))
|
|
1055
|
+
throw new EnvError("DEVFLOW_TC_SHA256 must be 64 lowercase hex", "runtime_env_invalid");
|
|
1056
|
+
if (path2.basename(r.resultFile) !== "result.json")
|
|
1057
|
+
throw new EnvError("DEVFLOW_RESULT_FILE must be <run_dir>/result.json", "runtime_env_invalid");
|
|
1058
|
+
const runDir = path2.dirname(path2.resolve(r.resultFile));
|
|
1059
|
+
if (path2.resolve(r.artifactDir) !== path2.join(runDir, "artifacts")) {
|
|
1060
|
+
throw new EnvError("DEVFLOW_ARTIFACT_DIR must be <run_dir>/artifacts next to DEVFLOW_RESULT_FILE", "runtime_env_invalid");
|
|
1061
|
+
}
|
|
1062
|
+
return { ...r, runDir };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// plugins/devflow-quality/runtime/playwright/src/redact.ts
|
|
1066
|
+
var REDACTED = "***";
|
|
1067
|
+
var SENSITIVE_KEY = /(pass(word|wd)?|secret|token|auth|session|cookie|api[-_]?key|signature|sig|credential|private)/i;
|
|
1068
|
+
var MAX_ERROR_LENGTH = 2000;
|
|
1069
|
+
var ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
|
|
1070
|
+
|
|
1071
|
+
class Redactor {
|
|
1072
|
+
values = [];
|
|
1073
|
+
add(value) {
|
|
1074
|
+
if (value === undefined || value === null)
|
|
1075
|
+
return;
|
|
1076
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
1077
|
+
if (!text)
|
|
1078
|
+
return;
|
|
1079
|
+
if (!this.values.includes(text)) {
|
|
1080
|
+
this.values.push(text);
|
|
1081
|
+
this.values.sort((a, b) => b.length - a.length);
|
|
1082
|
+
}
|
|
1083
|
+
for (const encoded of [encodeURIComponent(text), new URLSearchParams({ v: text }).toString().slice(2)]) {
|
|
1084
|
+
if (encoded !== text && !this.values.includes(encoded))
|
|
1085
|
+
this.values.push(encoded);
|
|
1086
|
+
}
|
|
1087
|
+
this.values.sort((a, b) => b.length - a.length);
|
|
1088
|
+
}
|
|
1089
|
+
list() {
|
|
1090
|
+
return [...this.values];
|
|
1091
|
+
}
|
|
1092
|
+
text(input) {
|
|
1093
|
+
let out = input;
|
|
1094
|
+
for (const v of this.values)
|
|
1095
|
+
out = out.split(v).join(REDACTED);
|
|
1096
|
+
return out;
|
|
1097
|
+
}
|
|
1098
|
+
url(input) {
|
|
1099
|
+
let masked = input;
|
|
1100
|
+
try {
|
|
1101
|
+
const u = new URL(input);
|
|
1102
|
+
if (u.username || u.password) {
|
|
1103
|
+
u.username = u.username ? REDACTED : "";
|
|
1104
|
+
u.password = u.password ? REDACTED : "";
|
|
1105
|
+
}
|
|
1106
|
+
for (const key of [...u.searchParams.keys()]) {
|
|
1107
|
+
if (SENSITIVE_KEY.test(key))
|
|
1108
|
+
u.searchParams.set(key, REDACTED);
|
|
1109
|
+
}
|
|
1110
|
+
masked = u.toString();
|
|
1111
|
+
} catch {}
|
|
1112
|
+
return this.text(masked);
|
|
1113
|
+
}
|
|
1114
|
+
value(input) {
|
|
1115
|
+
if (typeof input === "string")
|
|
1116
|
+
return this.text(input);
|
|
1117
|
+
if (Array.isArray(input))
|
|
1118
|
+
return input.map((v) => this.value(v));
|
|
1119
|
+
if (input && typeof input === "object") {
|
|
1120
|
+
return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, SENSITIVE_KEY.test(k) ? REDACTED : this.value(v)]));
|
|
1121
|
+
}
|
|
1122
|
+
if (input !== undefined && input !== null && this.values.includes(String(input)))
|
|
1123
|
+
return REDACTED;
|
|
1124
|
+
return input;
|
|
1125
|
+
}
|
|
1126
|
+
error(err) {
|
|
1127
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
1128
|
+
const text = this.text(raw.replace(ANSI, ""));
|
|
1129
|
+
return text.length > MAX_ERROR_LENGTH ? `${text.slice(0, MAX_ERROR_LENGTH)}…` : text;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// plugins/devflow-quality/runtime/playwright/src/version.ts
|
|
1134
|
+
var RUNTIME_VERSION = "0.1.0";
|
|
1135
|
+
var RESULT_ORIGIN = "devflow_playwright_runtime";
|
|
1136
|
+
var RESULT_SCHEMA_VERSION = 1;
|
|
1137
|
+
var DSL_SCHEMA_FILE = "devflow-test-dsl-v1.schema.json";
|
|
1138
|
+
var RESULT_ATTACHMENT = "devflow-runtime-result";
|
|
1139
|
+
|
|
1140
|
+
// plugins/devflow-quality/runtime/playwright/src/execute.ts
|
|
1141
|
+
var now = () => new Date().toISOString();
|
|
1142
|
+
function outcome(partial, redactor) {
|
|
1143
|
+
return {
|
|
1144
|
+
error_code: null,
|
|
1145
|
+
dsl_sha256: null,
|
|
1146
|
+
finished_at: now(),
|
|
1147
|
+
steps: [],
|
|
1148
|
+
cleanup: { status: "skipped", errors: [] },
|
|
1149
|
+
artifact_files: [],
|
|
1150
|
+
...partial,
|
|
1151
|
+
redact_values: redactor.list()
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function secretValues(profile, redactor) {
|
|
1155
|
+
const map = new Map;
|
|
1156
|
+
for (const [key, envName] of Object.entries(profile?.secret_env ?? {})) {
|
|
1157
|
+
const value = typeof envName === "string" ? process.env[envName] : undefined;
|
|
1158
|
+
if (value)
|
|
1159
|
+
redactor.add(value);
|
|
1160
|
+
map.set(key, value);
|
|
1161
|
+
}
|
|
1162
|
+
return map;
|
|
1163
|
+
}
|
|
1164
|
+
async function executeRun(page, runtimeDir) {
|
|
1165
|
+
const startedAt = now();
|
|
1166
|
+
const redactor = new Redactor;
|
|
1167
|
+
let env;
|
|
1168
|
+
try {
|
|
1169
|
+
env = readRuntimeEnv();
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
const code = err instanceof EnvError ? err.code : "runtime_env_invalid";
|
|
1172
|
+
return outcome({ status: "invalid", error_code: code, started_at: startedAt }, redactor);
|
|
1173
|
+
}
|
|
1174
|
+
let profile = null;
|
|
1175
|
+
let profileError = null;
|
|
1176
|
+
try {
|
|
1177
|
+
profile = JSON.parse(fs2.readFileSync(env.environmentFile, "utf8"));
|
|
1178
|
+
} catch {
|
|
1179
|
+
profileError = "environment_profile_unreadable";
|
|
1180
|
+
}
|
|
1181
|
+
const secrets = secretValues(profile, redactor);
|
|
1182
|
+
let bytes;
|
|
1183
|
+
try {
|
|
1184
|
+
bytes = fs2.readFileSync(env.dslFile);
|
|
1185
|
+
} catch {
|
|
1186
|
+
return outcome({ status: "invalid", error_code: "dsl_unreadable", started_at: startedAt }, redactor);
|
|
1187
|
+
}
|
|
1188
|
+
const dslSha = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
1189
|
+
if (dslSha !== env.dslSha256) {
|
|
1190
|
+
return outcome({ status: "invalid", error_code: "dsl_sha256_mismatch", dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1191
|
+
}
|
|
1192
|
+
let dsl;
|
|
1193
|
+
try {
|
|
1194
|
+
dsl = JSON.parse(bytes.toString("utf8"));
|
|
1195
|
+
} catch {
|
|
1196
|
+
return outcome({ status: "invalid", error_code: "dsl_not_json", dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1197
|
+
}
|
|
1198
|
+
const allSteps = [...dsl?.data?.setup ?? [], ...dsl?.steps ?? [], ...dsl?.data?.cleanup ?? []];
|
|
1199
|
+
const unknown = Array.isArray(allSteps) && allSteps.some((s) => {
|
|
1200
|
+
const set = s?.driver === "api" ? API_ACTIONS : s?.driver === "ui" ? UI_ACTIONS : null;
|
|
1201
|
+
return !set || !set.has(s?.action);
|
|
1202
|
+
});
|
|
1203
|
+
if (unknown)
|
|
1204
|
+
return outcome({ status: "invalid", error_code: "dsl_unknown_action", dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1205
|
+
let schema;
|
|
1206
|
+
try {
|
|
1207
|
+
schema = JSON.parse(fs2.readFileSync(path3.join(runtimeDir, DSL_SCHEMA_FILE), "utf8"));
|
|
1208
|
+
} catch {
|
|
1209
|
+
return outcome({ status: "blocked", error_code: "runtime_schema_unavailable", dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1210
|
+
}
|
|
1211
|
+
const issues = validateDsl(dsl, schema);
|
|
1212
|
+
if (issues.length) {
|
|
1213
|
+
console.error(`devflow runtime: DSL invalid: ${issues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join("; ")}`);
|
|
1214
|
+
return outcome({ status: "invalid", error_code: "dsl_invalid", dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1215
|
+
}
|
|
1216
|
+
if (dsl.tc_id !== env.tcId || dsl.tc_sha256 !== env.tcSha256) {
|
|
1217
|
+
return outcome({ status: "invalid", error_code: "dsl_identity_mismatch", dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1218
|
+
}
|
|
1219
|
+
if (profileError)
|
|
1220
|
+
return outcome({ status: "blocked", error_code: profileError, dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1221
|
+
let policy;
|
|
1222
|
+
try {
|
|
1223
|
+
policy = buildPolicy(profile);
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
const code = err instanceof ProfileError ? err.code : "environment_profile_invalid";
|
|
1226
|
+
console.error(`devflow runtime: ${redactor.error(err)}`);
|
|
1227
|
+
return outcome({ status: "blocked", error_code: code, dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1228
|
+
}
|
|
1229
|
+
const vars = new Map;
|
|
1230
|
+
for (const [name, variable] of Object.entries(dsl.variables)) {
|
|
1231
|
+
if ("secret" in variable && variable.secret !== undefined) {
|
|
1232
|
+
const value = secrets.get(variable.secret);
|
|
1233
|
+
if (!value) {
|
|
1234
|
+
console.error(`devflow runtime: secret ${variable.secret} is not declared in secret_env or its environment variable is unset`);
|
|
1235
|
+
return outcome({ status: "blocked", error_code: "secret_unavailable", dsl_sha256: dslSha, started_at: startedAt }, redactor);
|
|
1236
|
+
}
|
|
1237
|
+
vars.set(name, value);
|
|
1238
|
+
} else {
|
|
1239
|
+
if (variable.sensitive)
|
|
1240
|
+
redactor.add(variable.value);
|
|
1241
|
+
vars.set(name, variable.value);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
vars.set("execution_id", env.executionId);
|
|
1245
|
+
vars.set("namespace", interpolate(dsl.data.namespace, new Map([["execution_id", env.executionId]])));
|
|
1246
|
+
fs2.mkdirSync(env.artifactDir, { recursive: true, mode: 448 });
|
|
1247
|
+
const context = page.context();
|
|
1248
|
+
const recorder = new NetworkRecorder(redactor);
|
|
1249
|
+
await recorder.install(context, policy);
|
|
1250
|
+
const ctx = {
|
|
1251
|
+
page,
|
|
1252
|
+
context,
|
|
1253
|
+
request: context.request,
|
|
1254
|
+
vars,
|
|
1255
|
+
redactor,
|
|
1256
|
+
policy,
|
|
1257
|
+
recorder,
|
|
1258
|
+
artifactDir: env.artifactDir,
|
|
1259
|
+
runDir: env.runDir,
|
|
1260
|
+
artifactFiles: []
|
|
1261
|
+
};
|
|
1262
|
+
const setup = await runPhase(dsl.data.setup, "setup", ctx);
|
|
1263
|
+
let main = null;
|
|
1264
|
+
if (setup.status === "passed") {
|
|
1265
|
+
main = await runPhase(dsl.steps, "main", ctx);
|
|
1266
|
+
}
|
|
1267
|
+
const cleanup = await runCleanup(dsl.data.cleanup, ctx);
|
|
1268
|
+
const verdict = synthesize(setup, main, cleanup.summary);
|
|
1269
|
+
const mainResults = main?.results ?? dsl.steps.map((s) => ({
|
|
1270
|
+
id: s.id,
|
|
1271
|
+
phase: "main",
|
|
1272
|
+
action: s.action,
|
|
1273
|
+
status: "skipped",
|
|
1274
|
+
duration_ms: 0,
|
|
1275
|
+
locator: null,
|
|
1276
|
+
match_count: null,
|
|
1277
|
+
url: null,
|
|
1278
|
+
http_status: null,
|
|
1279
|
+
assertion: null,
|
|
1280
|
+
covers: s.covers ?? {},
|
|
1281
|
+
error: null
|
|
1282
|
+
}));
|
|
1283
|
+
const networkDir = path3.join(env.artifactDir, "network");
|
|
1284
|
+
fs2.mkdirSync(networkDir, { recursive: true, mode: 448 });
|
|
1285
|
+
const networkFile = path3.join(networkDir, "summary.json");
|
|
1286
|
+
const summary = { entries: recorder.entries, violations: recorder.violations };
|
|
1287
|
+
fs2.writeFileSync(networkFile, redactor.text(JSON.stringify(summary, null, 2)), { mode: 384 });
|
|
1288
|
+
ctx.artifactFiles.push({ path: path3.relative(env.runDir, networkFile), kind: "network" });
|
|
1289
|
+
return outcome({
|
|
1290
|
+
status: verdict.status,
|
|
1291
|
+
error_code: verdict.error_code,
|
|
1292
|
+
dsl_sha256: dslSha,
|
|
1293
|
+
started_at: startedAt,
|
|
1294
|
+
steps: [...setup.results, ...mainResults, ...cleanup.results],
|
|
1295
|
+
cleanup: cleanup.summary,
|
|
1296
|
+
artifact_files: ctx.artifactFiles
|
|
1297
|
+
}, redactor);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// plugins/devflow-quality/runtime/playwright/src/runner.spec.ts
|
|
1301
|
+
var runtimeDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1302
|
+
test("devflow-runtime", async ({ page }, testInfo) => {
|
|
1303
|
+
const outcome2 = await executeRun(page, runtimeDir);
|
|
1304
|
+
await testInfo.attach(RESULT_ATTACHMENT, { body: JSON.stringify(outcome2), contentType: "application/json" });
|
|
1305
|
+
if (outcome2.status !== "passed") {
|
|
1306
|
+
throw new Error(`devflow runtime status=${outcome2.status} error_code=${outcome2.error_code ?? "none"}`);
|
|
1307
|
+
}
|
|
1308
|
+
});
|