swipium 1.4.0 → 2.0.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 +313 -0
- package/README.md +175 -219
- package/THREAT_MODEL.md +223 -47
- package/dist/appMap/automationLink.js +39 -26
- package/dist/appMap/automationLink.js.map +1 -1
- package/dist/appMap/build.js +68 -20
- package/dist/appMap/build.js.map +1 -1
- package/dist/appMap/codeIndex.js +31 -5
- package/dist/appMap/codeIndex.js.map +1 -1
- package/dist/appMap/featureIndex.js +23 -5
- package/dist/appMap/featureIndex.js.map +1 -1
- package/dist/appMap/featureModel.js +44 -8
- package/dist/appMap/featureModel.js.map +1 -1
- package/dist/appMap/firstRunApply.js +24 -6
- 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 +10 -30
- package/dist/appMap/issues.js.map +1 -1
- package/dist/appMap/migrations.js +6 -6
- package/dist/appMap/migrations.js.map +1 -1
- package/dist/appMap/prelaunch.js +2 -2
- package/dist/appMap/prelaunch.js.map +1 -1
- package/dist/appMap/projectRegistry.js +83 -11
- package/dist/appMap/projectRegistry.js.map +1 -1
- package/dist/appMap/provenance.js +2 -2
- package/dist/appMap/provenance.js.map +1 -1
- package/dist/appMap/query.js +21 -5
- package/dist/appMap/query.js.map +1 -1
- package/dist/appMap/runtimeMerge.js +12 -5
- package/dist/appMap/runtimeMerge.js.map +1 -1
- package/dist/appMap/schema.js +1 -1
- package/dist/appMap/schema.js.map +1 -1
- package/dist/appMap/screenMatch.js +6 -2
- package/dist/appMap/screenMatch.js.map +1 -1
- package/dist/appMap/staticScan.js +151 -39
- package/dist/appMap/staticScan.js.map +1 -1
- package/dist/appMap/store.js +58 -85
- package/dist/appMap/store.js.map +1 -1
- package/dist/appMap/tsAstScan.js +34 -12
- package/dist/appMap/tsAstScan.js.map +1 -1
- package/dist/artifacts/bundletool.js +49 -12
- package/dist/artifacts/bundletool.js.map +1 -1
- package/dist/artifacts/resolve.js +41 -14
- package/dist/artifacts/resolve.js.map +1 -1
- package/dist/automation/capabilities.js +112 -30
- package/dist/automation/capabilities.js.map +1 -1
- package/dist/automation/gestures.js +6 -2
- package/dist/automation/gestures.js.map +1 -1
- package/dist/automation/plan.js +71 -23
- package/dist/automation/plan.js.map +1 -1
- package/dist/automation/report.js +15 -5
- package/dist/automation/report.js.map +1 -1
- package/dist/automation/selectors.js +22 -28
- package/dist/automation/selectors.js.map +1 -1
- package/dist/automation/types.js +1 -1
- package/dist/automation/types.js.map +1 -1
- package/dist/automation/waits.js +14 -8
- package/dist/automation/waits.js.map +1 -1
- package/dist/automation/webview.js +9 -3
- package/dist/automation/webview.js.map +1 -1
- package/dist/automationGen/appiumModel.js +16 -8
- package/dist/automationGen/appiumModel.js.map +1 -1
- package/dist/automationGen/ciEmitter.js +6 -6
- package/dist/automationGen/ciEmitter.js.map +1 -1
- package/dist/automationGen/identifiers.js +224 -0
- package/dist/automationGen/identifiers.js.map +1 -0
- package/dist/automationGen/jsEmitter.js +288 -121
- package/dist/automationGen/jsEmitter.js.map +1 -1
- package/dist/automationGen/packagePatch.js +2 -2
- package/dist/automationGen/packagePatch.js.map +1 -1
- package/dist/automationGen/platformResolve.js +36 -0
- package/dist/automationGen/platformResolve.js.map +1 -0
- package/dist/automationGen/projectProfile.js +76 -27
- package/dist/automationGen/projectProfile.js.map +1 -1
- package/dist/automationGen/pythonEmitter.js +278 -107
- package/dist/automationGen/pythonEmitter.js.map +1 -1
- package/dist/automationGen/readmeEmitter.js +7 -6
- package/dist/automationGen/readmeEmitter.js.map +1 -1
- package/dist/automationGen/run.js +388 -0
- package/dist/automationGen/run.js.map +1 -0
- package/dist/automationGen/suitePlan.js +17 -10
- package/dist/automationGen/suitePlan.js.map +1 -1
- package/dist/automationGen/validation.js +52 -12
- package/dist/automationGen/validation.js.map +1 -1
- package/dist/build/parseBuildLog.js +235 -31
- package/dist/build/parseBuildLog.js.map +1 -1
- package/dist/build/plan.js +39 -18
- package/dist/build/plan.js.map +1 -1
- package/dist/ci/preflight.js +13 -4
- package/dist/ci/preflight.js.map +1 -1
- package/dist/cli/gc.js +74 -0
- package/dist/cli/gc.js.map +1 -0
- package/dist/cli/init.js +217 -26
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.js +50 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/report.js +218 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/scan.js +36 -15
- package/dist/cli/scan.js.map +1 -1
- package/dist/cli/suite.js +7 -7
- package/dist/cli/suite.js.map +1 -1
- package/dist/cli/verify.js +7 -7
- package/dist/cli/verify.js.map +1 -1
- package/dist/consent/consent.js +162 -5
- package/dist/consent/consent.js.map +1 -1
- package/dist/context/detect.js +55 -18
- package/dist/context/detect.js.map +1 -1
- package/dist/context/findApps.js +22 -8
- package/dist/context/findApps.js.map +1 -1
- package/dist/context/projectRoot.js +174 -19
- package/dist/context/projectRoot.js.map +1 -1
- package/dist/context/scan.js +1 -1
- package/dist/context/scan.js.map +1 -1
- package/dist/core/capabilityGroups.js +90 -0
- package/dist/core/capabilityGroups.js.map +1 -0
- package/dist/core/target.js +10 -8
- package/dist/core/target.js.map +1 -1
- package/dist/core/targetPlan.js +160 -39
- package/dist/core/targetPlan.js.map +1 -1
- package/dist/drivers/DirectDriver.js +317 -43
- package/dist/drivers/DirectDriver.js.map +1 -1
- package/dist/drivers/SimctlDriver.js +124 -8
- package/dist/drivers/SimctlDriver.js.map +1 -1
- package/dist/drivers/WdaDriver.js +365 -55
- package/dist/drivers/WdaDriver.js.map +1 -1
- package/dist/explore/candidates.js +7 -3
- package/dist/explore/candidates.js.map +1 -1
- package/dist/explore/graph.js +15 -6
- 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 +11 -6
- package/dist/explore/policy.js.map +1 -1
- package/dist/explore/runner.js +363 -190
- package/dist/explore/runner.js.map +1 -1
- package/dist/explore/signatures.js +4 -2
- 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 +137 -29
- package/dist/featureTesting/executionBootstrap.js.map +1 -1
- package/dist/featureTesting/featureMap.js +6 -3
- package/dist/featureTesting/featureMap.js.map +1 -1
- package/dist/featureTesting/featureScope.js +101 -20
- package/dist/featureTesting/featureScope.js.map +1 -1
- package/dist/featureTesting/mapFeatureScope.js +44 -15
- package/dist/featureTesting/mapFeatureScope.js.map +1 -1
- package/dist/featureTesting/objectiveModel.js +15 -11
- package/dist/featureTesting/objectiveModel.js.map +1 -1
- package/dist/featureTesting/resultMerge.js +16 -8
- package/dist/featureTesting/resultMerge.js.map +1 -1
- package/dist/featureTesting/sources.js +8 -5
- package/dist/featureTesting/sources.js.map +1 -1
- package/dist/featureTesting/suiteBridge.js +8 -2
- package/dist/featureTesting/suiteBridge.js.map +1 -1
- package/dist/featureTesting/synonyms.js +32 -4
- package/dist/featureTesting/synonyms.js.map +1 -1
- package/dist/featureTesting/testCaseFactory.js +27 -15
- package/dist/featureTesting/testCaseFactory.js.map +1 -1
- package/dist/featureTesting/testPlan.js +34 -12
- package/dist/featureTesting/testPlan.js.map +1 -1
- package/dist/firstRun/authStateMachine.js +1 -1
- package/dist/firstRun/authStateMachine.js.map +1 -1
- package/dist/firstRun/classifyScreen.js +9 -6
- package/dist/firstRun/classifyScreen.js.map +1 -1
- package/dist/firstRun/firstRunPlanner.js +16 -13
- package/dist/firstRun/firstRunPlanner.js.map +1 -1
- package/dist/firstRun/firstRunRunner.js +92 -31
- package/dist/firstRun/firstRunRunner.js.map +1 -1
- package/dist/firstRun/generatedDataPolicy.js +25 -15
- package/dist/firstRun/generatedDataPolicy.js.map +1 -1
- package/dist/firstRun/inputPlanner.js +45 -11
- package/dist/firstRun/inputPlanner.js.map +1 -1
- package/dist/firstRun/onboardingStateMachine.js +1 -1
- package/dist/firstRun/onboardingStateMachine.js.map +1 -1
- package/dist/firstRun/paywallPolicy.js +3 -3
- package/dist/firstRun/paywallPolicy.js.map +1 -1
- package/dist/firstRun/types.js +1 -1
- package/dist/firstRun/types.js.map +1 -1
- package/dist/fixtures/catalog.js +72 -9
- package/dist/fixtures/catalog.js.map +1 -1
- package/dist/fixtures/load.js +50 -0
- package/dist/fixtures/load.js.map +1 -0
- package/dist/flows/discover.js +2 -2
- package/dist/flows/discover.js.map +1 -1
- package/dist/flows/generate.js +68 -17
- 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 +21 -5
- package/dist/flows/pack.js.map +1 -1
- package/dist/flows/paths.js +57 -0
- package/dist/flows/paths.js.map +1 -0
- package/dist/flows/repair.js +176 -37
- package/dist/flows/repair.js.map +1 -1
- package/dist/flows/run.js +308 -99
- package/dist/flows/run.js.map +1 -1
- package/dist/flows/schema.js +107 -25
- package/dist/flows/schema.js.map +1 -1
- package/dist/flows/seedExec.js +8 -4
- package/dist/flows/seedExec.js.map +1 -1
- package/dist/flows/templates.js +5 -5
- package/dist/index.js +50 -8
- package/dist/index.js.map +1 -1
- package/dist/ios/signing.js +32 -3
- package/dist/ios/signing.js.map +1 -1
- package/dist/issues/classify.js +28 -20
- package/dist/issues/classify.js.map +1 -1
- package/dist/issues/fingerprint.js +35 -9
- package/dist/issues/fingerprint.js.map +1 -1
- package/dist/issues/index.js +93 -46
- package/dist/issues/index.js.map +1 -1
- package/dist/issues/metrics.js +6 -5
- package/dist/issues/metrics.js.map +1 -1
- package/dist/issues/recurrence.js +35 -8
- package/dist/issues/recurrence.js.map +1 -1
- package/dist/issues/report.js +12 -8
- package/dist/issues/report.js.map +1 -1
- package/dist/issues/reportBridge.js +40 -9
- package/dist/issues/reportBridge.js.map +1 -1
- package/dist/issues/schema.js +2 -2
- package/dist/issues/schema.js.map +1 -1
- package/dist/issues/sourceRevision.js +2 -2
- package/dist/issues/sourceRevision.js.map +1 -1
- package/dist/issues/store.js +110 -32
- package/dist/issues/store.js.map +1 -1
- package/dist/lib/abortScope.js +48 -0
- package/dist/lib/abortScope.js.map +1 -0
- package/dist/lib/android.js +84 -8
- package/dist/lib/android.js.map +1 -1
- package/dist/lib/coordSpace.js +1 -1
- package/dist/lib/coordSpace.js.map +1 -1
- package/dist/lib/device.js +53 -12
- package/dist/lib/device.js.map +1 -1
- package/dist/lib/gestures.js +141 -0
- package/dist/lib/gestures.js.map +1 -0
- package/dist/lib/gitignore.js +4 -3
- package/dist/lib/gitignore.js.map +1 -1
- package/dist/lib/image.js +12 -3
- package/dist/lib/image.js.map +1 -1
- package/dist/lib/lockfile.js +399 -0
- package/dist/lib/lockfile.js.map +1 -0
- package/dist/lib/needsInput.js +9 -6
- package/dist/lib/needsInput.js.map +1 -1
- package/dist/lib/png.js +4 -3
- package/dist/lib/png.js.map +1 -1
- package/dist/lib/redact.js +170 -9
- package/dist/lib/redact.js.map +1 -1
- package/dist/lib/result.js +94 -10
- package/dist/lib/result.js.map +1 -1
- package/dist/lib/schemaHash.js +7 -5
- package/dist/lib/schemaHash.js.map +1 -1
- package/dist/lib/sensitive.js +6 -3
- package/dist/lib/sensitive.js.map +1 -1
- package/dist/lib/simctl.js +3 -3
- package/dist/lib/simctl.js.map +1 -1
- package/dist/lib/spawn.js +78 -17
- package/dist/lib/spawn.js.map +1 -1
- package/dist/lib/toolAnnotations.js +114 -0
- package/dist/lib/toolAnnotations.js.map +1 -0
- package/dist/lib/wda.js +321 -25
- 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 +150 -28
- package/dist/mobileAudit/checks.js.map +1 -1
- package/dist/mobileAudit/evidence.js +4 -4
- package/dist/mobileAudit/evidence.js.map +1 -1
- package/dist/mobileAudit/profiles.js +121 -20
- package/dist/mobileAudit/profiles.js.map +1 -1
- package/dist/mobileAudit/results.js +9 -4
- package/dist/mobileAudit/results.js.map +1 -1
- package/dist/mobileAudit/runner.js +214 -35
- package/dist/mobileAudit/runner.js.map +1 -1
- package/dist/oracle/auth.js +1 -1
- package/dist/oracle/auth.js.map +1 -1
- package/dist/oracle/failures.js +1085 -145
- package/dist/oracle/failures.js.map +1 -1
- package/dist/oracle/health.js +148 -26
- package/dist/oracle/health.js.map +1 -1
- package/dist/oracle/locator.js +148 -19
- package/dist/oracle/locator.js.map +1 -1
- package/dist/oracle/record.js +3 -3
- package/dist/oracle/record.js.map +1 -1
- package/dist/orchestration/envelope.js +2 -2
- package/dist/orchestration/envelope.js.map +1 -1
- package/dist/orchestration/goal.js +7 -13
- package/dist/orchestration/goal.js.map +1 -1
- package/dist/orchestration/testThis/execute.js +167 -0
- package/dist/orchestration/testThis/execute.js.map +1 -0
- package/dist/orchestration/testThis/pipeline.js +413 -0
- package/dist/orchestration/testThis/pipeline.js.map +1 -0
- package/dist/orchestration/testThis/plan.js +519 -0
- package/dist/orchestration/testThis/plan.js.map +1 -0
- package/dist/orchestration/testThis/sessionIntent.js +72 -0
- package/dist/orchestration/testThis/sessionIntent.js.map +1 -0
- package/dist/orchestration/testThis/terminal.js +136 -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 +9 -9
- package/dist/plan/plan.js.map +1 -1
- package/dist/prompts/index.js +24 -24
- 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 +449 -72
- package/dist/report/export.js.map +1 -1
- package/dist/report/findingsDedupe.js +41 -0
- package/dist/report/findingsDedupe.js.map +1 -0
- 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 +18 -7
- package/dist/report/policy.js.map +1 -1
- package/dist/report/qaLevel.js +10 -16
- 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/sarifSources.js +160 -0
- package/dist/report/sarifSources.js.map +1 -0
- package/dist/report/summary.js +2 -2
- package/dist/report/summary.js.map +1 -1
- package/dist/report/testCatalog.js +4 -4
- package/dist/report/testCatalog.js.map +1 -1
- package/dist/report/timing.js +23 -23
- package/dist/report/timing.js.map +1 -1
- package/dist/report/toolHealth.js +124 -0
- package/dist/report/toolHealth.js.map +1 -0
- package/dist/server.js +501 -69
- package/dist/server.js.map +1 -1
- package/dist/services/automationGenerate.js +8 -2
- package/dist/services/automationGenerate.js.map +1 -1
- package/dist/services/build.js +10 -4
- package/dist/services/build.js.map +1 -1
- package/dist/services/flowGenerate.js +75 -0
- package/dist/services/flowGenerate.js.map +1 -0
- package/dist/services/preflight.js +40 -8
- package/dist/services/preflight.js.map +1 -1
- package/dist/services/prepareAndroid.js +92 -18
- package/dist/services/prepareAndroid.js.map +1 -1
- package/dist/services/prepareIos.js +94 -17
- package/dist/services/prepareIos.js.map +1 -1
- package/dist/services/report.js +271 -80
- package/dist/services/report.js.map +1 -1
- package/dist/services/smoke.js +68 -22
- package/dist/services/smoke.js.map +1 -1
- package/dist/services/suiteGenerate.js +43 -6
- package/dist/services/suiteGenerate.js.map +1 -1
- package/dist/services/testSuiteKnowledge.js +78 -33
- package/dist/services/testSuiteKnowledge.js.map +1 -1
- package/dist/session/attach.js +284 -4
- package/dist/session/attach.js.map +1 -1
- package/dist/session/processRegistry.js +433 -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/retention.js +201 -0
- package/dist/session/retention.js.map +1 -0
- package/dist/session/store.js +459 -59
- package/dist/session/store.js.map +1 -1
- package/dist/snapshot/overlays.js +60 -10
- package/dist/snapshot/overlays.js.map +1 -1
- package/dist/snapshot/parse.js +30 -12
- package/dist/snapshot/parse.js.map +1 -1
- package/dist/snapshot/present.js +36 -6
- package/dist/snapshot/present.js.map +1 -1
- package/dist/snapshot/settle.js +39 -9
- package/dist/snapshot/settle.js.map +1 -1
- package/dist/state/consent.js +99 -0
- package/dist/state/consent.js.map +1 -0
- package/dist/state/profile.js +82 -11
- package/dist/state/profile.js.map +1 -1
- package/dist/suite/compile.js +15 -6
- package/dist/suite/compile.js.map +1 -1
- package/dist/suite/lint.js +22 -4
- package/dist/suite/lint.js.map +1 -1
- package/dist/suite/pom.js +75 -44
- package/dist/suite/pom.js.map +1 -1
- package/dist/suite/secretGuard.js +266 -0
- package/dist/suite/secretGuard.js.map +1 -0
- package/dist/suite/testcase.js +15 -11
- package/dist/suite/testcase.js.map +1 -1
- package/dist/testSuite/exporter.js +7 -3
- package/dist/testSuite/exporter.js.map +1 -1
- package/dist/testSuite/generator.js +43 -14
- package/dist/testSuite/generator.js.map +1 -1
- package/dist/testSuite/history.js +1 -1
- package/dist/testSuite/history.js.map +1 -1
- package/dist/testSuite/issueLinks.js +3 -3
- package/dist/testSuite/issueLinks.js.map +1 -1
- package/dist/testSuite/lint.js +4 -4
- package/dist/testSuite/lint.js.map +1 -1
- package/dist/testSuite/merge.js +13 -7
- package/dist/testSuite/merge.js.map +1 -1
- package/dist/testSuite/schema.js +21 -16
- package/dist/testSuite/schema.js.map +1 -1
- package/dist/testSuite/store.js +12 -4
- package/dist/testSuite/store.js.map +1 -1
- package/dist/testSuite/traceability.js +2 -2
- package/dist/testSuite/traceability.js.map +1 -1
- package/dist/tools/act.js +868 -174
- package/dist/tools/act.js.map +1 -1
- package/dist/tools/agent.js +521 -112
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/appControl.js +82 -19
- package/dist/tools/appControl.js.map +1 -1
- package/dist/tools/appMap.js +393 -189
- package/dist/tools/appMap.js.map +1 -1
- package/dist/tools/build.js +92 -51
- package/dist/tools/build.js.map +1 -1
- package/dist/tools/bundletool.js +141 -40
- package/dist/tools/bundletool.js.map +1 -1
- package/dist/tools/clearOverlay.js +98 -18
- package/dist/tools/clearOverlay.js.map +1 -1
- package/dist/tools/device.js +108 -19
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/doctor.js +98 -35
- package/dist/tools/doctor.js.map +1 -1
- package/dist/tools/explore.js +110 -42
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/featureTesting.js +152 -130
- package/dist/tools/featureTesting.js.map +1 -1
- package/dist/tools/firstRun.js +94 -66
- package/dist/tools/firstRun.js.map +1 -1
- package/dist/tools/flow.js +347 -132
- package/dist/tools/flow.js.map +1 -1
- package/dist/tools/flowRepair.js +42 -10
- package/dist/tools/flowRepair.js.map +1 -1
- package/dist/tools/generate.js +199 -0
- package/dist/tools/generate.js.map +1 -0
- package/dist/tools/getArtifact.js +37 -8
- package/dist/tools/getArtifact.js.map +1 -1
- package/dist/tools/health.js +17 -12
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/ios.js +148 -194
- package/dist/tools/ios.js.map +1 -1
- package/dist/tools/issues.js +256 -171
- package/dist/tools/issues.js.map +1 -1
- package/dist/tools/jobs.js +55 -11
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/metro.js +84 -38
- package/dist/tools/metro.js.map +1 -1
- package/dist/tools/mobileAudit.js +83 -26
- package/dist/tools/mobileAudit.js.map +1 -1
- package/dist/tools/network.js +36 -14
- package/dist/tools/network.js.map +1 -1
- package/dist/tools/note.js +31 -17
- package/dist/tools/note.js.map +1 -1
- package/dist/tools/prepareIosTarget.js +31 -14
- package/dist/tools/prepareIosTarget.js.map +1 -1
- package/dist/tools/prepareTarget.js +193 -47
- package/dist/tools/prepareTarget.js.map +1 -1
- package/dist/tools/report.js +16 -8
- package/dist/tools/report.js.map +1 -1
- package/dist/tools/resolveArtifact.js +12 -12
- package/dist/tools/resolveArtifact.js.map +1 -1
- package/dist/tools/resolveTarget.js +81 -18
- package/dist/tools/resolveTarget.js.map +1 -1
- package/dist/tools/screenRecord.js +85 -20
- package/dist/tools/screenRecord.js.map +1 -1
- package/dist/tools/screenshot.js +42 -18
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/smoke.js +29 -12
- package/dist/tools/smoke.js.map +1 -1
- package/dist/tools/snapshot.js +157 -107
- package/dist/tools/snapshot.js.map +1 -1
- package/dist/tools/startSession.js +117 -84
- package/dist/tools/startSession.js.map +1 -1
- package/dist/tools/suite.js +391 -310
- package/dist/tools/suite.js.map +1 -1
- package/dist/tools/testSuite.js +112 -36
- package/dist/tools/testSuite.js.map +1 -1
- package/dist/tools/testThis.js +35 -802
- package/dist/tools/testThis.js.map +1 -1
- package/dist/tools/visual.js +698 -116
- package/dist/tools/visual.js.map +1 -1
- package/dist/tools/wait.js +9 -23
- package/dist/tools/wait.js.map +1 -1
- package/dist/tools/wda.js +312 -54
- package/dist/tools/wda.js.map +1 -1
- package/dist/version.js +37 -64
- package/dist/version.js.map +1 -1
- package/dist/visual/ocr.js +65 -8
- package/dist/visual/ocr.js.map +1 -1
- package/dist/visual/provider.js +87 -15
- package/dist/visual/provider.js.map +1 -1
- package/docs/README.md +24 -10
- package/docs/ci-reports.md +328 -0
- package/docs/concepts.md +227 -0
- package/docs/flows.md +153 -0
- package/docs/mcp-server.md +210 -104
- package/docs/physical-devices.md +84 -0
- package/docs/tools.md +808 -217
- package/package.json +16 -6
- 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/assertVisual.js +0 -74
- package/dist/tools/assertVisual.js.map +0 -1
- package/dist/tools/automationGenerate.js +0 -394
- package/dist/tools/automationGenerate.js.map +0 -1
- package/dist/tools/capabilities.js +0 -209
- package/dist/tools/capabilities.js.map +0 -1
- package/dist/tools/detectContext.js +0 -41
- package/dist/tools/detectContext.js.map +0 -1
- package/dist/tools/flowGenerate.js +0 -55
- package/dist/tools/flowGenerate.js.map +0 -1
- package/dist/tools/history.js +0 -56
- package/dist/tools/history.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/locator.js +0 -57
- package/dist/tools/locator.js.map +0 -1
- package/dist/tools/maestro.js +0 -78
- package/dist/tools/maestro.js.map +0 -1
- package/dist/tools/permissions.js +0 -149
- package/dist/tools/permissions.js.map +0 -1
- package/dist/tools/plan.js +0 -49
- package/dist/tools/plan.js.map +0 -1
- package/dist/tools/screenInfo.js +0 -70
- package/dist/tools/screenInfo.js.map +0 -1
- package/dist/tools/seed.js +0 -101
- package/dist/tools/seed.js.map +0 -1
- package/dist/tools/state.js +0 -170
- package/dist/tools/state.js.map +0 -1
- package/dist/tools/visualText.js +0 -61
- package/dist/tools/visualText.js.map +0 -1
package/dist/tools/flow.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
// qa_flow_check + qa_flow_run
|
|
1
|
+
// qa_flow_check + qa_flow_run: 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';
|
|
9
|
-
import { parseFlow } from '../flows/schema.js';
|
|
10
|
+
import { qaOk, qaError, qaStop, qaAnnotate, unknownSessionError } from '../lib/result.js';
|
|
11
|
+
import { FLOW_ENV_PREFIX, isMutatingFlowStep, lookupFlowVar, parseFlow, SECRET_VAR_NAME, } from '../flows/schema.js';
|
|
12
|
+
import { seedExactCommand } from '../flows/seedExec.js';
|
|
13
|
+
import { withinRootOrNull } from '../flows/paths.js';
|
|
14
|
+
import { makeRedactor } from '../lib/redact.js';
|
|
10
15
|
import { lintFlowObjectWithOptions } from '../flows/lint.js';
|
|
11
16
|
import { runFlow } from '../flows/run.js';
|
|
12
|
-
import { getDriver } from '../session/attach.js';
|
|
17
|
+
import { blockedDeviceResult, getDriver } from '../session/attach.js';
|
|
13
18
|
import { loadPolicy } from '../report/policy.js';
|
|
14
19
|
import { classifyFlakeResults } from '../report/flake.js';
|
|
15
20
|
import { validateCiMutationPolicy, validateCiVariables } from '../ci/preflight.js';
|
|
@@ -21,14 +26,15 @@ import { resolveMaskProvider, resolveVisualProvider } from '../visual/provider.j
|
|
|
21
26
|
import { backendCapabilities, backendForDriverKind } from '../automation/capabilities.js';
|
|
22
27
|
import { compileAutomationPlan } from '../automation/plan.js';
|
|
23
28
|
import { buildReadiness } from '../automation/report.js';
|
|
29
|
+
import { resolveProjectRoot } from '../context/projectRoot.js';
|
|
24
30
|
const ALL_BACKENDS = ['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest'];
|
|
25
31
|
const STATUS_ICON = { ready: '✅', candidate: '🟡', blocked: '⛔' };
|
|
26
32
|
/** Resolve a flow's YAML from explicit text, an absolute/relative path, or a name under .swipium/flows. */
|
|
27
|
-
function loadFlowSource(root, flow, flowYaml) {
|
|
33
|
+
export function loadFlowSource(root, flow, flowYaml) {
|
|
28
34
|
if (flowYaml && flowYaml.trim())
|
|
29
35
|
return { yamlText: flowYaml, source: 'inline' };
|
|
30
36
|
if (!flow)
|
|
31
|
-
return { error: 'Provide a flow name, a path, or flowYaml.' };
|
|
37
|
+
return { error: 'Provide a flow name, a path, or flowYaml.', errorCode: 'INVALID_ARGUMENT' };
|
|
32
38
|
const candidates = [];
|
|
33
39
|
if (isAbsolute(flow))
|
|
34
40
|
candidates.push(flow);
|
|
@@ -43,23 +49,52 @@ function loadFlowSource(root, flow, flowYaml) {
|
|
|
43
49
|
return { yamlText: readFileSync(p, 'utf8'), source: p };
|
|
44
50
|
}
|
|
45
51
|
catch (e) {
|
|
46
|
-
return { error: `Could not read ${p}: ${String(e)}
|
|
52
|
+
return { error: `Could not read ${p}: ${String(e)}`, errorCode: 'INVALID_FLOW' };
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
error: `Flow not found. Looked for: ${candidates.join(', ') || '(no project root; pass projectRoot or set SWIPIUM_PROJECT_ROOT)'}`,
|
|
58
|
+
errorCode: 'FLOW_NOT_FOUND',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The project root a flow tool reads from: explicit projectRoot arg > the session's root >
|
|
63
|
+
* resolveProjectRoot (MCP roots > SWIPIUM_PROJECT_ROOT/CLAUDE_PROJECT_DIR > cwd marker), like
|
|
64
|
+
* every other root-aware tool. Undefined only when nothing resolves (inline flowYaml still works).
|
|
65
|
+
*/
|
|
66
|
+
async function flowRoot(server, sessionRoot, projectRoot) {
|
|
67
|
+
if (!projectRoot && sessionRoot)
|
|
68
|
+
return sessionRoot;
|
|
69
|
+
const resolved = await resolveProjectRoot(server, projectRoot);
|
|
70
|
+
return resolved.root ?? sessionRoot;
|
|
71
|
+
}
|
|
72
|
+
function flowSourceError(src) {
|
|
73
|
+
return qaError({
|
|
74
|
+
what: src.error ?? 'Flow could not be loaded',
|
|
75
|
+
changedState: false,
|
|
76
|
+
retrySafe: true,
|
|
77
|
+
failureCode: src.errorCode ?? 'FLOW_NOT_FOUND',
|
|
78
|
+
nextSteps: [
|
|
79
|
+
src.errorCode === 'INVALID_ARGUMENT'
|
|
80
|
+
? 'Pass flow="<name>" (under .swipium/flows), a .yaml path, or inline flowYaml.'
|
|
81
|
+
: 'Pass an existing flow name/path or inline flowYaml; pass projectRoot (or set SWIPIUM_PROJECT_ROOT) if the flow lives in another project.',
|
|
82
|
+
],
|
|
83
|
+
});
|
|
51
84
|
}
|
|
52
|
-
const MUTATING_FLOW_STEPS = new Set(['networkOffline', 'networkOnline', 'seed', 'restartApp']);
|
|
53
|
-
const EXTERNAL_PROVIDER_FLOW_STEPS = new Set(['tapOcrText', 'assertOcrText']);
|
|
54
85
|
function stable(value) {
|
|
55
86
|
if (Array.isArray(value))
|
|
56
87
|
return value.map(stable);
|
|
57
88
|
if (value && typeof value === 'object')
|
|
58
|
-
return Object.fromEntries(Object.entries(value)
|
|
89
|
+
return Object.fromEntries(Object.entries(value)
|
|
90
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
91
|
+
.map(([k, v]) => [k, stable(v)]));
|
|
59
92
|
return value;
|
|
60
93
|
}
|
|
61
94
|
function fullHash(value) {
|
|
62
|
-
return createHash('sha256')
|
|
95
|
+
return createHash('sha256')
|
|
96
|
+
.update(JSON.stringify(stable(value)))
|
|
97
|
+
.digest('hex');
|
|
63
98
|
}
|
|
64
99
|
function hash(value) {
|
|
65
100
|
return fullHash(value).slice(0, 16);
|
|
@@ -67,15 +102,41 @@ function hash(value) {
|
|
|
67
102
|
function allFlowSteps(flow) {
|
|
68
103
|
return [...flow.setup, ...flow.steps, ...flow.teardown];
|
|
69
104
|
}
|
|
70
|
-
|
|
105
|
+
/** Where a seed's command came from, stated in consent: fixtures are loaded from the repo's
|
|
106
|
+
* .swipium/fixtures.json (or qa_start_session fixtures). Swipium never reviewed them. */
|
|
107
|
+
const SEED_ORIGIN = 'declared fixture (.swipium/fixtures.json or qa_start_session fixtures), repo-supplied, UNREVIEWED';
|
|
108
|
+
/** `${VAR}` URL preview for consent: the URL as it will be opened, with credential-like variable
|
|
109
|
+
* values and registered session secrets masked, plus where each variable is read from. */
|
|
110
|
+
function openUrlPreview(url, vars, secrets) {
|
|
111
|
+
const variables = [];
|
|
112
|
+
const resolved = url.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
113
|
+
const v = lookupFlowVar(name, vars);
|
|
114
|
+
variables.push({ name, source: v == null ? 'missing' : vars[name] != null ? 'variables' : 'env' });
|
|
115
|
+
if (v == null)
|
|
116
|
+
return `\${${name}}`;
|
|
117
|
+
return SECRET_VAR_NAME.test(name) ? `«${name}»` : v;
|
|
118
|
+
});
|
|
119
|
+
return { url, resolvedUrl: makeRedactor(secrets)(resolved) ?? resolved, variables };
|
|
120
|
+
}
|
|
121
|
+
function flowMutationAffects(flow, session, source, repeat, externalProviders = [], vars = {}) {
|
|
71
122
|
const mutations = allFlowSteps(flow)
|
|
72
123
|
.map((step, index) => {
|
|
73
|
-
if (!
|
|
124
|
+
if (!isMutatingFlowStep(step))
|
|
74
125
|
return null;
|
|
75
126
|
if (step.kind === 'seed') {
|
|
76
|
-
const
|
|
77
|
-
return {
|
|
127
|
+
const seed = session.fixtures.find((f) => f.name === step.fixture)?.seed;
|
|
128
|
+
return {
|
|
129
|
+
step: index + 1,
|
|
130
|
+
kind: step.kind,
|
|
131
|
+
fixture: step.fixture,
|
|
132
|
+
seedType: seed?.type ?? 'unknown',
|
|
133
|
+
// The EXACT argv / URL the seed will run. Consent must show what executes.
|
|
134
|
+
command: seed ? seedExactCommand(seed) : null,
|
|
135
|
+
origin: SEED_ORIGIN,
|
|
136
|
+
};
|
|
78
137
|
}
|
|
138
|
+
if (step.kind === 'openUrl')
|
|
139
|
+
return { step: index + 1, kind: step.kind, ...openUrlPreview(step.url, vars, session.secrets ?? []) };
|
|
79
140
|
return { step: index + 1, kind: step.kind };
|
|
80
141
|
})
|
|
81
142
|
.filter((x) => !!x);
|
|
@@ -89,10 +150,21 @@ function flowMutationAffects(flow, session, source, repeat, externalProviders =
|
|
|
89
150
|
externalProviders,
|
|
90
151
|
};
|
|
91
152
|
}
|
|
153
|
+
/** One line per mutating step for the consent's exactCommand (seeds show their argv/URL). */
|
|
154
|
+
function mutationCommandLines(affects) {
|
|
155
|
+
return affects.mutations.map((m) => {
|
|
156
|
+
if (m.kind === 'seed' && 'fixture' in m)
|
|
157
|
+
return `${m.step}:seed ${m.fixture} [${m.seedType}] > ${m.command ?? '(no seed spec, step will fail)'} (repo-supplied, unreviewed)`;
|
|
158
|
+
if (m.kind === 'openUrl' && 'resolvedUrl' in m)
|
|
159
|
+
return `${m.step}:openUrl ${m.resolvedUrl} (variables: ${m.variables.map((v) => `${v.name}←${v.source}`).join(', ')})`;
|
|
160
|
+
return `${m.step}:${m.kind}`;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
92
163
|
function flowMutationRisk(affects) {
|
|
93
164
|
if (affects.mutations.some((m) => m.kind === 'seed' && m.seedType === 'script'))
|
|
94
165
|
return 'high';
|
|
95
|
-
if (affects.externalProviders.length ||
|
|
166
|
+
if (affects.externalProviders.length ||
|
|
167
|
+
affects.mutations.some((m) => m.kind === 'networkOffline' || m.kind === 'networkOnline' || m.kind === 'seed' || m.kind === 'openUrl'))
|
|
96
168
|
return 'medium';
|
|
97
169
|
return 'low';
|
|
98
170
|
}
|
|
@@ -105,35 +177,58 @@ function ocrSteps(flow) {
|
|
|
105
177
|
});
|
|
106
178
|
return out;
|
|
107
179
|
}
|
|
180
|
+
const LOCATOR_FAILURES = new Set(['ELEMENT_NOT_FOUND', 'AMBIGUOUS_SELECTOR', 'STALE_REF', 'INVALID_SELECTOR', 'ASSERTION_FAILED']);
|
|
181
|
+
/** nextSteps for a failed qa_flow_run: point at qa_flow_repair for the failing step. */
|
|
182
|
+
export function failedFlowNextSteps(name, flowArg, failedAtStep, failureCode) {
|
|
183
|
+
const out = [];
|
|
184
|
+
// A missing variable is not locator drift: qa_flow_repair cannot fix it (and a cancelled run is
|
|
185
|
+
// not a failure to repair).
|
|
186
|
+
if (failedAtStep != null && failureCode !== 'MISSING_FIXTURE' && failureCode !== 'CANCELLED') {
|
|
187
|
+
const target = flowArg ? `flow:"${flowArg}"` : 'flowYaml:<same YAML>';
|
|
188
|
+
out.push(`${failureCode && !LOCATOR_FAILURES.has(failureCode) ? 'If this is locator drift (renamed/moved control), ' : ''}call qa_flow_repair { ${target}, failedStep:${failedAtStep} } for a reviewable locator fix (review before apply).`);
|
|
189
|
+
}
|
|
190
|
+
if (failureCode === 'MISSING_FIXTURE')
|
|
191
|
+
out.push(`Pass missing values via qa_flow_run { variables } (flows read process.env only for ${FLOW_ENV_PREFIX}* names).`);
|
|
192
|
+
out.push(`If the app itself is wrong, record it with qa_note { workflow:"${name}", outcome:"fail" } with the step evidence.`);
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
108
195
|
export function registerFlow(server, sessions) {
|
|
109
196
|
server.registerTool('qa_flow_check', {
|
|
110
197
|
title: 'Check a flow',
|
|
111
|
-
description: '
|
|
198
|
+
description: 'Statically validate a flow (.swipium/flows/*.yaml: name + steps) without running it: syntax/schema errors with the offending ' +
|
|
199
|
+
'step, plus warnings. Pass flow (name or path) or flowYaml. For an execution preview per backend use qa_flow_run mode:"plan".',
|
|
112
200
|
inputSchema: {
|
|
113
201
|
sessionId: z.string().optional(),
|
|
202
|
+
projectRoot: z.string().optional().describe('Absolute app root (default: session root > MCP roots > env > cwd).'),
|
|
114
203
|
flow: z.string().optional().describe('Flow name under .swipium/flows, or a path to a .yaml file.'),
|
|
115
204
|
flowYaml: z.string().optional().describe('Inline flow YAML (instead of a file).'),
|
|
116
|
-
platform: z.enum(['android', 'ios', 'cross-platform']).optional().describe('
|
|
117
|
-
ci: z.boolean().optional().describe('
|
|
205
|
+
platform: z.enum(['android', 'ios', 'cross-platform']).optional().describe('Authoring target for platform-aware warnings.'),
|
|
206
|
+
ci: z.boolean().optional().describe('Add CI preflight warnings (missing variables, mutating steps policy forbids).'),
|
|
118
207
|
},
|
|
119
|
-
}, async ({ sessionId, flow, flowYaml, platform, ci }) => {
|
|
120
|
-
const root = sessionId ? sessions.get(sessionId)?.root : undefined;
|
|
208
|
+
}, async ({ sessionId, projectRoot, flow, flowYaml, platform, ci }) => {
|
|
209
|
+
const root = await flowRoot(server, sessionId ? sessions.get(sessionId)?.root : undefined, projectRoot);
|
|
121
210
|
const src = loadFlowSource(root, flow, flowYaml);
|
|
122
211
|
if (src.error)
|
|
123
|
-
return
|
|
212
|
+
return flowSourceError(src);
|
|
124
213
|
const { flow: parsed, errors } = parseFlow(src.yamlText);
|
|
125
214
|
if (errors.length || !parsed) {
|
|
126
|
-
return qaError({
|
|
215
|
+
return qaError({
|
|
216
|
+
what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'})`,
|
|
217
|
+
changedState: false,
|
|
218
|
+
retrySafe: true,
|
|
219
|
+
failureCode: 'INVALID_FLOW',
|
|
220
|
+
nextSteps: ['Fix the listed errors and re-check.'],
|
|
221
|
+
}, { source: src.source, errors });
|
|
127
222
|
}
|
|
128
223
|
const allSteps = [...parsed.setup, ...parsed.steps, ...parsed.teardown];
|
|
129
224
|
const STRUCTURED_KINDS = new Set(['tap', 'assertVisible', 'assertNotVisible', 'scrollTo', 'waitForVisible', 'inputText']);
|
|
130
225
|
const warnings = [];
|
|
131
226
|
if (!parsed.appId)
|
|
132
|
-
warnings.push(
|
|
227
|
+
warnings.push("No appId, so a prepareTarget step will rely on the session's prepared appId.");
|
|
133
228
|
if (allSteps.some((s) => s.kind === 'tap' && s.selector.startsWith('@')))
|
|
134
|
-
warnings.push('Uses @ref selectors
|
|
229
|
+
warnings.push('Uses @ref selectors. Refs are run-time only; prefer text/id selectors for durable flows.');
|
|
135
230
|
if (src.yamlText.includes('${'))
|
|
136
|
-
warnings.push(
|
|
231
|
+
warnings.push(`Uses \${VARIABLES}. Provide them via qa_flow_run { variables }, stored session inputs, or ${FLOW_ENV_PREFIX}* environment variables (other env names are never read).`);
|
|
137
232
|
const flowOcrSteps = ocrSteps(parsed);
|
|
138
233
|
if (flowOcrSteps.length) {
|
|
139
234
|
warnings.push(`Uses OCR visual-provider steps (${flowOcrSteps.map((s) => `${s.step}:${s.kind}`).join(', ')}); qa_flow_run will require provider consent and a configured ocrCommand.`);
|
|
@@ -145,20 +240,27 @@ export function registerFlow(server, sessions) {
|
|
|
145
240
|
const inferredPlatform = platform ?? (driverKind === 'simulator' || driverKind === 'wda' ? 'ios' : driverKind === 'direct' ? 'android' : undefined);
|
|
146
241
|
const usesStructured = allSteps.some((s) => STRUCTURED_KINDS.has(s.kind));
|
|
147
242
|
if (driverKind === 'simulator' && parsed.mode === 'structured') {
|
|
148
|
-
warnings.push('mode:structured on the iOS simulator backend
|
|
243
|
+
warnings.push('mode:structured on the iOS simulator backend. tap/assertVisible/inputText need a UI tree (unavailable). Use mode: visual with image/visual steps.');
|
|
149
244
|
}
|
|
150
245
|
if (parsed.mode === 'visual' && usesStructured) {
|
|
151
|
-
warnings.push('mode:visual but the flow uses structured steps (tap/assertVisible/…)
|
|
246
|
+
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
247
|
}
|
|
153
248
|
for (const s of allSteps) {
|
|
154
|
-
const text = s.kind === 'tap'
|
|
249
|
+
const text = s.kind === 'tap'
|
|
250
|
+
? s.selector
|
|
251
|
+
: s.kind === 'inputText'
|
|
252
|
+
? (s.into ?? s.value)
|
|
253
|
+
: 'query' in s && typeof s.query === 'string'
|
|
254
|
+
? s.query
|
|
255
|
+
: '';
|
|
155
256
|
if (!text)
|
|
156
257
|
continue;
|
|
157
258
|
if (inferredPlatform === 'ios' && /resource-id=|uiautomator|android\./i.test(text))
|
|
158
259
|
warnings.push(`Android-specific locator in iOS flow: "${text}". Prefer accessibilityIdentifier/name/label on iOS.`);
|
|
159
260
|
if (inferredPlatform === 'android' && /class chain|predicate string|XCUIElementType|accessibility id=/i.test(text))
|
|
160
261
|
warnings.push(`iOS-specific locator in Android flow: "${text}". Prefer resource-id/content-desc/text on Android.`);
|
|
161
|
-
if (inferredPlatform === 'cross-platform' &&
|
|
262
|
+
if (inferredPlatform === 'cross-platform' &&
|
|
263
|
+
/(resource-id=|uiautomator|android\.|accessibility id=|class chain|predicate string|XCUIElementType|xpath=|^\/\/)/i.test(text)) {
|
|
162
264
|
warnings.push(`Platform-specific or brittle locator in cross-platform flow: "${text}". Prefer shared text/testID/accessibility labels.`);
|
|
163
265
|
}
|
|
164
266
|
}
|
|
@@ -171,7 +273,10 @@ export function registerFlow(server, sessions) {
|
|
|
171
273
|
for (const v of missingVars)
|
|
172
274
|
warnings.push(`CI variable: ${v.variable} is required by ${v.flow} step ${v.step} but is not set in the environment.`);
|
|
173
275
|
}
|
|
174
|
-
const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, {
|
|
276
|
+
const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, {
|
|
277
|
+
platform,
|
|
278
|
+
policy: ci ? (root ? loadPolicy(root) : null) : undefined,
|
|
279
|
+
});
|
|
175
280
|
for (const f of lintFindings) {
|
|
176
281
|
const prefix = f.severity === 'error' ? 'Lint error' : 'Lint warning';
|
|
177
282
|
warnings.push(`${prefix} ${f.code}${f.step ? ` step ${f.step}` : ''}: ${f.message}`);
|
|
@@ -179,23 +284,57 @@ export function registerFlow(server, sessions) {
|
|
|
179
284
|
// Image-template steps: verify the referenced files exist (best-effort, when a root is known).
|
|
180
285
|
if (root) {
|
|
181
286
|
for (const s of allSteps) {
|
|
182
|
-
if (
|
|
183
|
-
const p =
|
|
184
|
-
if (!
|
|
287
|
+
if (s.kind === 'tapImage' || s.kind === 'assertImage') {
|
|
288
|
+
const p = withinRootOrNull(root, s.template);
|
|
289
|
+
if (!p)
|
|
290
|
+
warnings.push(`image template outside the project root: ${s.template}; qa_flow_run will refuse it.`);
|
|
291
|
+
else if (!existsSync(p))
|
|
185
292
|
warnings.push(`image template not found: ${s.template} (resolve relative to the project root).`);
|
|
186
293
|
}
|
|
187
294
|
}
|
|
188
295
|
}
|
|
189
|
-
return qaOk({
|
|
296
|
+
return qaOk({
|
|
297
|
+
valid: true,
|
|
298
|
+
source: src.source,
|
|
299
|
+
name: parsed.name,
|
|
300
|
+
appId: parsed.appId ?? null,
|
|
301
|
+
mode: parsed.mode,
|
|
302
|
+
budgetProfile: parsed.budgetProfile ?? null,
|
|
303
|
+
stepCount: parsed.steps.length,
|
|
304
|
+
setupCount: parsed.setup.length,
|
|
305
|
+
teardownCount: parsed.teardown.length,
|
|
306
|
+
fixtures: parsed.fixtures,
|
|
307
|
+
warnings,
|
|
308
|
+
lintFindings,
|
|
309
|
+
}, `✅ 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
310
|
});
|
|
191
|
-
server.registerTool('
|
|
192
|
-
title: '
|
|
193
|
-
description: '
|
|
311
|
+
server.registerTool('qa_flow_run', {
|
|
312
|
+
title: 'Run a flow (or preview its execution plan)',
|
|
313
|
+
description: 'Run a flow on the prepared app (mode:"run", default) or preview it without a device (mode:"plan"). run executes steps ' +
|
|
314
|
+
'server-side with setup/teardown, fail-fast, no auto-retry of mutating steps (consent-gated, like OCR steps); a failure ' +
|
|
315
|
+
'returns the step, screenshot, failureCode, and health. plan reports, per backend (Android, iOS simulator, iOS WDA, ' +
|
|
316
|
+
'Appium), whether each step is native / fallback / visual_only / unsupported. Pass flow (name/path) or flowYaml, plus ' +
|
|
317
|
+
'variables for ${VAR}. Steps: docs/tools.md#flow-steps.',
|
|
194
318
|
inputSchema: {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
319
|
+
mode: z.enum(['plan', 'run']).optional(),
|
|
320
|
+
sessionId: z.string().optional().describe('Required for run; plan: resolves names + marks the attached backend.'),
|
|
321
|
+
projectRoot: z
|
|
322
|
+
.string()
|
|
323
|
+
.optional()
|
|
324
|
+
.describe('plan: absolute app root for flow names (default: session root > MCP roots > env > cwd).'),
|
|
325
|
+
flow: z.string().optional().describe('Flow name under .swipium/flows, or a .yaml path.'),
|
|
326
|
+
flowYaml: z.string().optional().describe('Inline flow YAML.'),
|
|
327
|
+
variables: z
|
|
328
|
+
.record(z.string())
|
|
329
|
+
.optional()
|
|
330
|
+
.describe('run: ${VAR} values (win over stored session inputs, then SWIPIUM_* env only); credential-like names redacted.'),
|
|
331
|
+
repeat: z.number().int().min(1).max(10).optional().describe('run: repeat N times for flake classification (default 1).'),
|
|
332
|
+
consentId: z.string().optional().describe('run: consent for mutating / OCR steps.'),
|
|
333
|
+
approve: z.boolean().optional(),
|
|
334
|
+
backend: z
|
|
335
|
+
.enum(['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest'])
|
|
336
|
+
.optional()
|
|
337
|
+
.describe('plan: only this backend (default all five).'),
|
|
199
338
|
appium: z
|
|
200
339
|
.object({
|
|
201
340
|
automationName: z.string().optional(),
|
|
@@ -203,113 +342,153 @@ export function registerFlow(server, sessions) {
|
|
|
203
342
|
webviewContextsAvailable: z.boolean().optional(),
|
|
204
343
|
})
|
|
205
344
|
.optional()
|
|
206
|
-
.describe('Appium session hints
|
|
345
|
+
.describe('plan: Appium session hints.'),
|
|
207
346
|
},
|
|
208
|
-
}, async ({ sessionId, flow, flowYaml, backend, appium }) => {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
347
|
+
}, async ({ mode, sessionId, projectRoot, flow, flowYaml, variables, repeat, consentId, approve, backend, appium }) => {
|
|
348
|
+
const effectiveMode = mode ?? 'run';
|
|
349
|
+
const notes = [];
|
|
350
|
+
// ---- mode:"plan": read-only execution preview (merged twin, 1.5.0). ----
|
|
351
|
+
if (effectiveMode === 'plan') {
|
|
352
|
+
const ignored = [
|
|
353
|
+
variables !== undefined && 'variables',
|
|
354
|
+
repeat !== undefined && 'repeat',
|
|
355
|
+
consentId !== undefined && 'consentId',
|
|
356
|
+
approve !== undefined && 'approve',
|
|
357
|
+
].filter((x) => !!x);
|
|
358
|
+
if (ignored.length)
|
|
359
|
+
notes.push(`ignored parameter(s) not applicable to mode:"plan": ${ignored.join(', ')}; re-run with mode:"run" to execute`);
|
|
360
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
361
|
+
const root = await flowRoot(server, session?.root, projectRoot);
|
|
362
|
+
const src = loadFlowSource(root, flow, flowYaml);
|
|
363
|
+
if (src.error)
|
|
364
|
+
return qaAnnotate(flowSourceError(src), notes);
|
|
365
|
+
const { flow: parsed, errors } = parseFlow(src.yamlText);
|
|
366
|
+
if (errors.length || !parsed) {
|
|
367
|
+
return qaAnnotate(qaError({
|
|
368
|
+
what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}), run qa_flow_check`,
|
|
369
|
+
changedState: false,
|
|
370
|
+
retrySafe: true,
|
|
371
|
+
failureCode: 'INVALID_FLOW',
|
|
372
|
+
nextSteps: ['Fix the listed errors and re-plan.'],
|
|
373
|
+
}, { source: src.source, errors }), notes);
|
|
374
|
+
}
|
|
375
|
+
const appiumHints = appium ? { ...appium } : undefined;
|
|
376
|
+
const attachedBackend = session?.driver?.kind ? backendForDriverKind(session.driver.kind, appiumHints) : undefined;
|
|
377
|
+
// Which backends to plan for: an explicit one, else all concrete backends (so the answer covers
|
|
378
|
+
// Android + iOS even with no device attached: the pre-run "can this run?" product question).
|
|
379
|
+
const targets = backend ? [backend] : [...ALL_BACKENDS];
|
|
380
|
+
if (attachedBackend && attachedBackend !== 'unknown' && !targets.includes(attachedBackend))
|
|
381
|
+
targets.unshift(attachedBackend);
|
|
382
|
+
const plans = targets.map((b) => {
|
|
383
|
+
const caps = backendCapabilities(b, appiumHints);
|
|
384
|
+
const plan = compileAutomationPlan(parsed, caps);
|
|
385
|
+
const readiness = buildReadiness(plan);
|
|
386
|
+
return {
|
|
387
|
+
backend: b,
|
|
388
|
+
attached: b === attachedBackend,
|
|
389
|
+
status: readiness.status,
|
|
390
|
+
headline: readiness.headline,
|
|
391
|
+
executable: plan.executable,
|
|
392
|
+
supportedSteps: readiness.supportedSteps,
|
|
393
|
+
unsupportedSteps: readiness.unsupportedSteps,
|
|
394
|
+
fallbackUsed: readiness.fallbackUsed,
|
|
395
|
+
agentMessage: readiness.agentMessage,
|
|
396
|
+
developerMessage: readiness.developerMessage,
|
|
397
|
+
blockers: readiness.blockers,
|
|
398
|
+
warnings: readiness.warnings,
|
|
399
|
+
steps: plan.steps.map((s) => ({
|
|
400
|
+
index: s.index,
|
|
401
|
+
kind: s.action.kind,
|
|
402
|
+
target: s.action.note,
|
|
403
|
+
support: s.support,
|
|
404
|
+
reason: s.reason,
|
|
405
|
+
requiredCapability: s.requiredCapability,
|
|
406
|
+
})),
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
const summaryLines = plans.map((p) => {
|
|
410
|
+
const head = `${STATUS_ICON[p.status] ?? '•'} ${p.backend}${p.attached ? ' (attached)' : ''}: ${p.status}, ${p.supportedSteps} supported, ${p.unsupportedSteps} unsupported`;
|
|
411
|
+
return `${head}\n ${p.agentMessage}`;
|
|
412
|
+
});
|
|
413
|
+
const summary = `flow "${parsed.name}": automation plan across ${plans.length} backend(s) (mode:"plan", read-only, nothing executed):\n${summaryLines.join('\n')}`;
|
|
414
|
+
return qaAnnotate(qaOk({ flow: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, source: src.source, plans }, summary), notes);
|
|
217
415
|
}
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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);
|
|
266
|
-
const { driver } = session ? await getDriver(session) : { driver: undefined };
|
|
416
|
+
// ---- mode:"run": execute on the prepared device. ----
|
|
417
|
+
const runIgnored = [
|
|
418
|
+
backend !== undefined && 'backend',
|
|
419
|
+
appium !== undefined && 'appium',
|
|
420
|
+
projectRoot !== undefined && 'projectRoot',
|
|
421
|
+
].filter((x) => !!x);
|
|
422
|
+
if (runIgnored.length)
|
|
423
|
+
notes.push(`ignored parameter(s) not applicable to mode:"run": ${runIgnored.join(', ')}; they refine the mode:"plan" preview`);
|
|
424
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
425
|
+
if (sessionId && !session)
|
|
426
|
+
return qaAnnotate(unknownSessionError(sessionId), notes);
|
|
427
|
+
const { driver, blocked } = session ? await getDriver(session) : { driver: undefined, blocked: undefined };
|
|
267
428
|
if (!session || !driver) {
|
|
268
|
-
|
|
429
|
+
const refused = blockedDeviceResult(blocked);
|
|
430
|
+
if (refused)
|
|
431
|
+
return qaAnnotate(refused, notes);
|
|
432
|
+
return qaAnnotate(qaError({
|
|
433
|
+
what: sessionId
|
|
434
|
+
? 'No device attached to this session'
|
|
435
|
+
: 'qa_flow_run mode:"run" needs a sessionId with a prepared device (mode:"plan" works without one)',
|
|
436
|
+
changedState: false,
|
|
437
|
+
retrySafe: true,
|
|
438
|
+
nextSteps: ['Call qa_prepare_target / qa_ios boot first, then qa_flow_run.'],
|
|
439
|
+
}), notes);
|
|
269
440
|
}
|
|
270
441
|
const src = loadFlowSource(session.root, flow, flowYaml);
|
|
271
442
|
if (src.error)
|
|
272
|
-
return
|
|
443
|
+
return qaAnnotate(flowSourceError(src), notes);
|
|
273
444
|
const { flow: parsed, errors } = parseFlow(src.yamlText);
|
|
274
445
|
if (errors.length || !parsed) {
|
|
275
|
-
return qaError({
|
|
446
|
+
return qaAnnotate(qaError({
|
|
447
|
+
what: `Flow is invalid. Run qa_flow_check first`,
|
|
448
|
+
changedState: false,
|
|
449
|
+
retrySafe: true,
|
|
450
|
+
failureCode: 'INVALID_FLOW',
|
|
451
|
+
nextSteps: ['Fix the flow and re-run.'],
|
|
452
|
+
}, { errors }), notes);
|
|
276
453
|
}
|
|
277
|
-
// Backend/mode gate (
|
|
454
|
+
// Backend/mode gate (catch unsupported combos before running). A structured-mode
|
|
278
455
|
// flow needs a UI tree, which the iOS simulator and a visual-fallback session don't have.
|
|
279
456
|
if (parsed.mode === 'structured' && (driver.kind === 'simulator' || session.mode === 'visual-fallback')) {
|
|
280
|
-
return qaError({
|
|
457
|
+
return qaAnnotate(qaError({
|
|
281
458
|
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
459
|
changedState: false,
|
|
283
460
|
retrySafe: false,
|
|
284
461
|
failureCode: 'BACKEND_UNSUPPORTED',
|
|
285
|
-
nextSteps: [
|
|
286
|
-
|
|
462
|
+
nextSteps: [
|
|
463
|
+
'Author the flow with `mode: visual` (or auto) and use image/visual steps (tapImage/assertImage/assertDiff/assertVisual).',
|
|
464
|
+
],
|
|
465
|
+
}), notes);
|
|
287
466
|
}
|
|
288
467
|
const budget = sessions.budgetStop(session);
|
|
289
468
|
if (budget)
|
|
290
|
-
return qaStop(budget, { counters: session.counters });
|
|
469
|
+
return qaAnnotate(qaStop(budget, { counters: session.counters }), notes);
|
|
291
470
|
const runs = repeat ?? 1;
|
|
292
471
|
const externalProviderSteps = ocrSteps(parsed);
|
|
293
472
|
const externalProviders = [];
|
|
294
473
|
if (externalProviderSteps.length) {
|
|
295
474
|
if (session.sensitive) {
|
|
296
|
-
return qaError({
|
|
475
|
+
return qaAnnotate(qaError({
|
|
297
476
|
what: 'Sensitive mode refuses OCR flow steps because they would pass screenshots to an external provider',
|
|
298
477
|
changedState: false,
|
|
299
478
|
retrySafe: false,
|
|
300
479
|
failureCode: 'UNSAFE_ACTION_REFUSED',
|
|
301
480
|
nextSteps: ['Disable sensitive mode only for a safe, masked QA screen, or remove tapOcrText/assertOcrText from the flow.'],
|
|
302
|
-
});
|
|
481
|
+
}), notes);
|
|
303
482
|
}
|
|
304
483
|
const command = configuredOcrCommand(session.root);
|
|
305
484
|
if (!command) {
|
|
306
|
-
return qaError({
|
|
485
|
+
return qaAnnotate(qaError({
|
|
307
486
|
what: 'Flow uses OCR text steps, but OCR is not configured',
|
|
308
487
|
changedState: false,
|
|
309
488
|
retrySafe: true,
|
|
310
489
|
failureCode: 'VISUAL_ONLY_SCREEN',
|
|
311
490
|
nextSteps: ['Configure ocrCommand in .swipium/config.json or remove tapOcrText/assertOcrText from the flow.'],
|
|
312
|
-
});
|
|
491
|
+
}), notes);
|
|
313
492
|
}
|
|
314
493
|
try {
|
|
315
494
|
const preview = resolveVisualProvider(command, { image: '<screenshot>' }, 30000);
|
|
@@ -325,18 +504,31 @@ export function registerFlow(server, sessions) {
|
|
|
325
504
|
});
|
|
326
505
|
}
|
|
327
506
|
catch (e) {
|
|
328
|
-
return qaError({
|
|
507
|
+
return qaAnnotate(qaError({
|
|
329
508
|
what: e instanceof GitScopeForbiddenError ? e.message : `Invalid OCR command template: ${String(e)}`,
|
|
330
509
|
changedState: false,
|
|
331
510
|
retrySafe: !(e instanceof GitScopeForbiddenError),
|
|
332
511
|
failureCode: e instanceof GitScopeForbiddenError ? 'GIT_SCOPE_FORBIDDEN' : 'INVALID_FLOW',
|
|
333
|
-
nextSteps: e instanceof GitScopeForbiddenError
|
|
334
|
-
|
|
512
|
+
nextSteps: e instanceof GitScopeForbiddenError
|
|
513
|
+
? ['Run Git yourself outside Swipium; configure ocrCommand to use a non-Git executable.']
|
|
514
|
+
: ['Use an argv array in .swipium/config.json, e.g. ["node","ocr.js","{image}"].'],
|
|
515
|
+
}), notes);
|
|
335
516
|
}
|
|
336
517
|
}
|
|
337
|
-
|
|
518
|
+
// Stored session inputs (qa_agent needs_input / first-run credentials) fill ${VAR}s so a
|
|
519
|
+
// generated flow replays without the agent re-sending raw secrets; explicit args win.
|
|
520
|
+
const sessionInputs = sessions.inputVariables(session);
|
|
521
|
+
const runVariables = { ...sessionInputs, ...(variables ?? {}) };
|
|
522
|
+
const usedSessionInputs = Object.keys(sessionInputs).filter((k) => variables?.[k] == null);
|
|
523
|
+
if (usedSessionInputs.length)
|
|
524
|
+
notes.push(`using stored session input(s): ${usedSessionInputs.join(', ')} (values not shown)`);
|
|
525
|
+
const affects = flowMutationAffects(parsed, session, src.source, runs, externalProviders, runVariables);
|
|
338
526
|
const mutationRisk = flowMutationRisk(affects);
|
|
339
|
-
let mutationConsent = {
|
|
527
|
+
let mutationConsent = {
|
|
528
|
+
required: false,
|
|
529
|
+
approved: true,
|
|
530
|
+
payloadHash: affects.flowHash,
|
|
531
|
+
};
|
|
340
532
|
const privileged = affects.mutations.length > 0 || affects.externalProviders.length > 0;
|
|
341
533
|
if (privileged) {
|
|
342
534
|
const payloadHash = fullHash(affects);
|
|
@@ -353,15 +545,21 @@ export function registerFlow(server, sessions) {
|
|
|
353
545
|
const externalCommand = affects.externalProviders.length
|
|
354
546
|
? `; external provider: ${displayArgv(affects.externalProviders[0].argv ?? [])}`
|
|
355
547
|
: '';
|
|
356
|
-
return requireConsent({
|
|
548
|
+
return qaAnnotate(requireConsent({
|
|
357
549
|
action: 'flow_mutation_run',
|
|
358
550
|
risk: mutationRisk,
|
|
359
|
-
exactCommand: `qa_flow_run ${parsed.name} (${[...affects.mutations.map((m) => `${m.step}:${m.kind}`), ...externalProviderSteps.map((s) => `${s.step}:${s.kind}`)].join(', ')})${externalCommand}`,
|
|
551
|
+
exactCommand: `qa_flow_run ${parsed.name} (${[...affects.mutations.map((m) => `${m.step}:${m.kind}`), ...externalProviderSteps.map((s) => `${s.step}:${s.kind}`)].join(', ')})${externalCommand}${mutationCommandLines(affects).length ? `\n${mutationCommandLines(affects).join('\n')}` : ''}`,
|
|
360
552
|
affects,
|
|
361
|
-
explain: affects.externalProviders.length
|
|
553
|
+
explain: (affects.externalProviders.length
|
|
362
554
|
? `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
|
-
: `Run mutating flow "${parsed.name}"? Mutating steps can change app/device/test state and will be recorded in the mutation ledger
|
|
364
|
-
|
|
555
|
+
: `Run mutating flow "${parsed.name}"? Mutating steps can change app/device/test state and will be recorded in the mutation ledger.`) +
|
|
556
|
+
(affects.mutations.some((m) => m.kind === 'seed')
|
|
557
|
+
? " Seed steps run the exact command/URL shown, taken from the repo's fixture declarations. These are repo-supplied and UNREVIEWED by Swipium; approve only if you trust them."
|
|
558
|
+
: '') +
|
|
559
|
+
(affects.mutations.some((m) => m.kind === 'openUrl')
|
|
560
|
+
? ' openUrl steps interpolate variables into a URL that leaves the test harness, so check the destination.'
|
|
561
|
+
: ''),
|
|
562
|
+
}), notes);
|
|
365
563
|
}
|
|
366
564
|
mutationConsent = { required: true, consentId, approved: true, payloadHash };
|
|
367
565
|
sessions.recordMutation(session, {
|
|
@@ -380,18 +578,35 @@ export function registerFlow(server, sessions) {
|
|
|
380
578
|
const b = sessions.budgetStop(session);
|
|
381
579
|
if (b)
|
|
382
580
|
break;
|
|
383
|
-
results.push(await runFlow(sessions, session, driver, parsed, { variables, mutationConsent }));
|
|
581
|
+
results.push(await runFlow(sessions, session, driver, parsed, { variables: runVariables, mutationConsent }));
|
|
384
582
|
}
|
|
385
583
|
const flake = classifyFlakeResults(results, runs);
|
|
386
|
-
return qaOk({
|
|
584
|
+
return qaAnnotate(qaOk({
|
|
585
|
+
flow: parsed.name,
|
|
586
|
+
runs: results.length,
|
|
587
|
+
passes: flake.passed,
|
|
588
|
+
fails: flake.failed,
|
|
589
|
+
passRate: flake.passRate,
|
|
590
|
+
classification: flake.classification,
|
|
591
|
+
triage: flake.triage,
|
|
592
|
+
results: results.map((r) => ({
|
|
593
|
+
passed: r.passed,
|
|
594
|
+
failedAtStep: r.failedAtStep,
|
|
595
|
+
reason: r.reason,
|
|
596
|
+
failureCode: r.failureCode,
|
|
597
|
+
})),
|
|
598
|
+
}, `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
599
|
}
|
|
388
|
-
const result = await runFlow(sessions, session, driver, parsed, { variables, mutationConsent });
|
|
600
|
+
const result = await runFlow(sessions, session, driver, parsed, { variables: runVariables, mutationConsent });
|
|
389
601
|
const head = `flow "${result.name}" ${result.passed ? '✅ PASSED' : `❌ FAILED at step ${result.failedAtStep} (${result.reason})`} ` +
|
|
390
|
-
|
|
602
|
+
`: ${result.steps.filter((s) => s.ok).length}/${result.steps.length} steps in ${Math.round(result.durationMs / 100) / 10}s` +
|
|
391
603
|
(result.appHealth ? `\nhealth: native=${result.nativeHealth} app=${result.appHealth}` : '');
|
|
392
|
-
const stepLines = result.steps
|
|
393
|
-
|
|
394
|
-
|
|
604
|
+
const stepLines = result.steps
|
|
605
|
+
.map((s) => `${s.ok ? '✓' : '✗'} ${s.index}. ${s.summary}${s.detail ? `: ${s.detail}` : ''}${s.screenshotUri ? `\n evidence: ${s.screenshotUri}` : ''}`)
|
|
606
|
+
.join('\n');
|
|
607
|
+
// A failed flow is a structured result, not a protocol error. The agent should record it.
|
|
608
|
+
const nextSteps = result.passed ? undefined : failedFlowNextSteps(parsed.name, flow, result.failedAtStep, result.failureCode);
|
|
609
|
+
return qaAnnotate(qaOk({ ...result, ...(nextSteps ? { nextSteps } : {}) }, `${head}\n${stepLines}${nextSteps ? `\nnext:\n - ${nextSteps.join('\n - ')}` : ''}`), notes);
|
|
395
610
|
});
|
|
396
611
|
}
|
|
397
612
|
//# sourceMappingURL=flow.js.map
|