swipium 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +89 -25
- package/THREAT_MODEL.md +1 -1
- package/dist/appMap/automationLink.js +13 -3
- package/dist/appMap/automationLink.js.map +1 -1
- package/dist/appMap/build.js +28 -6
- package/dist/appMap/build.js.map +1 -1
- package/dist/appMap/codeIndex.js +29 -3
- package/dist/appMap/codeIndex.js.map +1 -1
- package/dist/appMap/featureIndex.js +20 -2
- package/dist/appMap/featureIndex.js.map +1 -1
- package/dist/appMap/featureModel.js +42 -6
- package/dist/appMap/featureModel.js.map +1 -1
- package/dist/appMap/firstRunApply.js +22 -4
- package/dist/appMap/firstRunApply.js.map +1 -1
- package/dist/appMap/fsWalk.js +19 -2
- package/dist/appMap/fsWalk.js.map +1 -1
- package/dist/appMap/issues.js +0 -20
- package/dist/appMap/issues.js.map +1 -1
- package/dist/appMap/migrations.js +1 -1
- package/dist/appMap/migrations.js.map +1 -1
- package/dist/appMap/prelaunch.js.map +1 -1
- package/dist/appMap/projectRegistry.js +8 -2
- package/dist/appMap/projectRegistry.js.map +1 -1
- package/dist/appMap/query.js +20 -4
- package/dist/appMap/query.js.map +1 -1
- package/dist/appMap/runtimeMerge.js +9 -2
- package/dist/appMap/runtimeMerge.js.map +1 -1
- package/dist/appMap/schema.js.map +1 -1
- package/dist/appMap/screenMatch.js +5 -1
- package/dist/appMap/screenMatch.js.map +1 -1
- package/dist/appMap/staticScan.js +134 -22
- package/dist/appMap/staticScan.js.map +1 -1
- package/dist/appMap/store.js +4 -79
- package/dist/appMap/store.js.map +1 -1
- package/dist/appMap/tsAstScan.js +31 -9
- package/dist/appMap/tsAstScan.js.map +1 -1
- package/dist/artifacts/bundletool.js +44 -7
- package/dist/artifacts/bundletool.js.map +1 -1
- package/dist/artifacts/resolve.js +33 -6
- package/dist/artifacts/resolve.js.map +1 -1
- package/dist/automation/capabilities.js +109 -27
- package/dist/automation/capabilities.js.map +1 -1
- package/dist/automation/gestures.js +5 -1
- package/dist/automation/gestures.js.map +1 -1
- package/dist/automation/plan.js +65 -17
- package/dist/automation/plan.js.map +1 -1
- package/dist/automation/report.js +12 -2
- package/dist/automation/report.js.map +1 -1
- package/dist/automation/selectors.js +8 -3
- package/dist/automation/selectors.js.map +1 -1
- package/dist/automation/types.js.map +1 -1
- package/dist/automation/waits.js +9 -3
- package/dist/automation/waits.js.map +1 -1
- package/dist/automation/webview.js +7 -1
- package/dist/automation/webview.js.map +1 -1
- package/dist/automationGen/appiumModel.js +3 -1
- package/dist/automationGen/appiumModel.js.map +1 -1
- package/dist/automationGen/ciEmitter.js +1 -1
- package/dist/automationGen/ciEmitter.js.map +1 -1
- package/dist/automationGen/jsEmitter.js +29 -21
- package/dist/automationGen/jsEmitter.js.map +1 -1
- package/dist/automationGen/projectProfile.js +5 -1
- package/dist/automationGen/projectProfile.js.map +1 -1
- package/dist/automationGen/pythonEmitter.js +3 -3
- package/dist/automationGen/pythonEmitter.js.map +1 -1
- package/dist/automationGen/readmeEmitter.js.map +1 -1
- package/dist/automationGen/suitePlan.js +11 -6
- package/dist/automationGen/suitePlan.js.map +1 -1
- package/dist/automationGen/validation.js +33 -5
- package/dist/automationGen/validation.js.map +1 -1
- package/dist/build/parseBuildLog.js +234 -30
- package/dist/build/parseBuildLog.js.map +1 -1
- package/dist/build/plan.js +33 -12
- package/dist/build/plan.js.map +1 -1
- package/dist/ci/preflight.js.map +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/scan.js +1 -1
- package/dist/cli/scan.js.map +1 -1
- package/dist/cli/suite.js +2 -2
- package/dist/cli/suite.js.map +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/context/detect.js +10 -4
- package/dist/context/detect.js.map +1 -1
- package/dist/context/findApps.js +16 -2
- package/dist/context/findApps.js.map +1 -1
- package/dist/context/scan.js.map +1 -1
- package/dist/core/target.js +7 -5
- package/dist/core/target.js.map +1 -1
- package/dist/core/targetPlan.js +135 -17
- package/dist/core/targetPlan.js.map +1 -1
- package/dist/drivers/DirectDriver.js +17 -7
- package/dist/drivers/DirectDriver.js.map +1 -1
- package/dist/drivers/SimctlDriver.js +4 -1
- package/dist/drivers/SimctlDriver.js.map +1 -1
- package/dist/drivers/WdaDriver.js +15 -5
- package/dist/drivers/WdaDriver.js.map +1 -1
- package/dist/explore/candidates.js +5 -1
- package/dist/explore/candidates.js.map +1 -1
- package/dist/explore/graph.js +11 -2
- package/dist/explore/graph.js.map +1 -1
- package/dist/explore/memory.js +4 -1
- package/dist/explore/memory.js.map +1 -1
- package/dist/explore/planner.js +54 -9
- package/dist/explore/planner.js.map +1 -1
- package/dist/explore/policy.js +6 -1
- package/dist/explore/policy.js.map +1 -1
- package/dist/explore/runner.js +179 -23
- package/dist/explore/runner.js.map +1 -1
- package/dist/explore/signatures.js +3 -1
- package/dist/explore/signatures.js.map +1 -1
- package/dist/explore/suite.js +3 -1
- package/dist/explore/suite.js.map +1 -1
- package/dist/featureTesting/executionBootstrap.js +128 -19
- package/dist/featureTesting/executionBootstrap.js.map +1 -1
- package/dist/featureTesting/featureMap.js +4 -1
- package/dist/featureTesting/featureMap.js.map +1 -1
- package/dist/featureTesting/featureScope.js +94 -13
- package/dist/featureTesting/featureScope.js.map +1 -1
- package/dist/featureTesting/mapFeatureScope.js +37 -8
- package/dist/featureTesting/mapFeatureScope.js.map +1 -1
- package/dist/featureTesting/objectiveModel.js +5 -1
- package/dist/featureTesting/objectiveModel.js.map +1 -1
- package/dist/featureTesting/resultMerge.js +10 -2
- package/dist/featureTesting/resultMerge.js.map +1 -1
- package/dist/featureTesting/sources.js +4 -1
- package/dist/featureTesting/sources.js.map +1 -1
- package/dist/featureTesting/suiteBridge.js +7 -1
- package/dist/featureTesting/suiteBridge.js.map +1 -1
- package/dist/featureTesting/synonyms.js +31 -3
- package/dist/featureTesting/synonyms.js.map +1 -1
- package/dist/featureTesting/testCaseFactory.js +16 -4
- package/dist/featureTesting/testCaseFactory.js.map +1 -1
- package/dist/featureTesting/testPlan.js +32 -10
- package/dist/featureTesting/testPlan.js.map +1 -1
- package/dist/firstRun/classifyScreen.js +4 -1
- package/dist/firstRun/classifyScreen.js.map +1 -1
- package/dist/firstRun/firstRunPlanner.js +5 -2
- package/dist/firstRun/firstRunPlanner.js.map +1 -1
- package/dist/firstRun/firstRunRunner.js +84 -23
- package/dist/firstRun/firstRunRunner.js.map +1 -1
- package/dist/firstRun/generatedDataPolicy.js +16 -6
- package/dist/firstRun/generatedDataPolicy.js.map +1 -1
- package/dist/firstRun/inputPlanner.js +40 -6
- package/dist/firstRun/inputPlanner.js.map +1 -1
- package/dist/firstRun/paywallPolicy.js.map +1 -1
- package/dist/fixtures/catalog.js +39 -6
- package/dist/fixtures/catalog.js.map +1 -1
- package/dist/flows/generate.js +53 -12
- package/dist/flows/generate.js.map +1 -1
- package/dist/flows/lint.js +88 -14
- package/dist/flows/lint.js.map +1 -1
- package/dist/flows/pack.js +18 -2
- package/dist/flows/pack.js.map +1 -1
- package/dist/flows/repair.js +71 -20
- package/dist/flows/repair.js.map +1 -1
- package/dist/flows/run.js +164 -51
- package/dist/flows/run.js.map +1 -1
- package/dist/flows/schema.js +57 -20
- package/dist/flows/schema.js.map +1 -1
- package/dist/flows/seedExec.js +5 -1
- package/dist/flows/seedExec.js.map +1 -1
- package/dist/ios/signing.js +32 -3
- package/dist/ios/signing.js.map +1 -1
- package/dist/issues/classify.js +12 -4
- package/dist/issues/classify.js.map +1 -1
- package/dist/issues/fingerprint.js +11 -5
- package/dist/issues/fingerprint.js.map +1 -1
- package/dist/issues/index.js +4 -20
- package/dist/issues/index.js.map +1 -1
- package/dist/issues/metrics.js +2 -1
- package/dist/issues/metrics.js.map +1 -1
- package/dist/issues/recurrence.js.map +1 -1
- package/dist/issues/report.js +8 -4
- package/dist/issues/report.js.map +1 -1
- package/dist/issues/reportBridge.js +36 -5
- package/dist/issues/reportBridge.js.map +1 -1
- package/dist/issues/schema.js.map +1 -1
- package/dist/issues/store.js.map +1 -1
- package/dist/lib/android.js +9 -1
- package/dist/lib/android.js.map +1 -1
- package/dist/lib/device.js +21 -5
- package/dist/lib/device.js.map +1 -1
- package/dist/lib/image.js +11 -2
- package/dist/lib/image.js.map +1 -1
- package/dist/lib/lockfile.js +60 -0
- package/dist/lib/lockfile.js.map +1 -0
- package/dist/lib/needsInput.js +6 -3
- package/dist/lib/needsInput.js.map +1 -1
- package/dist/lib/png.js +2 -1
- package/dist/lib/png.js.map +1 -1
- package/dist/lib/result.js +16 -1
- package/dist/lib/result.js.map +1 -1
- package/dist/lib/schemaHash.js +4 -2
- package/dist/lib/schemaHash.js.map +1 -1
- package/dist/lib/sensitive.js +3 -1
- package/dist/lib/sensitive.js.map +1 -1
- package/dist/lib/simctl.js.map +1 -1
- package/dist/lib/spawn.js +29 -10
- package/dist/lib/spawn.js.map +1 -1
- package/dist/lib/wda.js +27 -10
- package/dist/lib/wda.js.map +1 -1
- package/dist/lib/wdaConfig.js +12 -5
- package/dist/lib/wdaConfig.js.map +1 -1
- package/dist/lib/wdaTune.js +52 -23
- package/dist/lib/wdaTune.js.map +1 -1
- package/dist/mobileAudit/checks.js +141 -19
- package/dist/mobileAudit/checks.js.map +1 -1
- package/dist/mobileAudit/profiles.js +120 -19
- package/dist/mobileAudit/profiles.js.map +1 -1
- package/dist/mobileAudit/results.js +7 -2
- package/dist/mobileAudit/results.js.map +1 -1
- package/dist/mobileAudit/runner.js +144 -19
- package/dist/mobileAudit/runner.js.map +1 -1
- package/dist/oracle/auth.js.map +1 -1
- package/dist/oracle/failures.js +888 -131
- package/dist/oracle/failures.js.map +1 -1
- package/dist/oracle/health.js +90 -14
- package/dist/oracle/health.js.map +1 -1
- package/dist/oracle/locator.js +147 -18
- package/dist/oracle/locator.js.map +1 -1
- package/dist/oracle/record.js.map +1 -1
- package/dist/orchestration/goal.js +3 -9
- package/dist/orchestration/goal.js.map +1 -1
- package/dist/orchestration/testThis/execute.js +126 -0
- package/dist/orchestration/testThis/execute.js.map +1 -0
- package/dist/orchestration/testThis/pipeline.js +324 -0
- package/dist/orchestration/testThis/pipeline.js.map +1 -0
- package/dist/orchestration/testThis/plan.js +430 -0
- package/dist/orchestration/testThis/plan.js.map +1 -0
- package/dist/orchestration/testThis/terminal.js +118 -0
- package/dist/orchestration/testThis/terminal.js.map +1 -0
- package/dist/orchestration/testThis/types.js +12 -0
- package/dist/orchestration/testThis/types.js.map +1 -0
- package/dist/plan/plan.js.map +1 -1
- package/dist/prompts/index.js +3 -3
- package/dist/prompts/index.js.map +1 -1
- package/dist/report/coverage.js +0 -1
- package/dist/report/coverage.js.map +1 -1
- package/dist/report/evidence.js.map +1 -1
- package/dist/report/export.js +59 -37
- package/dist/report/export.js.map +1 -1
- package/dist/report/flake.js +13 -14
- package/dist/report/flake.js.map +1 -1
- package/dist/report/history.js +72 -38
- package/dist/report/history.js.map +1 -1
- package/dist/report/policy.js +11 -3
- package/dist/report/policy.js.map +1 -1
- package/dist/report/qaLevel.js +2 -8
- package/dist/report/qaLevel.js.map +1 -1
- package/dist/report/readiness.js +42 -12
- package/dist/report/readiness.js.map +1 -1
- package/dist/report/summary.js.map +1 -1
- package/dist/report/timing.js +23 -23
- package/dist/report/timing.js.map +1 -1
- package/dist/server.js +42 -37
- package/dist/server.js.map +1 -1
- package/dist/services/automationGenerate.js.map +1 -1
- package/dist/services/build.js +8 -2
- package/dist/services/build.js.map +1 -1
- package/dist/services/preflight.js +38 -6
- package/dist/services/preflight.js.map +1 -1
- package/dist/services/prepareAndroid.js +46 -10
- package/dist/services/prepareAndroid.js.map +1 -1
- package/dist/services/prepareIos.js +72 -10
- package/dist/services/prepareIos.js.map +1 -1
- package/dist/services/report.js +173 -37
- package/dist/services/report.js.map +1 -1
- package/dist/services/smoke.js +36 -6
- package/dist/services/smoke.js.map +1 -1
- package/dist/services/suiteGenerate.js +16 -4
- package/dist/services/suiteGenerate.js.map +1 -1
- package/dist/services/testSuiteKnowledge.js +38 -6
- package/dist/services/testSuiteKnowledge.js.map +1 -1
- package/dist/session/attach.js +16 -0
- package/dist/session/attach.js.map +1 -1
- package/dist/session/processRegistry.js +164 -0
- package/dist/session/processRegistry.js.map +1 -0
- package/dist/session/progress.js +1 -1
- package/dist/session/progress.js.map +1 -1
- package/dist/session/store.js +216 -43
- package/dist/session/store.js.map +1 -1
- package/dist/snapshot/overlays.js +7 -2
- package/dist/snapshot/overlays.js.map +1 -1
- package/dist/snapshot/parse.js +2 -8
- package/dist/snapshot/parse.js.map +1 -1
- package/dist/snapshot/present.js +27 -3
- package/dist/snapshot/present.js.map +1 -1
- package/dist/state/profile.js +36 -9
- package/dist/state/profile.js.map +1 -1
- package/dist/suite/compile.js +7 -2
- package/dist/suite/compile.js.map +1 -1
- package/dist/suite/lint.js +21 -3
- package/dist/suite/lint.js.map +1 -1
- package/dist/suite/pom.js +26 -4
- package/dist/suite/pom.js.map +1 -1
- package/dist/suite/testcase.js +6 -4
- package/dist/suite/testcase.js.map +1 -1
- package/dist/testSuite/exporter.js +6 -2
- package/dist/testSuite/exporter.js.map +1 -1
- package/dist/testSuite/generator.js +31 -6
- package/dist/testSuite/generator.js.map +1 -1
- package/dist/testSuite/issueLinks.js.map +1 -1
- package/dist/testSuite/lint.js +1 -1
- package/dist/testSuite/lint.js.map +1 -1
- package/dist/testSuite/merge.js +8 -2
- package/dist/testSuite/merge.js.map +1 -1
- package/dist/testSuite/schema.js +16 -11
- package/dist/testSuite/schema.js.map +1 -1
- package/dist/testSuite/store.js +9 -1
- package/dist/testSuite/store.js.map +1 -1
- package/dist/tools/act.js +107 -43
- package/dist/tools/act.js.map +1 -1
- package/dist/tools/agent.js +123 -25
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/appControl.js +16 -3
- package/dist/tools/appControl.js.map +1 -1
- package/dist/tools/appMap.js +331 -63
- package/dist/tools/appMap.js.map +1 -1
- package/dist/tools/assertVisual.js +12 -2
- package/dist/tools/assertVisual.js.map +1 -1
- package/dist/tools/automationGenerate.js +276 -241
- package/dist/tools/automationGenerate.js.map +1 -1
- package/dist/tools/build.js +92 -41
- package/dist/tools/build.js.map +1 -1
- package/dist/tools/bundletool.js +139 -28
- package/dist/tools/bundletool.js.map +1 -1
- package/dist/tools/capabilities.js +45 -71
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/clearOverlay.js +45 -8
- package/dist/tools/clearOverlay.js.map +1 -1
- package/dist/tools/detectContext.js +1 -1
- package/dist/tools/detectContext.js.map +1 -1
- package/dist/tools/device.js +44 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/doctor.js +38 -15
- package/dist/tools/doctor.js.map +1 -1
- package/dist/tools/explore.js +96 -22
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/featureTesting.js +153 -114
- package/dist/tools/featureTesting.js.map +1 -1
- package/dist/tools/firstRun.js +95 -60
- package/dist/tools/firstRun.js.map +1 -1
- package/dist/tools/flow.js +236 -101
- package/dist/tools/flow.js.map +1 -1
- package/dist/tools/flowGenerate.js +51 -47
- package/dist/tools/flowGenerate.js.map +1 -1
- package/dist/tools/flowRepair.js +24 -4
- package/dist/tools/flowRepair.js.map +1 -1
- package/dist/tools/generate.js +236 -0
- package/dist/tools/generate.js.map +1 -0
- package/dist/tools/getArtifact.js +21 -3
- package/dist/tools/getArtifact.js.map +1 -1
- package/dist/tools/health.js +3 -1
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/history.js +40 -10
- package/dist/tools/history.js.map +1 -1
- package/dist/tools/ios.js +157 -25
- package/dist/tools/ios.js.map +1 -1
- package/dist/tools/issues.js +19 -150
- package/dist/tools/issues.js.map +1 -1
- package/dist/tools/jobs.js +23 -4
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/locator.js +36 -8
- package/dist/tools/locator.js.map +1 -1
- package/dist/tools/metro.js +61 -16
- package/dist/tools/metro.js.map +1 -1
- package/dist/tools/mobileAudit.js +67 -15
- package/dist/tools/mobileAudit.js.map +1 -1
- package/dist/tools/network.js +6 -1
- package/dist/tools/network.js.map +1 -1
- package/dist/tools/note.js +20 -6
- package/dist/tools/note.js.map +1 -1
- package/dist/tools/permissions.js +31 -6
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/plan.js +5 -2
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/prepareIosTarget.js +31 -8
- package/dist/tools/prepareIosTarget.js.map +1 -1
- package/dist/tools/prepareTarget.js +97 -18
- package/dist/tools/prepareTarget.js.map +1 -1
- package/dist/tools/report.js +24 -5
- package/dist/tools/report.js.map +1 -1
- package/dist/tools/resolveArtifact.js +9 -5
- package/dist/tools/resolveArtifact.js.map +1 -1
- package/dist/tools/resolveTarget.js +11 -2
- package/dist/tools/resolveTarget.js.map +1 -1
- package/dist/tools/screenInfo.js +8 -2
- package/dist/tools/screenInfo.js.map +1 -1
- package/dist/tools/screenRecord.js +74 -11
- package/dist/tools/screenRecord.js.map +1 -1
- package/dist/tools/screenshot.js +23 -6
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seed.js +38 -6
- package/dist/tools/seed.js.map +1 -1
- package/dist/tools/smoke.js +16 -3
- package/dist/tools/smoke.js.map +1 -1
- package/dist/tools/snapshot.js +59 -6
- package/dist/tools/snapshot.js.map +1 -1
- package/dist/tools/startSession.js +54 -9
- package/dist/tools/startSession.js.map +1 -1
- package/dist/tools/state.js +62 -15
- package/dist/tools/state.js.map +1 -1
- package/dist/tools/suite.js +350 -298
- package/dist/tools/suite.js.map +1 -1
- package/dist/tools/testSuite.js +94 -28
- package/dist/tools/testSuite.js.map +1 -1
- package/dist/tools/testThis.js +30 -793
- package/dist/tools/testThis.js.map +1 -1
- package/dist/tools/visual.js +124 -18
- package/dist/tools/visual.js.map +1 -1
- package/dist/tools/visualText.js +51 -6
- package/dist/tools/visualText.js.map +1 -1
- package/dist/tools/wait.js +18 -3
- package/dist/tools/wait.js.map +1 -1
- package/dist/tools/wda.js +157 -29
- package/dist/tools/wda.js.map +1 -1
- package/dist/version.js +17 -49
- package/dist/version.js.map +1 -1
- package/dist/visual/ocr.js +12 -2
- package/dist/visual/ocr.js.map +1 -1
- package/dist/visual/provider.js +7 -3
- package/dist/visual/provider.js.map +1 -1
- package/docs/mcp-server.md +44 -4
- package/docs/tools.md +42 -100
- package/package.json +14 -5
- package/dist/appMap/diff.js +0 -60
- package/dist/appMap/diff.js.map +0 -1
- package/dist/automation/maestroIr.js +0 -282
- package/dist/automation/maestroIr.js.map +0 -1
- package/dist/interop/maestro.js +0 -151
- package/dist/interop/maestro.js.map +0 -1
- package/dist/tools/idling.js +0 -104
- package/dist/tools/idling.js.map +0 -1
- package/dist/tools/inputCapabilities.js +0 -30
- package/dist/tools/inputCapabilities.js.map +0 -1
- package/dist/tools/maestro.js +0 -78
- package/dist/tools/maestro.js.map +0 -1
package/dist/tools/flow.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// qa_flow_check + qa_flow_run (PHASE3-PLAN §3.4) — turn exploration into repeatable QA.
|
|
2
2
|
// Flows live as .swipium/flows/*.yaml. check = parse + static validation (no device).
|
|
3
|
-
// run = the orchestrator (src/flows/run.ts), reporting the exact failing
|
|
3
|
+
// run (mode:"run", default) = the orchestrator (src/flows/run.ts), reporting the exact failing
|
|
4
|
+
// step + evidence. run (mode:"plan", 1.5.0 consolidation) = read-only execution preview:
|
|
5
|
+
// compile the flow against backend capabilities without touching a device.
|
|
4
6
|
import { z } from 'zod';
|
|
5
7
|
import { createHash } from 'node:crypto';
|
|
6
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
9
|
import { join, isAbsolute } from 'node:path';
|
|
8
|
-
import { qaOk, qaError, qaStop } from '../lib/result.js';
|
|
10
|
+
import { qaOk, qaError, qaStop, qaAnnotate } from '../lib/result.js';
|
|
9
11
|
import { parseFlow } from '../flows/schema.js';
|
|
10
12
|
import { lintFlowObjectWithOptions } from '../flows/lint.js';
|
|
11
13
|
import { runFlow } from '../flows/run.js';
|
|
@@ -50,16 +52,19 @@ function loadFlowSource(root, flow, flowYaml) {
|
|
|
50
52
|
return { error: `Flow not found. Looked for: ${candidates.join(', ') || '(no project root)'}` };
|
|
51
53
|
}
|
|
52
54
|
const MUTATING_FLOW_STEPS = new Set(['networkOffline', 'networkOnline', 'seed', 'restartApp']);
|
|
53
|
-
const EXTERNAL_PROVIDER_FLOW_STEPS = new Set(['tapOcrText', 'assertOcrText']);
|
|
54
55
|
function stable(value) {
|
|
55
56
|
if (Array.isArray(value))
|
|
56
57
|
return value.map(stable);
|
|
57
58
|
if (value && typeof value === 'object')
|
|
58
|
-
return Object.fromEntries(Object.entries(value)
|
|
59
|
+
return Object.fromEntries(Object.entries(value)
|
|
60
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
61
|
+
.map(([k, v]) => [k, stable(v)]));
|
|
59
62
|
return value;
|
|
60
63
|
}
|
|
61
64
|
function fullHash(value) {
|
|
62
|
-
return createHash('sha256')
|
|
65
|
+
return createHash('sha256')
|
|
66
|
+
.update(JSON.stringify(stable(value)))
|
|
67
|
+
.digest('hex');
|
|
63
68
|
}
|
|
64
69
|
function hash(value) {
|
|
65
70
|
return fullHash(value).slice(0, 16);
|
|
@@ -92,7 +97,8 @@ function flowMutationAffects(flow, session, source, repeat, externalProviders =
|
|
|
92
97
|
function flowMutationRisk(affects) {
|
|
93
98
|
if (affects.mutations.some((m) => m.kind === 'seed' && m.seedType === 'script'))
|
|
94
99
|
return 'high';
|
|
95
|
-
if (affects.externalProviders.length ||
|
|
100
|
+
if (affects.externalProviders.length ||
|
|
101
|
+
affects.mutations.some((m) => m.kind === 'networkOffline' || m.kind === 'networkOnline' || m.kind === 'seed'))
|
|
96
102
|
return 'medium';
|
|
97
103
|
return 'low';
|
|
98
104
|
}
|
|
@@ -108,28 +114,44 @@ function ocrSteps(flow) {
|
|
|
108
114
|
export function registerFlow(server, sessions) {
|
|
109
115
|
server.registerTool('qa_flow_check', {
|
|
110
116
|
title: 'Check a flow',
|
|
111
|
-
description: 'Parse and statically validate a Swipium flow (a .swipium/flows/*.yaml authored as name + steps) WITHOUT running it. Reports syntax/schema errors with the offending step, plus warnings (e.g. selector resolvability is only known at run time). Provide `flow` (a name under .swipium/flows, or a path) or `flowYaml` (inline). A sessionId lets it resolve names against that session\'s project root.',
|
|
117
|
+
description: 'Parse and statically validate a Swipium flow (a .swipium/flows/*.yaml authored as name + steps) WITHOUT running it. Reports syntax/schema errors with the offending step, plus warnings (e.g. selector resolvability is only known at run time). Provide `flow` (a name under .swipium/flows, or a path) or `flowYaml` (inline). A sessionId lets it resolve names against that session\'s project root. Related: qa_flow_check is the STATIC LINT of the YAML; qa_flow_run mode:"plan" is the read-only EXECUTION PREVIEW (which backends can run it, step-by-step support); qa_flow_run mode:"run" executes.',
|
|
112
118
|
inputSchema: {
|
|
113
119
|
sessionId: z.string().optional(),
|
|
114
120
|
flow: z.string().optional().describe('Flow name under .swipium/flows, or a path to a .yaml file.'),
|
|
115
121
|
flowYaml: z.string().optional().describe('Inline flow YAML (instead of a file).'),
|
|
116
|
-
platform: z
|
|
117
|
-
|
|
122
|
+
platform: z
|
|
123
|
+
.enum(['android', 'ios', 'cross-platform'])
|
|
124
|
+
.optional()
|
|
125
|
+
.describe('Optional authoring target for platform-aware warnings.'),
|
|
126
|
+
ci: z
|
|
127
|
+
.boolean()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('When true, add CI preflight warnings such as missing variables and mutating steps not allowed by policy.'),
|
|
118
130
|
},
|
|
119
131
|
}, async ({ sessionId, flow, flowYaml, platform, ci }) => {
|
|
120
132
|
const root = sessionId ? sessions.get(sessionId)?.root : undefined;
|
|
121
133
|
const src = loadFlowSource(root, flow, flowYaml);
|
|
122
134
|
if (src.error)
|
|
123
|
-
return qaError({
|
|
135
|
+
return qaError({
|
|
136
|
+
what: src.error,
|
|
137
|
+
changedState: false,
|
|
138
|
+
retrySafe: true,
|
|
139
|
+
nextSteps: ['Pass an existing flow name/path or inline flowYaml.'],
|
|
140
|
+
});
|
|
124
141
|
const { flow: parsed, errors } = parseFlow(src.yamlText);
|
|
125
142
|
if (errors.length || !parsed) {
|
|
126
|
-
return qaError({
|
|
143
|
+
return qaError({
|
|
144
|
+
what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'})`,
|
|
145
|
+
changedState: false,
|
|
146
|
+
retrySafe: true,
|
|
147
|
+
nextSteps: ['Fix the listed errors and re-check.'],
|
|
148
|
+
}, { source: src.source, errors });
|
|
127
149
|
}
|
|
128
150
|
const allSteps = [...parsed.setup, ...parsed.steps, ...parsed.teardown];
|
|
129
151
|
const STRUCTURED_KINDS = new Set(['tap', 'assertVisible', 'assertNotVisible', 'scrollTo', 'waitForVisible', 'inputText']);
|
|
130
152
|
const warnings = [];
|
|
131
153
|
if (!parsed.appId)
|
|
132
|
-
warnings.push(
|
|
154
|
+
warnings.push("No appId — a prepareTarget step will rely on the session's prepared appId.");
|
|
133
155
|
if (allSteps.some((s) => s.kind === 'tap' && s.selector.startsWith('@')))
|
|
134
156
|
warnings.push('Uses @ref selectors — refs are run-time only; prefer text/id selectors for durable flows.');
|
|
135
157
|
if (src.yamlText.includes('${'))
|
|
@@ -151,14 +173,21 @@ export function registerFlow(server, sessions) {
|
|
|
151
173
|
warnings.push('mode:visual but the flow uses structured steps (tap/assertVisible/…) — those need a UI tree and will fail on a visual-only screen.');
|
|
152
174
|
}
|
|
153
175
|
for (const s of allSteps) {
|
|
154
|
-
const text = s.kind === 'tap'
|
|
176
|
+
const text = s.kind === 'tap'
|
|
177
|
+
? s.selector
|
|
178
|
+
: s.kind === 'inputText'
|
|
179
|
+
? (s.into ?? s.value)
|
|
180
|
+
: 'query' in s && typeof s.query === 'string'
|
|
181
|
+
? s.query
|
|
182
|
+
: '';
|
|
155
183
|
if (!text)
|
|
156
184
|
continue;
|
|
157
185
|
if (inferredPlatform === 'ios' && /resource-id=|uiautomator|android\./i.test(text))
|
|
158
186
|
warnings.push(`Android-specific locator in iOS flow: "${text}". Prefer accessibilityIdentifier/name/label on iOS.`);
|
|
159
187
|
if (inferredPlatform === 'android' && /class chain|predicate string|XCUIElementType|accessibility id=/i.test(text))
|
|
160
188
|
warnings.push(`iOS-specific locator in Android flow: "${text}". Prefer resource-id/content-desc/text on Android.`);
|
|
161
|
-
if (inferredPlatform === 'cross-platform' &&
|
|
189
|
+
if (inferredPlatform === 'cross-platform' &&
|
|
190
|
+
/(resource-id=|uiautomator|android\.|accessibility id=|class chain|predicate string|XCUIElementType|xpath=|^\/\/)/i.test(text)) {
|
|
162
191
|
warnings.push(`Platform-specific or brittle locator in cross-platform flow: "${text}". Prefer shared text/testID/accessibility labels.`);
|
|
163
192
|
}
|
|
164
193
|
}
|
|
@@ -171,7 +200,10 @@ export function registerFlow(server, sessions) {
|
|
|
171
200
|
for (const v of missingVars)
|
|
172
201
|
warnings.push(`CI variable: ${v.variable} is required by ${v.flow} step ${v.step} but is not set in the environment.`);
|
|
173
202
|
}
|
|
174
|
-
const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, {
|
|
203
|
+
const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, {
|
|
204
|
+
platform,
|
|
205
|
+
policy: ci ? (root ? loadPolicy(root) : null) : undefined,
|
|
206
|
+
});
|
|
175
207
|
for (const f of lintFindings) {
|
|
176
208
|
const prefix = f.severity === 'error' ? 'Lint error' : 'Lint warning';
|
|
177
209
|
warnings.push(`${prefix} ${f.code}${f.step ? ` step ${f.step}` : ''}: ${f.message}`);
|
|
@@ -179,23 +211,65 @@ export function registerFlow(server, sessions) {
|
|
|
179
211
|
// Image-template steps: verify the referenced files exist (best-effort, when a root is known).
|
|
180
212
|
if (root) {
|
|
181
213
|
for (const s of allSteps) {
|
|
182
|
-
if (
|
|
214
|
+
if (s.kind === 'tapImage' || s.kind === 'assertImage') {
|
|
183
215
|
const p = isAbsolute(s.template) ? s.template : join(root, s.template);
|
|
184
216
|
if (!existsSync(p))
|
|
185
217
|
warnings.push(`image template not found: ${s.template} (resolve relative to the project root).`);
|
|
186
218
|
}
|
|
187
219
|
}
|
|
188
220
|
}
|
|
189
|
-
return qaOk({
|
|
221
|
+
return qaOk({
|
|
222
|
+
valid: true,
|
|
223
|
+
source: src.source,
|
|
224
|
+
name: parsed.name,
|
|
225
|
+
appId: parsed.appId ?? null,
|
|
226
|
+
mode: parsed.mode,
|
|
227
|
+
budgetProfile: parsed.budgetProfile ?? null,
|
|
228
|
+
stepCount: parsed.steps.length,
|
|
229
|
+
setupCount: parsed.setup.length,
|
|
230
|
+
teardownCount: parsed.teardown.length,
|
|
231
|
+
fixtures: parsed.fixtures,
|
|
232
|
+
warnings,
|
|
233
|
+
lintFindings,
|
|
234
|
+
}, `✅ flow "${parsed.name}" is valid — ${parsed.steps.length} steps (mode=${parsed.mode}${parsed.setup.length ? `, ${parsed.setup.length} setup` : ''}${parsed.teardown.length ? `, ${parsed.teardown.length} teardown` : ''})${warnings.length ? `\nwarnings:\n - ${warnings.join('\n - ')}` : ''}`);
|
|
190
235
|
});
|
|
191
|
-
server.registerTool('
|
|
192
|
-
title: '
|
|
193
|
-
description: '
|
|
236
|
+
server.registerTool('qa_flow_run', {
|
|
237
|
+
title: 'Run a flow (or preview its execution plan)',
|
|
238
|
+
description: 'Run a Swipium flow against the prepared app and report the result — or, with mode:"plan", preview the execution WITHOUT touching a device. mode:"run" (default) drives steps server-side: tap/tapAt/tapImage, inputText (incl. { into, text } to focus a named field), assertVisible/assertNotVisible/assertImage/assertVisual/assertDiff, swipe (device-relative { direction, area, distance }), scrollTo, press, openUrl, wait/waitForIdle/waitForVisible, clearOverlay, networkOffline/Online, restartApp, seed, note, screenshot — with optional setup/teardown (teardown always runs) and flow mode structured|visual|auto. Fail-fast, NO auto-retry of mutating steps; on failure returns the exact failing step + screenshot + typed failureCode + health. mode:"plan" (read-only) compiles the flow into a deterministic backend-specific automation plan: per backend, can it run, each step classified native | supported_with_fallback | visual_only | unsupported with the missing capability, and an agent-repeatable ready | candidate | blocked message. Provide `flow` (name/path) or `flowYaml`, plus `variables` for any ${VAR}. Related: qa_flow_check is the STATIC LINT of the YAML; mode:"plan" here is the read-only execution preview.',
|
|
194
239
|
inputSchema: {
|
|
195
|
-
|
|
240
|
+
mode: z
|
|
241
|
+
.enum(['plan', 'run'])
|
|
242
|
+
.optional()
|
|
243
|
+
.describe("run (default): execute the flow on the session's prepared device. plan: READ-ONLY execution preview — no device is touched; compiles the flow against backend capabilities (Android direct, iOS raw simulator, iOS WDA, Appium UiAutomator2/XCUITest) and reports what would run, what falls back, and what is blocked."),
|
|
244
|
+
sessionId: z
|
|
245
|
+
.string()
|
|
246
|
+
.optional()
|
|
247
|
+
.describe('REQUIRED for mode:"run" (needs a prepared device). Optional for mode:"plan" — it resolves flow names against the session\'s project root and marks the attached backend.'),
|
|
196
248
|
flow: z.string().optional().describe('Flow name under .swipium/flows, or a path to a .yaml file.'),
|
|
197
249
|
flowYaml: z.string().optional().describe('Inline flow YAML (instead of a file).'),
|
|
198
|
-
|
|
250
|
+
variables: z
|
|
251
|
+
.record(z.string())
|
|
252
|
+
.optional()
|
|
253
|
+
.describe('(mode:"run" only) Values for ${VAR} placeholders (merged over process.env). Credential-looking names are auto-redacted.'),
|
|
254
|
+
repeat: z
|
|
255
|
+
.number()
|
|
256
|
+
.int()
|
|
257
|
+
.min(1)
|
|
258
|
+
.max(10)
|
|
259
|
+
.optional()
|
|
260
|
+
.describe('(mode:"run" only) Run the flow N times for flake detection (default 1). Reports pass-rate + a deterministic-pass | deterministic-fail | flaky classification.'),
|
|
261
|
+
consentId: z
|
|
262
|
+
.string()
|
|
263
|
+
.optional()
|
|
264
|
+
.describe('(mode:"run" only) Consent token returned when the flow contains mutating steps or external visual-provider steps such as OCR.'),
|
|
265
|
+
approve: z
|
|
266
|
+
.boolean()
|
|
267
|
+
.optional()
|
|
268
|
+
.describe('(mode:"run" only) Set true with consentId to execute after reviewing the exact flow hash, mutation list, and external provider command if present.'),
|
|
269
|
+
backend: z
|
|
270
|
+
.enum(['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest'])
|
|
271
|
+
.optional()
|
|
272
|
+
.describe('(mode:"plan" only) Plan only for this backend. Omit to plan across all five (the pre-run "can this run on Android and iOS?" answer).'),
|
|
199
273
|
appium: z
|
|
200
274
|
.object({
|
|
201
275
|
automationName: z.string().optional(),
|
|
@@ -203,113 +277,152 @@ export function registerFlow(server, sessions) {
|
|
|
203
277
|
webviewContextsAvailable: z.boolean().optional(),
|
|
204
278
|
})
|
|
205
279
|
.optional()
|
|
206
|
-
.describe('Appium session hints that refine the Appium backend capability picture (driver family, WebView contexts).'),
|
|
280
|
+
.describe('(mode:"plan" only) Appium session hints that refine the Appium backend capability picture (driver family, WebView contexts).'),
|
|
207
281
|
},
|
|
208
|
-
}, async ({ sessionId, flow, flowYaml, backend, appium }) => {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
282
|
+
}, async ({ mode, sessionId, flow, flowYaml, variables, repeat, consentId, approve, backend, appium }) => {
|
|
283
|
+
const effectiveMode = mode ?? 'run';
|
|
284
|
+
const notes = [];
|
|
285
|
+
// ---- mode:"plan" — read-only execution preview (merged twin, 1.5.0). ----
|
|
286
|
+
if (effectiveMode === 'plan') {
|
|
287
|
+
const ignored = [
|
|
288
|
+
variables !== undefined && 'variables',
|
|
289
|
+
repeat !== undefined && 'repeat',
|
|
290
|
+
consentId !== undefined && 'consentId',
|
|
291
|
+
approve !== undefined && 'approve',
|
|
292
|
+
].filter((x) => !!x);
|
|
293
|
+
if (ignored.length)
|
|
294
|
+
notes.push(`ignored parameter(s) not applicable to mode:"plan": ${ignored.join(', ')} — re-run with mode:"run" to execute`);
|
|
295
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
296
|
+
const root = session?.root;
|
|
297
|
+
const src = loadFlowSource(root, flow, flowYaml);
|
|
298
|
+
if (src.error)
|
|
299
|
+
return qaAnnotate(qaError({
|
|
300
|
+
what: src.error,
|
|
301
|
+
changedState: false,
|
|
302
|
+
retrySafe: true,
|
|
303
|
+
nextSteps: ['Pass an existing flow name/path or inline flowYaml.'],
|
|
304
|
+
}), notes);
|
|
305
|
+
const { flow: parsed, errors } = parseFlow(src.yamlText);
|
|
306
|
+
if (errors.length || !parsed) {
|
|
307
|
+
return qaAnnotate(qaError({
|
|
308
|
+
what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}) — run qa_flow_check`,
|
|
309
|
+
changedState: false,
|
|
310
|
+
retrySafe: true,
|
|
311
|
+
nextSteps: ['Fix the listed errors and re-plan.'],
|
|
312
|
+
}, { source: src.source, errors }), notes);
|
|
313
|
+
}
|
|
314
|
+
const appiumHints = appium ? { ...appium } : undefined;
|
|
315
|
+
const attachedBackend = session?.driver?.kind ? backendForDriverKind(session.driver.kind, appiumHints) : undefined;
|
|
316
|
+
// Which backends to plan for: an explicit one, else all concrete backends (so the answer covers
|
|
317
|
+
// Android + iOS even with no device attached — the pre-run "can this run?" product question).
|
|
318
|
+
const targets = backend ? [backend] : [...ALL_BACKENDS];
|
|
319
|
+
if (attachedBackend && attachedBackend !== 'unknown' && !targets.includes(attachedBackend))
|
|
320
|
+
targets.unshift(attachedBackend);
|
|
321
|
+
const plans = targets.map((b) => {
|
|
322
|
+
const caps = backendCapabilities(b, appiumHints);
|
|
323
|
+
const plan = compileAutomationPlan(parsed, caps);
|
|
324
|
+
const readiness = buildReadiness(plan);
|
|
325
|
+
return {
|
|
326
|
+
backend: b,
|
|
327
|
+
attached: b === attachedBackend,
|
|
328
|
+
status: readiness.status,
|
|
329
|
+
headline: readiness.headline,
|
|
330
|
+
executable: plan.executable,
|
|
331
|
+
supportedSteps: readiness.supportedSteps,
|
|
332
|
+
unsupportedSteps: readiness.unsupportedSteps,
|
|
333
|
+
fallbackUsed: readiness.fallbackUsed,
|
|
334
|
+
agentMessage: readiness.agentMessage,
|
|
335
|
+
developerMessage: readiness.developerMessage,
|
|
336
|
+
blockers: readiness.blockers,
|
|
337
|
+
warnings: readiness.warnings,
|
|
338
|
+
steps: plan.steps.map((s) => ({
|
|
339
|
+
index: s.index,
|
|
340
|
+
kind: s.action.kind,
|
|
341
|
+
target: s.action.note,
|
|
342
|
+
support: s.support,
|
|
343
|
+
reason: s.reason,
|
|
344
|
+
requiredCapability: s.requiredCapability,
|
|
345
|
+
})),
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
const summaryLines = plans.map((p) => {
|
|
349
|
+
const head = `${STATUS_ICON[p.status] ?? '•'} ${p.backend}${p.attached ? ' (attached)' : ''}: ${p.status} — ${p.supportedSteps} supported, ${p.unsupportedSteps} unsupported`;
|
|
350
|
+
return `${head}\n ${p.agentMessage}`;
|
|
351
|
+
});
|
|
352
|
+
const summary = `flow "${parsed.name}" — automation plan across ${plans.length} backend(s) (mode:"plan" — read-only, nothing executed):\n${summaryLines.join('\n')}`;
|
|
353
|
+
return qaAnnotate(qaOk({ flow: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, source: src.source, plans }, summary), notes);
|
|
217
354
|
}
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
if (attachedBackend && attachedBackend !== 'unknown' && !targets.includes(attachedBackend))
|
|
224
|
-
targets.unshift(attachedBackend);
|
|
225
|
-
const plans = targets.map((b) => {
|
|
226
|
-
const caps = backendCapabilities(b, appiumHints);
|
|
227
|
-
const plan = compileAutomationPlan(parsed, caps);
|
|
228
|
-
const readiness = buildReadiness(plan);
|
|
229
|
-
return {
|
|
230
|
-
backend: b,
|
|
231
|
-
attached: b === attachedBackend,
|
|
232
|
-
status: readiness.status,
|
|
233
|
-
headline: readiness.headline,
|
|
234
|
-
executable: plan.executable,
|
|
235
|
-
supportedSteps: readiness.supportedSteps,
|
|
236
|
-
unsupportedSteps: readiness.unsupportedSteps,
|
|
237
|
-
fallbackUsed: readiness.fallbackUsed,
|
|
238
|
-
agentMessage: readiness.agentMessage,
|
|
239
|
-
developerMessage: readiness.developerMessage,
|
|
240
|
-
blockers: readiness.blockers,
|
|
241
|
-
warnings: readiness.warnings,
|
|
242
|
-
steps: plan.steps.map((s) => ({ index: s.index, kind: s.action.kind, target: s.action.note, support: s.support, reason: s.reason, requiredCapability: s.requiredCapability })),
|
|
243
|
-
};
|
|
244
|
-
});
|
|
245
|
-
const summaryLines = plans.map((p) => {
|
|
246
|
-
const head = `${STATUS_ICON[p.status] ?? '•'} ${p.backend}${p.attached ? ' (attached)' : ''}: ${p.status} — ${p.supportedSteps} supported, ${p.unsupportedSteps} unsupported`;
|
|
247
|
-
return `${head}\n ${p.agentMessage}`;
|
|
248
|
-
});
|
|
249
|
-
const summary = `flow "${parsed.name}" — automation plan across ${plans.length} backend(s):\n${summaryLines.join('\n')}`;
|
|
250
|
-
return qaOk({ flow: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, source: src.source, plans }, summary);
|
|
251
|
-
});
|
|
252
|
-
server.registerTool('qa_flow_run', {
|
|
253
|
-
title: 'Run a flow',
|
|
254
|
-
description: 'Run a Swipium flow against the prepared app and report the result. Drives steps server-side: tap/tapAt/tapImage, inputText (incl. { into, text } to focus a named field), assertVisible/assertNotVisible/assertImage/assertVisual/assertDiff, swipe (device-relative { direction, area, distance }), scrollTo, press, openUrl, wait/waitForIdle/waitForVisible, clearOverlay, networkOffline/Online, restartApp, seed, note, screenshot — with optional setup/teardown (teardown always runs) and mode structured|visual|auto. Fail-fast, NO auto-retry of mutating steps; on failure returns the exact failing step + screenshot + typed failureCode + health. Provide `flow` (name/path) or `flowYaml`, plus `variables` for any ${VAR}.',
|
|
255
|
-
inputSchema: {
|
|
256
|
-
sessionId: z.string(),
|
|
257
|
-
flow: z.string().optional(),
|
|
258
|
-
flowYaml: z.string().optional(),
|
|
259
|
-
variables: z.record(z.string()).optional().describe('Values for ${VAR} placeholders (merged over process.env). Credential-looking names are auto-redacted.'),
|
|
260
|
-
repeat: z.number().int().min(1).max(10).optional().describe('Run the flow N times for flake detection (default 1). Reports pass-rate + a deterministic-pass | deterministic-fail | flaky classification.'),
|
|
261
|
-
consentId: z.string().optional().describe('Consent token returned when the flow contains mutating steps or external visual-provider steps such as OCR.'),
|
|
262
|
-
approve: z.boolean().optional().describe('Set true with consentId to execute after reviewing the exact flow hash, mutation list, and external provider command if present.'),
|
|
263
|
-
},
|
|
264
|
-
}, async ({ sessionId, flow, flowYaml, variables, repeat, consentId, approve }) => {
|
|
265
|
-
const session = sessions.get(sessionId);
|
|
355
|
+
// ---- mode:"run" — execute on the prepared device. ----
|
|
356
|
+
const runIgnored = [backend !== undefined && 'backend', appium !== undefined && 'appium'].filter((x) => !!x);
|
|
357
|
+
if (runIgnored.length)
|
|
358
|
+
notes.push(`ignored parameter(s) not applicable to mode:"run": ${runIgnored.join(', ')} — they refine the mode:"plan" preview`);
|
|
359
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
266
360
|
const { driver } = session ? await getDriver(session) : { driver: undefined };
|
|
267
361
|
if (!session || !driver) {
|
|
268
|
-
return qaError({
|
|
362
|
+
return qaAnnotate(qaError({
|
|
363
|
+
what: sessionId
|
|
364
|
+
? 'No device attached to this session'
|
|
365
|
+
: 'qa_flow_run mode:"run" needs a sessionId with a prepared device (mode:"plan" works without one)',
|
|
366
|
+
changedState: false,
|
|
367
|
+
retrySafe: true,
|
|
368
|
+
nextSteps: ['Call qa_prepare_target / qa_ios boot first, then qa_flow_run.'],
|
|
369
|
+
}), notes);
|
|
269
370
|
}
|
|
270
371
|
const src = loadFlowSource(session.root, flow, flowYaml);
|
|
271
372
|
if (src.error)
|
|
272
|
-
return qaError({
|
|
373
|
+
return qaAnnotate(qaError({
|
|
374
|
+
what: src.error,
|
|
375
|
+
changedState: false,
|
|
376
|
+
retrySafe: true,
|
|
377
|
+
nextSteps: ['Pass an existing flow name/path or inline flowYaml.'],
|
|
378
|
+
}), notes);
|
|
273
379
|
const { flow: parsed, errors } = parseFlow(src.yamlText);
|
|
274
380
|
if (errors.length || !parsed) {
|
|
275
|
-
return qaError({
|
|
381
|
+
return qaAnnotate(qaError({
|
|
382
|
+
what: `Flow is invalid — run qa_flow_check first`,
|
|
383
|
+
changedState: false,
|
|
384
|
+
retrySafe: true,
|
|
385
|
+
nextSteps: ['Fix the flow and re-run.'],
|
|
386
|
+
}, { errors }), notes);
|
|
276
387
|
}
|
|
277
388
|
// Backend/mode gate (NEXT-PLAN: catch unsupported combos before running). A structured-mode
|
|
278
389
|
// flow needs a UI tree, which the iOS simulator and a visual-fallback session don't have.
|
|
279
390
|
if (parsed.mode === 'structured' && (driver.kind === 'simulator' || session.mode === 'visual-fallback')) {
|
|
280
|
-
return qaError({
|
|
391
|
+
return qaAnnotate(qaError({
|
|
281
392
|
what: `This flow is mode:structured but ${driver.kind === 'simulator' ? 'the iOS simulator backend has no UI tree' : 'the session is in visual-fallback'}`,
|
|
282
393
|
changedState: false,
|
|
283
394
|
retrySafe: false,
|
|
284
395
|
failureCode: 'BACKEND_UNSUPPORTED',
|
|
285
|
-
nextSteps: [
|
|
286
|
-
|
|
396
|
+
nextSteps: [
|
|
397
|
+
'Author the flow with `mode: visual` (or auto) and use image/visual steps (tapImage/assertImage/assertDiff/assertVisual).',
|
|
398
|
+
],
|
|
399
|
+
}), notes);
|
|
287
400
|
}
|
|
288
401
|
const budget = sessions.budgetStop(session);
|
|
289
402
|
if (budget)
|
|
290
|
-
return qaStop(budget, { counters: session.counters });
|
|
403
|
+
return qaAnnotate(qaStop(budget, { counters: session.counters }), notes);
|
|
291
404
|
const runs = repeat ?? 1;
|
|
292
405
|
const externalProviderSteps = ocrSteps(parsed);
|
|
293
406
|
const externalProviders = [];
|
|
294
407
|
if (externalProviderSteps.length) {
|
|
295
408
|
if (session.sensitive) {
|
|
296
|
-
return qaError({
|
|
409
|
+
return qaAnnotate(qaError({
|
|
297
410
|
what: 'Sensitive mode refuses OCR flow steps because they would pass screenshots to an external provider',
|
|
298
411
|
changedState: false,
|
|
299
412
|
retrySafe: false,
|
|
300
413
|
failureCode: 'UNSAFE_ACTION_REFUSED',
|
|
301
414
|
nextSteps: ['Disable sensitive mode only for a safe, masked QA screen, or remove tapOcrText/assertOcrText from the flow.'],
|
|
302
|
-
});
|
|
415
|
+
}), notes);
|
|
303
416
|
}
|
|
304
417
|
const command = configuredOcrCommand(session.root);
|
|
305
418
|
if (!command) {
|
|
306
|
-
return qaError({
|
|
419
|
+
return qaAnnotate(qaError({
|
|
307
420
|
what: 'Flow uses OCR text steps, but OCR is not configured',
|
|
308
421
|
changedState: false,
|
|
309
422
|
retrySafe: true,
|
|
310
423
|
failureCode: 'VISUAL_ONLY_SCREEN',
|
|
311
424
|
nextSteps: ['Configure ocrCommand in .swipium/config.json or remove tapOcrText/assertOcrText from the flow.'],
|
|
312
|
-
});
|
|
425
|
+
}), notes);
|
|
313
426
|
}
|
|
314
427
|
try {
|
|
315
428
|
const preview = resolveVisualProvider(command, { image: '<screenshot>' }, 30000);
|
|
@@ -325,18 +438,24 @@ export function registerFlow(server, sessions) {
|
|
|
325
438
|
});
|
|
326
439
|
}
|
|
327
440
|
catch (e) {
|
|
328
|
-
return qaError({
|
|
441
|
+
return qaAnnotate(qaError({
|
|
329
442
|
what: e instanceof GitScopeForbiddenError ? e.message : `Invalid OCR command template: ${String(e)}`,
|
|
330
443
|
changedState: false,
|
|
331
444
|
retrySafe: !(e instanceof GitScopeForbiddenError),
|
|
332
445
|
failureCode: e instanceof GitScopeForbiddenError ? 'GIT_SCOPE_FORBIDDEN' : 'INVALID_FLOW',
|
|
333
|
-
nextSteps: e instanceof GitScopeForbiddenError
|
|
334
|
-
|
|
446
|
+
nextSteps: e instanceof GitScopeForbiddenError
|
|
447
|
+
? ['Run Git yourself outside Swipium; configure ocrCommand to use a non-Git executable.']
|
|
448
|
+
: ['Use an argv array in .swipium/config.json, e.g. ["node","ocr.js","{image}"].'],
|
|
449
|
+
}), notes);
|
|
335
450
|
}
|
|
336
451
|
}
|
|
337
452
|
const affects = flowMutationAffects(parsed, session, src.source, runs, externalProviders);
|
|
338
453
|
const mutationRisk = flowMutationRisk(affects);
|
|
339
|
-
let mutationConsent = {
|
|
454
|
+
let mutationConsent = {
|
|
455
|
+
required: false,
|
|
456
|
+
approved: true,
|
|
457
|
+
payloadHash: affects.flowHash,
|
|
458
|
+
};
|
|
340
459
|
const privileged = affects.mutations.length > 0 || affects.externalProviders.length > 0;
|
|
341
460
|
if (privileged) {
|
|
342
461
|
const payloadHash = fullHash(affects);
|
|
@@ -353,7 +472,7 @@ export function registerFlow(server, sessions) {
|
|
|
353
472
|
const externalCommand = affects.externalProviders.length
|
|
354
473
|
? `; external provider: ${displayArgv(affects.externalProviders[0].argv ?? [])}`
|
|
355
474
|
: '';
|
|
356
|
-
return requireConsent({
|
|
475
|
+
return qaAnnotate(requireConsent({
|
|
357
476
|
action: 'flow_mutation_run',
|
|
358
477
|
risk: mutationRisk,
|
|
359
478
|
exactCommand: `qa_flow_run ${parsed.name} (${[...affects.mutations.map((m) => `${m.step}:${m.kind}`), ...externalProviderSteps.map((s) => `${s.step}:${s.kind}`)].join(', ')})${externalCommand}`,
|
|
@@ -361,7 +480,7 @@ export function registerFlow(server, sessions) {
|
|
|
361
480
|
explain: affects.externalProviders.length
|
|
362
481
|
? `Run flow "${parsed.name}" with external visual provider steps? OCR steps pass ${runs * externalProviderSteps.length} screenshot(s) to the configured provider and any mutating steps can change app/device/test state.`
|
|
363
482
|
: `Run mutating flow "${parsed.name}"? Mutating steps can change app/device/test state and will be recorded in the mutation ledger.`,
|
|
364
|
-
});
|
|
483
|
+
}), notes);
|
|
365
484
|
}
|
|
366
485
|
mutationConsent = { required: true, consentId, approved: true, payloadHash };
|
|
367
486
|
sessions.recordMutation(session, {
|
|
@@ -383,15 +502,31 @@ export function registerFlow(server, sessions) {
|
|
|
383
502
|
results.push(await runFlow(sessions, session, driver, parsed, { variables, mutationConsent }));
|
|
384
503
|
}
|
|
385
504
|
const flake = classifyFlakeResults(results, runs);
|
|
386
|
-
return qaOk({
|
|
505
|
+
return qaAnnotate(qaOk({
|
|
506
|
+
flow: parsed.name,
|
|
507
|
+
runs: results.length,
|
|
508
|
+
passes: flake.passed,
|
|
509
|
+
fails: flake.failed,
|
|
510
|
+
passRate: flake.passRate,
|
|
511
|
+
classification: flake.classification,
|
|
512
|
+
triage: flake.triage,
|
|
513
|
+
results: results.map((r) => ({
|
|
514
|
+
passed: r.passed,
|
|
515
|
+
failedAtStep: r.failedAtStep,
|
|
516
|
+
reason: r.reason,
|
|
517
|
+
failureCode: r.failureCode,
|
|
518
|
+
})),
|
|
519
|
+
}, `flow "${parsed.name}" × ${results.length}: ${flake.passed} passed, ${flake.failed} failed → ${flake.classification === 'flaky' ? '⚠ FLAKY' : flake.classification === 'deterministic-pass' ? '✅ deterministic-pass' : '❌ deterministic-fail'} (${flake.passRate}% pass-rate); triage=${flake.triage.likelyCause}`), notes);
|
|
387
520
|
}
|
|
388
521
|
const result = await runFlow(sessions, session, driver, parsed, { variables, mutationConsent });
|
|
389
522
|
const head = `flow "${result.name}" ${result.passed ? '✅ PASSED' : `❌ FAILED at step ${result.failedAtStep} (${result.reason})`} ` +
|
|
390
|
-
`— ${result.steps.filter((s) => s.ok).length}/${result.steps.length} steps in ${result.
|
|
523
|
+
`— ${result.steps.filter((s) => s.ok).length}/${result.steps.length} steps in ${Math.round(result.durationMs / 100) / 10}s` +
|
|
391
524
|
(result.appHealth ? `\nhealth: native=${result.nativeHealth} app=${result.appHealth}` : '');
|
|
392
|
-
const stepLines = result.steps
|
|
525
|
+
const stepLines = result.steps
|
|
526
|
+
.map((s) => `${s.ok ? '✓' : '✗'} ${s.index}. ${s.summary}${s.detail ? ` — ${s.detail}` : ''}${s.screenshotUri ? `\n evidence: ${s.screenshotUri}` : ''}`)
|
|
527
|
+
.join('\n');
|
|
393
528
|
// A failed flow is a structured result, not a protocol error — the agent should record it.
|
|
394
|
-
return qaOk({ ...result }, `${head}\n${stepLines}`);
|
|
529
|
+
return qaAnnotate(qaOk({ ...result }, `${head}\n${stepLines}`), notes);
|
|
395
530
|
});
|
|
396
531
|
}
|
|
397
532
|
//# sourceMappingURL=flow.js.map
|