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/agent.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// Agent-efficiency layer. Compact, deterministic helpers so an MCP client spends fewer turns:
|
|
2
|
-
// a one-glance
|
|
2
|
+
// the server instructions, a one-glance status (orientation without a session; state + next best
|
|
3
|
+
// action with one), a blocker explainer, and a resume-from-blocker entry that consumes
|
|
3
4
|
// user-provided input safely.
|
|
5
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
6
|
+
import { isAbsolute, resolve as resolvePath, sep } from 'node:path';
|
|
4
7
|
import { z } from 'zod';
|
|
5
|
-
import { qaOk, qaError } from '../lib/result.js';
|
|
8
|
+
import { qaOk, qaError, unknownSessionError } from '../lib/result.js';
|
|
6
9
|
import { FAILURES, failureOwner, isSelfFixable } from '../oracle/failures.js';
|
|
7
10
|
import { progressLine } from '../session/progress.js';
|
|
8
11
|
import { readinessForSession } from '../report/readiness.js';
|
|
9
12
|
import { SWIPIUM_VERSION, TOOL_COUNT, PROMPT_COUNT } from '../version.js';
|
|
10
13
|
import { TEST_GOALS } from '../orchestration/goal.js';
|
|
14
|
+
import { CAPABILITY_GROUPS } from '../core/capabilityGroups.js';
|
|
15
|
+
import { recallTestThisIntent, markLoginDeclined } from '../orchestration/testThis/sessionIntent.js';
|
|
16
|
+
import { SECRET_VAR_NAME } from '../flows/schema.js';
|
|
11
17
|
function budgetRemaining(s) {
|
|
12
18
|
const elapsedMin = (Date.now() - s.createdAt) / 60000;
|
|
13
19
|
return {
|
|
@@ -16,127 +22,333 @@ function budgetRemaining(s) {
|
|
|
16
22
|
screenshots: Math.max(0, s.budget.maxScreenshots - s.counters.screenshots),
|
|
17
23
|
};
|
|
18
24
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
25
|
+
/** Has this session already produced a durable generated asset (suite/flow/POM/test cases)?
|
|
26
|
+
* Both generation paths leave a workaround-trail entry on success: the qa_test_this pipeline
|
|
27
|
+
* records "generated a POM suite (…)" and qa_generate records "generated <target> test asset(s)
|
|
28
|
+
* from recorded actions (qa_generate)". */
|
|
29
|
+
function hasGeneratedAssets(s) {
|
|
30
|
+
return s.workarounds.some((w) => /^generated .*(suite|flow|pom|test)/i.test(w));
|
|
31
|
+
}
|
|
32
|
+
/** The mode qa_status reports (JSON and text alike). A WDA-less iOS simulator (SimctlDriver, kind
|
|
33
|
+
* 'simulator') is visual-only regardless of the stored mode, which only flips on a failed UI dump. */
|
|
34
|
+
export function effectiveMode(s) {
|
|
35
|
+
// After a restart there is no live driver, but the persisted driverKind still tells the transport.
|
|
36
|
+
return (s.driver?.kind ?? s.driverKind) === 'simulator' ? 'visual-only' : s.mode;
|
|
37
|
+
}
|
|
38
|
+
const SIMULATOR_UDID_RE = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
39
|
+
/** Which platform the session's bound device is on. The live driver kind is authoritative
|
|
40
|
+
* (simctl/WDA ⇒ iOS); a rehydrated session has no driver, so fall back to the device id shape:
|
|
41
|
+
* iOS simulators are UUIDs, adb serials never are. */
|
|
42
|
+
export function sessionPlatform(s) {
|
|
43
|
+
const kind = s.driver?.kind ?? s.driverKind;
|
|
44
|
+
if (kind === 'simulator' || kind === 'wda')
|
|
45
|
+
return 'ios';
|
|
46
|
+
if (kind === 'direct')
|
|
47
|
+
return 'android';
|
|
48
|
+
if (!s.device)
|
|
49
|
+
return undefined;
|
|
50
|
+
return SIMULATOR_UDID_RE.test(s.device) ? 'ios' : 'android';
|
|
51
|
+
}
|
|
52
|
+
/** Has a smoke pass run in this session? Persisted signals only: the qa_test_this pipeline's
|
|
53
|
+
* `smoke_completed` milestone, or the launch_smoke note every smoke run records (qa_smoke too). */
|
|
54
|
+
function smokeRan(s) {
|
|
55
|
+
return s.milestones?.smoke_completed != null || s.notes.some((n) => n.workflow === 'launch_smoke');
|
|
56
|
+
}
|
|
57
|
+
/** Latest time anything was exercised/observed (notes, findings, recorded actions). */
|
|
58
|
+
function lastActivityAt(s) {
|
|
59
|
+
let t = 0;
|
|
60
|
+
for (const n of s.notes)
|
|
61
|
+
t = Math.max(t, n.at ?? 0);
|
|
62
|
+
for (const f of s.findings)
|
|
63
|
+
t = Math.max(t, f.at ?? 0);
|
|
64
|
+
for (const a of s.recordedActions)
|
|
65
|
+
t = Math.max(t, a.at ?? 0);
|
|
66
|
+
return t;
|
|
67
|
+
}
|
|
68
|
+
/** Milestone keys stamped by the agent-layer tools so qa_status can tell a terminal job's
|
|
69
|
+
* recommended action was carried out (each overwritten with the latest time). */
|
|
70
|
+
export const BLOCKER_ANSWERED_MILESTONE = 'blocker_answered';
|
|
71
|
+
export const BLOCKER_EXPLAINED_MILESTONE = 'blocker_explained';
|
|
72
|
+
/** Latest time the user answered a needs_input question: a stored input or a
|
|
73
|
+
* qa_continue_from_blocker call (choices-only answers store no input). */
|
|
74
|
+
function lastAnsweredAt(s) {
|
|
75
|
+
let t = s.milestones?.[BLOCKER_ANSWERED_MILESTONE] ?? 0;
|
|
76
|
+
for (const i of s.inputs ?? [])
|
|
77
|
+
t = Math.max(t, i.at ?? 0);
|
|
78
|
+
return t;
|
|
79
|
+
}
|
|
80
|
+
/** The newest report artifact, if one exists. */
|
|
81
|
+
function latestReport(s) {
|
|
82
|
+
return s.artifacts.filter((a) => a.kind === 'report' && /\/report\/report-/.test(a.uri)).sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
83
|
+
}
|
|
84
|
+
/** Deterministic "what next" given the session's observed state (optionally goal-aware).
|
|
85
|
+
* An explicit state ladder, evaluated top-down, the first matching state wins. Every rung keys
|
|
86
|
+
* on state its recommended tool CHANGES, so following the advice always advances the ladder:
|
|
87
|
+
* 1. a job is running > qa_job_status (poll it)
|
|
88
|
+
* 2. the last qa_test_this job ended > its nextRecommendedAction (report / explain blocker /
|
|
89
|
+
* and nothing happened since answer the needs_input question), but only until that
|
|
90
|
+
* action is observably done: a report newer than the job,
|
|
91
|
+
* an answer (stored input / resume call) newer than the job
|
|
92
|
+
* (> re-run the autopilot with it), or a qa_explain_blocker
|
|
93
|
+
* {sessionId} call newer than the job. Replaying a done
|
|
94
|
+
* action would loop, so the ladder moves on.
|
|
95
|
+
* 3. no device bound > qa_test_this (orchestrate setup)
|
|
96
|
+
* 4. device but no app > qa_prepare_target (Android) / qa_prepare_ios_target (iOS sim)
|
|
97
|
+
* 5. no smoke yet and nothing recorded > qa_smoke (records the smoke milestone)
|
|
98
|
+
* 6. findings, no report since > qa_report
|
|
99
|
+
* 7. clean run, actions, no assets yet > qa_generate (make the run durable)
|
|
100
|
+
* 8. no report since the last activity > qa_report (wrap up)
|
|
101
|
+
* 9. a fresh report exists > qa_get_artifact (read it, done; TERMINAL: the only
|
|
102
|
+
* rung whose advice changes nothing, by design)
|
|
103
|
+
* Exported for unit tests (test/nextBestAction.test.ts, test/orchStatusLadder.test.ts). */
|
|
104
|
+
export function nextBestAction(s, goal) {
|
|
21
105
|
const sid = s.id;
|
|
106
|
+
// 1. A job is still running: poll it before anything else.
|
|
22
107
|
const lastJob = [...s.jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
23
108
|
if (lastJob?.status === 'running')
|
|
24
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
tool: 'qa_job_status',
|
|
111
|
+
why: `job ${lastJob.jobId} (${lastJob.kind}) is still running`,
|
|
112
|
+
args: { sessionId: sid, jobId: lastJob.jobId },
|
|
113
|
+
};
|
|
114
|
+
// 2. The last autopilot job is terminal and nothing was exercised after it: its own
|
|
115
|
+
// nextRecommendedAction is authoritative (a finished run must not be re-smoked).
|
|
116
|
+
const result = lastJob?.result;
|
|
117
|
+
const jobEnd = lastJob ? (lastJob.endedAt ?? lastJob.startedAt) : 0;
|
|
118
|
+
if (lastJob && result?.state && jobEnd >= lastActivityAt(s)) {
|
|
119
|
+
const nra = result.nextRecommendedAction;
|
|
120
|
+
const blocked = result.state === 'blocked' || result.state === 'unsafe';
|
|
121
|
+
const explained = (s.milestones?.[BLOCKER_EXPLAINED_MILESTONE] ?? 0) > jobEnd;
|
|
122
|
+
const answered = result.state === 'needs_input' && lastAnsweredAt(s) > jobEnd;
|
|
123
|
+
const reportedSince = (latestReport(s)?.createdAt ?? 0) > jobEnd;
|
|
124
|
+
if (blocked && !explained)
|
|
125
|
+
return {
|
|
126
|
+
tool: 'qa_explain_blocker',
|
|
127
|
+
why: `last job ${lastJob.jobId} ended ${result.state} (${result.failureCode ?? 'UNKNOWN'}): explain the blocker and relay the fix`,
|
|
128
|
+
args: { failureCode: result.failureCode ?? 'UNKNOWN', sessionId: sid },
|
|
129
|
+
};
|
|
130
|
+
// The question was answered (qa_continue_from_blocker stored the input): resume the autopilot
|
|
131
|
+
// with it. Starting that job is the state change; replaying the resume call would loop.
|
|
132
|
+
if (answered)
|
|
133
|
+
return {
|
|
134
|
+
tool: 'qa_test_this',
|
|
135
|
+
why: `the needs_input question of job ${lastJob.jobId} was answered; re-run the autopilot with the answer`,
|
|
136
|
+
args: { mode: 'execute', ...recallTestThisIntent(s), sessionId: sid, stopOnNeedsInput: false },
|
|
137
|
+
};
|
|
138
|
+
// A qa_report recommendation is satisfied by any report newer than the job.
|
|
139
|
+
const satisfied = blocked || (nra?.tool === 'qa_report' && reportedSince);
|
|
140
|
+
if (nra?.tool && !satisfied)
|
|
141
|
+
return {
|
|
142
|
+
tool: nra.tool,
|
|
143
|
+
why: result.state === 'needs_input'
|
|
144
|
+
? `last job ${lastJob.jobId} is waiting on one question: ${nra.why ?? 'answer it and resume'}`
|
|
145
|
+
: `last job ${lastJob.jobId} ${result.state}: ${nra.why ?? 'follow its recommendation'}; no further calls needed after that`,
|
|
146
|
+
args: { ...(nra.args ?? {}) },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// 3. No device bound: orchestrate setup end-to-end.
|
|
25
150
|
if (!s.device)
|
|
26
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
tool: 'qa_test_this',
|
|
153
|
+
why: goal ? `no device/app prepared yet, run the autopilot for goal "${goal}"` : 'no device/app prepared yet, orchestrate setup',
|
|
154
|
+
args: { sessionId: sid, mode: 'execute', ...(goal ? { goal } : {}) },
|
|
155
|
+
};
|
|
156
|
+
// 4. Device bound but no app launched: route to the platform's prepare tool (H9: after
|
|
157
|
+
// `qa_ios boot` the bound device is a simulator; the Android-only qa_prepare_target would fail).
|
|
27
158
|
if (!s.appId)
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
159
|
+
return sessionPlatform(s) === 'ios'
|
|
160
|
+
? {
|
|
161
|
+
tool: 'qa_prepare_ios_target',
|
|
162
|
+
why: 'iOS simulator bound but no app launched',
|
|
163
|
+
args: { sessionId: sid, device: s.device },
|
|
164
|
+
}
|
|
165
|
+
: { tool: 'qa_prepare_target', why: 'device bound but no app launched', args: { sessionId: sid } };
|
|
166
|
+
// 5. App is up but nothing has been exercised (a smoke pass is a milestone, not an action count).
|
|
167
|
+
if (!smokeRan(s) && s.recordedActions.length === 0)
|
|
168
|
+
return { tool: 'qa_smoke', why: 'app is up but nothing exercised yet, run a smoke pass', args: { sessionId: sid } };
|
|
169
|
+
const report = latestReport(s);
|
|
170
|
+
const reportFresh = !!report && report.createdAt >= lastActivityAt(s);
|
|
171
|
+
// 6. Findings recorded: reporting them beats generating more assets.
|
|
172
|
+
if (s.findings.length > 0 && !reportFresh)
|
|
173
|
+
return { tool: 'qa_report', why: `${s.findings.length} finding(s) recorded, summarize with evidence`, args: { sessionId: sid } };
|
|
174
|
+
// 7. Clean run with recorded actions but no durable asset yet: make the run reusable.
|
|
175
|
+
if (s.findings.length === 0 && s.recordedActions.length > 0 && !hasGeneratedAssets(s))
|
|
176
|
+
return {
|
|
177
|
+
tool: 'qa_generate',
|
|
178
|
+
why: 'actions recorded, turn the run into a durable POM suite',
|
|
179
|
+
args: { sessionId: sid, target: 'suite' },
|
|
180
|
+
};
|
|
181
|
+
// 8. Wrap up with a report covering everything done so far.
|
|
182
|
+
if (!reportFresh)
|
|
183
|
+
return {
|
|
184
|
+
tool: 'qa_report',
|
|
185
|
+
why: hasGeneratedAssets(s)
|
|
186
|
+
? 'clean run and test assets already generated, wrap up and report'
|
|
187
|
+
: 'smoke done, wrap up and report what was covered',
|
|
188
|
+
args: { sessionId: sid },
|
|
189
|
+
};
|
|
190
|
+
// 9. Terminal: a report newer than any activity exists. Read it; nothing else to do.
|
|
191
|
+
return {
|
|
192
|
+
tool: 'qa_get_artifact',
|
|
193
|
+
why: 'the run is reported. Read the report; no further Swipium calls are needed',
|
|
194
|
+
args: { uri: report.uri },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Server `instructions` (MCP InitializeResult.instructions): the operating manual clients may put
|
|
198
|
+
* in the model's system prompt. Kept short on purpose; per-tool detail lives in tool descriptions
|
|
199
|
+
* and docs/tools.md; qa_status (no sessionId) returns the same rules plus capability groups. */
|
|
200
|
+
export const SERVER_INSTRUCTIONS = [
|
|
201
|
+
'Swipium runs mobile QA on local Android Emulators and iOS Simulators (physical devices are out of scope).',
|
|
202
|
+
'',
|
|
203
|
+
'First call: qa_test_this {mode:"execute"} (add goal, e.g. "release_gate"). It finds or builds the app, prepares an emulator/simulator, tests, and reports in a background job. It usually returns requiresConsent first (boot/install/build, see Consent), then {sessionId, jobId, state:"running"}.',
|
|
204
|
+
'Polling: qa_job_status {sessionId, jobId, waitMs:60000} until status is no longer "running". Then result.state is completed | blocked | unsafe | needs_input (none if cancelled), with reportUri. On completed, read it (qa_get_artifact {uri: reportUri}) and stop unless the user asks for more.',
|
|
205
|
+
'Orientation: qa_status without sessionId returns these rules + tool groups; with sessionId, session state and nextBestAction (goal biases it).',
|
|
206
|
+
'',
|
|
207
|
+
'needs_input: relay exactly the one returned question (fields, secret flags) to the user, then make the returned `resume` call (qa_continue_from_blocker) with their answer and follow its nextAction. Never invent extra questions.',
|
|
208
|
+
'blocked / unsafe: relay failureCode, owner, what Swipium tried, and the fix (qa_explain_blocker {failureCode, sessionId}). A build failure is not a test failure.',
|
|
209
|
+
'Ask the user only on needs_input or a consent request; otherwise keep going.',
|
|
210
|
+
'Consent: clients with MCP elicitation prompt the user directly. Otherwise show a requiresConsent result to the user and re-call with consentId + approve:true only after they agree. CONSENT_DECLINED / CONSENT_CANCELLED / CONSENT_REFUSED mean nothing ran - do not retry without asking.',
|
|
211
|
+
'',
|
|
212
|
+
'Project root: projectRoot arg, else MCP roots, else SWIPIUM_PROJECT_ROOT, else CLAUDE_PROJECT_DIR, else the server cwd if it is not / or $HOME and contains a project marker (package.json, app.json, pubspec.yaml, Gradle/Xcode files, Podfile, android/, ios/). On PROJECT_ROOT_UNRESOLVED pass an absolute projectRoot.',
|
|
213
|
+
'iOS Simulator without WebDriverAgent is visual-only: qa_snapshot/qa_act return BACKEND_UNSUPPORTED; use qa_screenshot + qa_visual, or attach WebDriverAgent via qa_wda. qa_test_this then runs a visual-only smoke.',
|
|
214
|
+
'Unknown arguments fail with INVALID_ARGUMENT (nothing runs). STALE_CLIENT means the client runs an outdated server: restart the client.',
|
|
215
|
+
'Feature work: read the app map first (qa_app_map_read / qa_app_map_query / qa_app_map_feature_scope); qa_test_feature tests one feature.',
|
|
216
|
+
'Low-level escape hatches: qa_start_session, qa_prepare_target / qa_prepare_ios_target, qa_snapshot, qa_act, qa_visual, qa_flow_run.',
|
|
217
|
+
].join('\n');
|
|
218
|
+
/** qa_status without a sessionId: first-call orientation (the operating rules + capability groups
|
|
219
|
+
* by name). Pure + deterministic so it is snapshot-testable. */
|
|
220
|
+
export function orientation(goal) {
|
|
40
221
|
return {
|
|
41
|
-
|
|
222
|
+
orientation: true,
|
|
223
|
+
swipiumVersion: SWIPIUM_VERSION,
|
|
42
224
|
tools: TOOL_COUNT,
|
|
43
225
|
prompts: PROMPT_COUNT,
|
|
44
|
-
purpose: 'Turn a coding agent into a mobile QA tester. For a zero-context "test this" request, call the macro tool first.',
|
|
45
226
|
firstCall: {
|
|
46
227
|
tool: 'qa_test_this',
|
|
47
|
-
args: { mode: 'execute' },
|
|
48
|
-
note: 'Pass projectRoot if the client
|
|
228
|
+
args: { mode: 'execute', ...(goal ? { goal } : {}) },
|
|
229
|
+
note: 'Pass projectRoot="/abs/path" if the client exposes no workspace root. Usually returns requiresConsent first (boot/install/build); after approval it runs as a background job.',
|
|
49
230
|
},
|
|
50
231
|
polling: {
|
|
51
232
|
tool: 'qa_job_status',
|
|
52
|
-
args: { sessionId: '<from firstCall>', jobId: '<from firstCall>' },
|
|
233
|
+
args: { sessionId: '<from firstCall>', jobId: '<from firstCall>', waitMs: 60000 },
|
|
234
|
+
until: 'status is no longer "running" (done | failed | cancelled; a cancelled job has no result)',
|
|
53
235
|
terminalStates: ['completed', 'blocked', 'unsafe', 'needs_input'],
|
|
54
236
|
},
|
|
55
|
-
report: {
|
|
56
|
-
tool: 'qa_get_artifact',
|
|
57
|
-
args: { uri: '<reportUri from the terminal result>' },
|
|
58
|
-
rule: 'Fetch reportUri instead of calling more tools, unless nextRecommendedAction says otherwise.',
|
|
59
|
-
},
|
|
237
|
+
report: { tool: 'qa_get_artifact', args: { uri: '<reportUri from the terminal result>' } },
|
|
60
238
|
goals: TEST_GOALS,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
239
|
+
rules: {
|
|
240
|
+
needsInput: 'Relay exactly the one returned question; make its `resume` call (qa_continue_from_blocker) with the answer, then follow nextAction.',
|
|
241
|
+
blocker: 'Relay failureCode, owner, what was tried, and the fix (qa_explain_blocker {failureCode, sessionId}). A build failure is not a test failure.',
|
|
242
|
+
consent: 'Clients with MCP elicitation prompt the user directly. Otherwise show requiresConsent to the user; re-call with consentId + approve:true only after they agree. CONSENT_DECLINED / CANCELLED / REFUSED: nothing ran, do not retry without asking.',
|
|
243
|
+
stop: 'Ask the user only on needs_input or a consent request; otherwise continue. After a completed job, read the report and stop.',
|
|
244
|
+
iosVisualOnly: 'An iOS Simulator without WebDriverAgent is visual-only: use qa_screenshot + qa_visual, or attach WebDriverAgent via qa_wda.',
|
|
245
|
+
appMap: 'Before feature testing, read the app map (qa_app_map_read / qa_app_map_query / qa_app_map_feature_scope).',
|
|
246
|
+
},
|
|
247
|
+
capabilityGroups: CAPABILITY_GROUPS.map((g) => ({ group: g.group, purpose: g.purpose, tools: [...g.tools] })),
|
|
248
|
+
nextBestAction: {
|
|
249
|
+
tool: 'qa_test_this',
|
|
250
|
+
why: goal ? `no session yet, run the autopilot for goal "${goal}"` : 'no session yet, start the autopilot',
|
|
251
|
+
args: { mode: 'execute', ...(goal ? { goal } : {}) },
|
|
252
|
+
},
|
|
68
253
|
};
|
|
69
254
|
}
|
|
70
255
|
export function registerAgentTools(server, sessions) {
|
|
71
|
-
// ---- qa_agent_brief ----
|
|
72
|
-
server.registerTool('qa_agent_brief', {
|
|
73
|
-
title: 'Agent brief (how to drive Swipium)',
|
|
74
|
-
description: 'Read-only, cheap, deterministic. Returns the current-version instruction set for coding agents: the first call for low-context QA (qa_test_this), the polling + report-fetch flow, the NeedsInput/blocker rules, when to stop vs. continue, and which tools are macros vs. low-level escape hatches. Call this once at the start instead of inspecting the source tree.',
|
|
75
|
-
inputSchema: {},
|
|
76
|
-
}, async () => {
|
|
77
|
-
const b = agentBrief();
|
|
78
|
-
const summary = `Swipium v${b.version} (${b.tools} tools, ${b.prompts} prompts) — agent brief\n` +
|
|
79
|
-
`1. ${b.firstCall.tool} ${JSON.stringify(b.firstCall.args)}\n` +
|
|
80
|
-
`2. ${b.polling.tool} until state ∈ {${b.polling.terminalStates.join(', ')}}\n` +
|
|
81
|
-
`3. ${b.report.tool} { uri: reportUri }\n` +
|
|
82
|
-
'needs_input -> ask the one returned question; otherwise continue.';
|
|
83
|
-
return qaOk(b, summary);
|
|
84
|
-
});
|
|
85
256
|
// ---- qa_status ----
|
|
86
257
|
server.registerTool('qa_status', {
|
|
87
|
-
title: '
|
|
88
|
-
description: '
|
|
89
|
-
|
|
90
|
-
|
|
258
|
+
title: 'Status, orientation, and next step',
|
|
259
|
+
description: 'Without sessionId: first-call orientation: how to drive Swipium (first call, polling, stop rules) plus the tool groups. ' +
|
|
260
|
+
'With sessionId: compact session state (device, app, budget left, counters, findings, last job, workarounds, readiness) and ' +
|
|
261
|
+
'nextBestAction: the single next tool to call, with args and why. Pass goal to bias the recommendation. Cheap; call between steps.',
|
|
262
|
+
inputSchema: {
|
|
263
|
+
sessionId: z.string().optional().describe('Omit for orientation; pass to get that session state.'),
|
|
264
|
+
goal: z
|
|
265
|
+
.enum(TEST_GOALS)
|
|
266
|
+
.optional()
|
|
267
|
+
.describe('Bias nextBestAction toward this autopilot goal.'),
|
|
268
|
+
},
|
|
269
|
+
}, async ({ sessionId, goal }) => {
|
|
270
|
+
if (!sessionId) {
|
|
271
|
+
const o = orientation(goal);
|
|
272
|
+
const summary = `Swipium v${o.swipiumVersion} (${o.tools} tools, ${o.prompts} prompts)\n` +
|
|
273
|
+
`1. ${o.firstCall.tool} ${JSON.stringify(o.firstCall.args)}\n` +
|
|
274
|
+
`2. ${o.polling.tool} {sessionId, jobId, waitMs} until status ≠ running; result.state ∈ {${o.polling.terminalStates.join(', ')}}\n` +
|
|
275
|
+
`3. ${o.report.tool} { uri: reportUri }, then stop\n` +
|
|
276
|
+
'requiresConsent > show it to the user; needs_input > ask the one returned question; otherwise continue.\n' +
|
|
277
|
+
o.capabilityGroups.map((g) => `[${g.group}] ${g.tools.join(', ')}`).join('\n');
|
|
278
|
+
return qaOk(o, summary);
|
|
279
|
+
}
|
|
91
280
|
const s = sessions.get(sessionId);
|
|
92
281
|
if (!s)
|
|
93
|
-
return
|
|
282
|
+
return unknownSessionError(sessionId, [
|
|
283
|
+
'Call qa_status without sessionId for orientation, or qa_start_session / qa_test_this to create a session.',
|
|
284
|
+
]);
|
|
94
285
|
const remaining = budgetRemaining(s);
|
|
95
286
|
const lastJob = [...s.jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
96
|
-
const next = nextBestAction(s);
|
|
287
|
+
const next = nextBestAction(s, goal);
|
|
97
288
|
const status = {
|
|
98
|
-
sessionId: s.id,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
289
|
+
sessionId: s.id,
|
|
290
|
+
root: s.root,
|
|
291
|
+
device: s.device ?? null,
|
|
292
|
+
appId: s.appId ?? null,
|
|
293
|
+
mode: effectiveMode(s),
|
|
294
|
+
budgetRemaining: remaining,
|
|
295
|
+
counters: s.counters,
|
|
296
|
+
recordedActions: s.recordedActions.length,
|
|
297
|
+
findings: s.findings.length,
|
|
298
|
+
notes: s.notes.length,
|
|
299
|
+
workarounds: s.workarounds,
|
|
300
|
+
inputsProvided: s.inputs.map((i) => i.varName),
|
|
102
301
|
readiness: readinessForSession(s),
|
|
103
|
-
lastJob: lastJob
|
|
302
|
+
lastJob: lastJob
|
|
303
|
+
? {
|
|
304
|
+
jobId: lastJob.jobId,
|
|
305
|
+
kind: lastJob.kind,
|
|
306
|
+
status: lastJob.status,
|
|
307
|
+
progress: lastJob.progress,
|
|
308
|
+
progressDetail: lastJob.progressDetail ?? null,
|
|
309
|
+
}
|
|
310
|
+
: null,
|
|
104
311
|
nextBestAction: next,
|
|
105
312
|
};
|
|
106
313
|
const progLine = progressLine(lastJob?.progressDetail);
|
|
107
|
-
const summary = `session ${s.id}
|
|
314
|
+
const summary = `session ${s.id}: ${s.device ?? 'no device'}${s.appId ? ` / ${s.appId}` : ''} (mode=${status.mode})\n` +
|
|
108
315
|
`budget left: ${remaining.minutes}m / ${remaining.actions} actions / ${remaining.screenshots} shots\n` +
|
|
109
316
|
`recorded=${s.recordedActions.length} findings=${s.findings.length} notes=${s.notes.length}` +
|
|
110
|
-
(lastJob
|
|
317
|
+
(lastJob
|
|
318
|
+
? `\nlast job: ${lastJob.kind} [${lastJob.status}]${progLine ? `\n ${progLine}` : lastJob.progress ? ` ${lastJob.progress}` : ''}`
|
|
319
|
+
: '') +
|
|
111
320
|
(s.workarounds.length ? `\nworkarounds: ${s.workarounds.length}` : '') +
|
|
112
|
-
`\
|
|
321
|
+
`\nnext: ${next.tool} ${JSON.stringify(next.args)} (${next.why})`;
|
|
113
322
|
return qaOk(status, summary);
|
|
114
323
|
});
|
|
115
|
-
// ---- qa_next_best_action ----
|
|
116
|
-
server.registerTool('qa_next_best_action', {
|
|
117
|
-
title: 'What should I call next?',
|
|
118
|
-
description: 'Given the current session state, returns the single best next tool to call (with args) and why — so an agent does not have to reason out the orchestration sequence. Deterministic, read-only. Pass `goal` to bias the first call toward a specific autopilot intent.',
|
|
119
|
-
inputSchema: { sessionId: z.string(), goal: z.enum(['smoke', 'explore', 'create_automation_suite', 'release_gate', 'test_login', 'reproduce_bug']).optional() },
|
|
120
|
-
}, async ({ sessionId, goal }) => {
|
|
121
|
-
const s = sessions.get(sessionId);
|
|
122
|
-
if (!s)
|
|
123
|
-
return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
|
|
124
|
-
const next = nextBestAction(s, goal);
|
|
125
|
-
return qaOk({ nextBestAction: next }, `→ ${next.tool} ${JSON.stringify(next.args)} — ${next.why}`);
|
|
126
|
-
});
|
|
127
324
|
// ---- qa_explain_blocker ----
|
|
128
325
|
server.registerTool('qa_explain_blocker', {
|
|
129
326
|
title: 'Explain a blocker',
|
|
130
|
-
description: '
|
|
327
|
+
description: 'Explain a failureCode in plain language: what it means, who owns the fix (app / environment / Swipium / user), whether ' +
|
|
328
|
+
'it is retry-safe or Swipium can fix it, and the recovery step.',
|
|
131
329
|
inputSchema: {
|
|
132
|
-
failureCode: z.string().describe('
|
|
330
|
+
failureCode: z.string().describe('failureCode from a Swipium error.'),
|
|
133
331
|
context: z.string().optional(),
|
|
332
|
+
sessionId: z.string().optional().describe('The blocked session (qa_status then moves past the blocker).'),
|
|
134
333
|
},
|
|
135
|
-
}, async ({ failureCode, context }) => {
|
|
334
|
+
}, async ({ failureCode, context, sessionId }) => {
|
|
335
|
+
const sess = sessionId ? sessions.get(sessionId) : undefined;
|
|
336
|
+
if (sessionId && !sess)
|
|
337
|
+
return unknownSessionError(sessionId);
|
|
136
338
|
const code = failureCode;
|
|
137
339
|
const info = FAILURES[code];
|
|
138
340
|
if (!info) {
|
|
139
|
-
return qaError({
|
|
341
|
+
return qaError({
|
|
342
|
+
what: `Unknown failure code "${failureCode}"`,
|
|
343
|
+
changedState: false,
|
|
344
|
+
retrySafe: true,
|
|
345
|
+
nextSteps: ['Pass a code surfaced by a Swipium tool (the `failureCode` field of an error).'],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// Mark the blocker explained so qa_status's ladder stops recommending this call.
|
|
349
|
+
if (sess) {
|
|
350
|
+
sess.milestones[BLOCKER_EXPLAINED_MILESTONE] = Date.now();
|
|
351
|
+
sessions.persist(sess);
|
|
140
352
|
}
|
|
141
353
|
const owner = failureOwner(code);
|
|
142
354
|
const ownerText = {
|
|
@@ -146,12 +358,18 @@ export function registerAgentTools(server, sessions) {
|
|
|
146
358
|
user: 'you (provide input/approval/test data)',
|
|
147
359
|
};
|
|
148
360
|
const explanation = {
|
|
149
|
-
failureCode: code,
|
|
150
|
-
|
|
151
|
-
|
|
361
|
+
failureCode: code,
|
|
362
|
+
bucket: info.bucket,
|
|
363
|
+
owner,
|
|
364
|
+
severity: info.severity,
|
|
365
|
+
retrySafe: info.retrySafe,
|
|
366
|
+
canSwipiumFix: isSelfFixable(code),
|
|
367
|
+
whatItMeans: info.summary,
|
|
368
|
+
whoFixesIt: ownerText[owner],
|
|
369
|
+
howToFix: info.recovery,
|
|
152
370
|
context: context ?? null,
|
|
153
371
|
};
|
|
154
|
-
const summary = `${code}
|
|
372
|
+
const summary = `${code}: ${info.summary}\n` +
|
|
155
373
|
`owner: ${ownerText[owner]}; retry-safe: ${info.retrySafe}; Swipium can fix: ${isSelfFixable(code)}\n` +
|
|
156
374
|
`fix: ${info.recovery}`;
|
|
157
375
|
return qaOk(explanation, summary);
|
|
@@ -159,66 +377,257 @@ export function registerAgentTools(server, sessions) {
|
|
|
159
377
|
// ---- qa_continue_from_blocker ----
|
|
160
378
|
server.registerTool('qa_continue_from_blocker', {
|
|
161
379
|
title: 'Resume after providing input',
|
|
162
|
-
description: '
|
|
380
|
+
description: 'Answer a needs_input question and get the next call. Secret values (passwords/OTP/tokens) join the redaction set at ' +
|
|
381
|
+
'once and are never echoed or logged (flows reference them as ${VAR}); non-secret choices (platform, device, target, allowOutsideRoot) map onto ' +
|
|
382
|
+
'the re-invocation args. Returns accepted, ignored[] (with how to apply), projectRoot for monorepo_target, and ' +
|
|
383
|
+
'nextAction.',
|
|
163
384
|
inputSchema: {
|
|
164
385
|
sessionId: z.string(),
|
|
165
|
-
kind: z.string().describe('The
|
|
166
|
-
values: z
|
|
167
|
-
|
|
386
|
+
kind: z.string().describe('The needs_input kind (e.g. credentials, monorepo_target).'),
|
|
387
|
+
values: z
|
|
388
|
+
.record(z.union([z.string(), z.boolean()]))
|
|
389
|
+
.optional()
|
|
390
|
+
.describe('Field name > value. Secret fields are redacted on receipt.'),
|
|
391
|
+
secretFields: z.array(z.string()).optional().describe('Secret keys (default: password/otp/token-like names).'),
|
|
168
392
|
},
|
|
169
393
|
}, async ({ sessionId, kind, values, secretFields }) => {
|
|
170
394
|
const s = sessions.get(sessionId);
|
|
171
395
|
if (!s)
|
|
172
|
-
return
|
|
173
|
-
const vals = values ?? {};
|
|
174
|
-
|
|
175
|
-
const
|
|
396
|
+
return unknownSessionError(sessionId);
|
|
397
|
+
const vals = { ...(values ?? {}) };
|
|
398
|
+
// Shared secret-name rule (flows/schema.ts); `code` covers OTP fields named code / verificationCode.
|
|
399
|
+
const secretRe = SECRET_VAR_NAME;
|
|
400
|
+
// "test pre-login only" (the credentials question's own fallback) is a DECISION, not an input:
|
|
401
|
+
// mark login out of scope for the session instead of reporting it as an unknown field.
|
|
402
|
+
const declined = (kind === 'credentials' || kind === 'otp_or_manual_verification') && extractDecline(vals);
|
|
403
|
+
if (declined)
|
|
404
|
+
markLoginDeclined(sessions, s);
|
|
405
|
+
const isSecret = (k) => secretFields?.includes(k) || secretRe.test(k);
|
|
176
406
|
// Register inputs into the SECURE STORE (P0.5): each maps to a flow variable; secret values
|
|
177
407
|
// join the redaction set and are NEVER echoed back. Generated flows reference the var name.
|
|
178
408
|
const accepted = [];
|
|
179
409
|
const storedVars = [];
|
|
180
|
-
const
|
|
410
|
+
const choices = {};
|
|
181
411
|
for (const [k, v] of Object.entries(vals)) {
|
|
182
412
|
const secret = isSecret(k);
|
|
183
413
|
if (typeof v === 'string' && (secret || /email|user|account|otp|code|token|pin/i.test(k))) {
|
|
184
414
|
const varName = inputVarName(k);
|
|
185
415
|
sessions.setInput(s, varName, v, secret, `needs_input:${kind}`);
|
|
186
416
|
storedVars.push(varName);
|
|
187
|
-
accepted.push(`${k}
|
|
417
|
+
accepted.push(`${k} > \${${varName}}${secret ? ' (redacted)' : ''}`);
|
|
188
418
|
}
|
|
189
419
|
else {
|
|
190
|
-
|
|
191
|
-
if (k === 'platform')
|
|
192
|
-
reInvokeArgs.platform = v;
|
|
193
|
-
if (k === 'allowOutsideRoot')
|
|
194
|
-
reInvokeArgs.allowOutsideRoot = v;
|
|
195
|
-
if (k === 'target' || k === 'device')
|
|
196
|
-
reInvokeArgs.device = v;
|
|
197
|
-
if (k === 'developmentTeam' || k === 'provisioningProfile' || k === 'serviceEndpoint')
|
|
198
|
-
reInvokeArgs[k] = v;
|
|
420
|
+
choices[k] = v;
|
|
199
421
|
}
|
|
200
422
|
}
|
|
423
|
+
// H1: map non-secret choices onto args the re-invoked tool ACTUALLY accepts; anything it
|
|
424
|
+
// doesn't consume is reported back as `ignored` (with how to apply it), never silently dropped.
|
|
425
|
+
const mapped = mapBlockerChoices(kind, choices, s.root);
|
|
426
|
+
if (mapped.error) {
|
|
427
|
+
sessions.persist(s);
|
|
428
|
+
return qaError({
|
|
429
|
+
failureCode: 'INVALID_ARGUMENT',
|
|
430
|
+
what: mapped.error,
|
|
431
|
+
changedState: storedVars.length > 0,
|
|
432
|
+
retrySafe: true,
|
|
433
|
+
nextSteps: [
|
|
434
|
+
'Re-call qa_continue_from_blocker with target set to one of the offered candidate paths (absolute, or relative to the project root).',
|
|
435
|
+
],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
for (const [k, v] of Object.entries(choices))
|
|
439
|
+
if (!mapped.ignored.some((i) => i.field === k))
|
|
440
|
+
accepted.push(`${k}=${v}`);
|
|
441
|
+
if (mapped.projectRoot) {
|
|
442
|
+
// qa_test_this resolves an existing session's root from the SESSION (projectRoot is only
|
|
443
|
+
// read when creating one), so adopt the chosen app directory as the effective root; the
|
|
444
|
+
// same thing the single-candidate discovery path does.
|
|
445
|
+
s.root = mapped.projectRoot;
|
|
446
|
+
s.chosenTarget = mapped.projectRoot; // qa_test_this skips the monorepo question for this root
|
|
447
|
+
}
|
|
448
|
+
// Persisted answer marker: qa_status stops replaying the resume call once answered.
|
|
449
|
+
s.milestones[BLOCKER_ANSWERED_MILESTONE] = Date.now();
|
|
201
450
|
sessions.persist(s);
|
|
202
451
|
sessions.addWorkaround(s, `resumed from "${kind}" blocker with: ${accepted.join(', ') || '(no values)'}`);
|
|
452
|
+
const reInvokeArgs = { sessionId: s.id, ...mapped.args };
|
|
453
|
+
// Replay the ORIGINAL qa_test_this intent (goal/goalText/flags). A resume that drops it
|
|
454
|
+
// silently downgrades e.g. release_gate to the default smoke.
|
|
455
|
+
const intent = recallTestThisIntent(s);
|
|
203
456
|
// Decide the resume action by kind.
|
|
204
457
|
const reInvokeKinds = new Set([
|
|
205
|
-
'artifact_outside_root',
|
|
458
|
+
'artifact_outside_root',
|
|
459
|
+
'preferred_platform',
|
|
460
|
+
'monorepo_target',
|
|
206
461
|
'signing_team',
|
|
207
|
-
'destructive_exploration_approval',
|
|
462
|
+
'destructive_exploration_approval',
|
|
463
|
+
'external_service_required',
|
|
208
464
|
]);
|
|
209
465
|
// Every resume is a DIRECTLY executable call. Credentials/OTP are now registered as secure
|
|
210
466
|
// inputs, so re-invoking the autopilot drives the authenticated flows with them (the macro
|
|
211
|
-
// tool resolves the already-prepared session/device and continues)
|
|
212
|
-
const resume =
|
|
213
|
-
? {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
467
|
+
// tool resolves the already-prepared session/device and continues), not a bare qa_act guess.
|
|
468
|
+
const resume = declined
|
|
469
|
+
? {
|
|
470
|
+
tool: 'qa_test_this',
|
|
471
|
+
why: kind === 'credentials'
|
|
472
|
+
? 'login marked out of scope for this session. Continue with pre-login coverage only (authenticated flows are reported as blocked, not failed)'
|
|
473
|
+
: 'verification marked out of scope; flows behind it are skipped and reported as blocked',
|
|
474
|
+
args: { mode: 'execute', ...intent, sessionId: s.id, stopOnNeedsInput: false },
|
|
475
|
+
}
|
|
476
|
+
: kind === 'credentials' || kind === 'otp_or_manual_verification'
|
|
477
|
+
? {
|
|
478
|
+
tool: 'qa_test_this',
|
|
479
|
+
why: storedVars.length
|
|
480
|
+
? 'credentials registered (redacted). Re-run the autopilot to drive authenticated flows with them'
|
|
481
|
+
: 'no credentials were provided. Re-run the autopilot (pre-login coverage)',
|
|
482
|
+
args: { mode: 'execute', ...intent, sessionId: s.id, stopOnNeedsInput: false },
|
|
483
|
+
}
|
|
484
|
+
: kind === 'destructive_exploration_approval' && mapped.approveDestructive
|
|
485
|
+
? {
|
|
486
|
+
// qa_test_this has no destructive switch; the supported path is qa_explore's
|
|
487
|
+
// candidate-bound approval: discover candidates first, then approve one exactly.
|
|
488
|
+
tool: 'qa_explore',
|
|
489
|
+
why: 'destructive exploration approved. Discover exact destructive candidates first (dry run), then approve one with safeMode:"approved_destructive_candidate"',
|
|
490
|
+
args: { sessionId: s.id, safeMode: 'dry_run_destructive' },
|
|
491
|
+
}
|
|
492
|
+
: reInvokeKinds.has(kind)
|
|
493
|
+
? {
|
|
494
|
+
tool: 'qa_test_this',
|
|
495
|
+
why: 're-run orchestration with your choice applied',
|
|
496
|
+
args: { mode: 'execute', ...intent, ...reInvokeArgs },
|
|
497
|
+
}
|
|
498
|
+
: { tool: 'qa_test_this', why: 'resume orchestration', args: { mode: 'execute', ...intent, ...reInvokeArgs } };
|
|
499
|
+
return qaOk({
|
|
500
|
+
kind,
|
|
501
|
+
accepted,
|
|
502
|
+
...(declined ? { loginOutOfScope: true } : {}),
|
|
503
|
+
storedVariables: storedVars,
|
|
504
|
+
ignored: mapped.ignored,
|
|
505
|
+
...(mapped.projectRoot ? { projectRoot: mapped.projectRoot } : {}),
|
|
506
|
+
nextAction: resume,
|
|
507
|
+
secretsRegistered: accepted.filter((a) => a.includes('redacted')).length,
|
|
508
|
+
}, (declined ? 'Login marked out of scope for this session. Testing continues pre-login only.\n' : '') +
|
|
509
|
+
`Accepted ${accepted.length} field(s)${accepted.some((a) => a.includes('redacted')) ? ' (secrets redacted)' : ''}.` +
|
|
218
510
|
(storedVars.length ? `\nstored for replay: ${storedVars.join(', ')}` : '') +
|
|
219
|
-
`\
|
|
511
|
+
(mapped.ignored.length ? `\nnot applied: ${mapped.ignored.map((i) => `${i.field} (${i.howToApply})`).join('; ')}` : '') +
|
|
512
|
+
`\nnext: ${resume.tool} ${JSON.stringify(resume.args)} (${resume.why})`);
|
|
220
513
|
});
|
|
221
514
|
}
|
|
515
|
+
/** The qa_test_this input keys a blocker resume may set (mirrors its zod schema in
|
|
516
|
+
* src/tools/testThis.ts; test/blockerResume.test.ts asserts they stay in lockstep). */
|
|
517
|
+
export const TEST_THIS_RESUME_KEYS = ['projectRoot', 'platform', 'device', 'allowOutsideRoot'];
|
|
518
|
+
function realOrSelf(p) {
|
|
519
|
+
try {
|
|
520
|
+
return realpathSync(p);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return resolvePath(p);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function truthy(v) {
|
|
527
|
+
return v === true || (typeof v === 'string' && /^(true|yes|1)$/i.test(v.trim()));
|
|
528
|
+
}
|
|
529
|
+
/** Map a NeedsInput answer's non-secret choices onto re-invocation args (H1). Pure except for the
|
|
530
|
+
* existence check on a monorepo target. Exported for tests. */
|
|
531
|
+
export function mapBlockerChoices(kind, choices, sessionRoot) {
|
|
532
|
+
const out = { args: {}, ignored: [] };
|
|
533
|
+
const ignore = (field, value, howToApply) => out.ignored.push({ field, value, howToApply });
|
|
534
|
+
for (const [k, v] of Object.entries(choices)) {
|
|
535
|
+
switch (k) {
|
|
536
|
+
case 'platform':
|
|
537
|
+
if (v === 'android' || v === 'ios')
|
|
538
|
+
out.args.platform = v;
|
|
539
|
+
else
|
|
540
|
+
ignore(k, v, 'platform must be "android" or "ios"');
|
|
541
|
+
break;
|
|
542
|
+
case 'allowOutsideRoot':
|
|
543
|
+
out.args.allowOutsideRoot = truthy(v);
|
|
544
|
+
break;
|
|
545
|
+
case 'device':
|
|
546
|
+
if (typeof v === 'string' && v.trim())
|
|
547
|
+
out.args.device = v.trim();
|
|
548
|
+
else
|
|
549
|
+
ignore(k, v, 'device must be a serial/UDID string');
|
|
550
|
+
break;
|
|
551
|
+
case 'target': {
|
|
552
|
+
if (kind === 'monorepo_target') {
|
|
553
|
+
// The monorepo question offers app DIRECTORIES. This is the project root, not a device.
|
|
554
|
+
if (typeof v !== 'string' || !v.trim()) {
|
|
555
|
+
out.error = 'monorepo target must be an app directory path';
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
const raw = v.trim();
|
|
559
|
+
if (raw === '~' || raw.startsWith('~/') || raw.startsWith('~' + sep)) {
|
|
560
|
+
out.error = `monorepo target "${v}" must be one of the offered app directories inside the project root (no ~ paths)`;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
const abs = isAbsolute(raw) ? resolvePath(raw) : resolvePath(sessionRoot, raw);
|
|
564
|
+
let isDir = false;
|
|
565
|
+
try {
|
|
566
|
+
isDir = statSync(abs).isDirectory();
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
isDir = false;
|
|
570
|
+
}
|
|
571
|
+
if (!isDir) {
|
|
572
|
+
out.error = `monorepo target "${v}" is not an existing directory (resolved to ${abs})`;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
// The offered candidates are always app directories discovered UNDER the project root
|
|
576
|
+
// (findMobileApps), so containment in the (real) root is the constraint: an answer can
|
|
577
|
+
// never re-point the session at `/`, the home dir, or an unrelated tree.
|
|
578
|
+
const realRoot = realOrSelf(sessionRoot);
|
|
579
|
+
const realTarget = realOrSelf(abs);
|
|
580
|
+
if (realTarget !== realRoot && !realTarget.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep)) {
|
|
581
|
+
out.error = `monorepo target "${v}" (${realTarget}) is outside the project root ${realRoot}; pick one of the offered app directories`;
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
out.projectRoot = abs;
|
|
585
|
+
out.args.projectRoot = abs;
|
|
586
|
+
}
|
|
587
|
+
else if (typeof v === 'string' && v.trim()) {
|
|
588
|
+
out.args.device = v.trim();
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
ignore(k, v, 'target must be a device serial/UDID string');
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case 'approveDestructive':
|
|
596
|
+
out.approveDestructive = truthy(v);
|
|
597
|
+
if (!out.approveDestructive)
|
|
598
|
+
ignore(k, v, 'exploration stays non-destructive (the default)');
|
|
599
|
+
break;
|
|
600
|
+
case 'developmentTeam':
|
|
601
|
+
ignore(k, v, 'qa_test_this does not take a signing team. Set ios.wda.developmentTeam in .swipium/config.json (or DEVELOPMENT_TEAM in the server environment), then resume');
|
|
602
|
+
break;
|
|
603
|
+
case 'provisioningProfile':
|
|
604
|
+
ignore(k, v, 'Swipium builds for the simulator and does not consume a provisioning profile. Configure it in Xcode if your scheme needs one');
|
|
605
|
+
break;
|
|
606
|
+
case 'serviceEndpoint':
|
|
607
|
+
ignore(k, v, 'Swipium cannot route the app to a service endpoint. Configure it in the app build/env (e.g. a staging config), then resume');
|
|
608
|
+
break;
|
|
609
|
+
default:
|
|
610
|
+
ignore(k, v, `not an input qa_test_this accepts (accepted: ${TEST_THIS_RESUME_KEYS.join(', ')})`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
const DECLINE_KEYS = /^(choice|fallback|fallbackOption|option|answer|decision|selected|preLoginOnly|decline|declined|skip)$/i;
|
|
616
|
+
const DECLINE_TEXT = /pre-?login only|stay pre-?login|decline|no credentials|skip (login|flows requiring verification)|^skip$/i;
|
|
617
|
+
/** Detect (and remove from `vals`) a "decline the input" answer: a fallback-option string such as
|
|
618
|
+
* "test pre-login only", or a boolean preLoginOnly/decline flag. Exported for tests. */
|
|
619
|
+
export function extractDecline(vals) {
|
|
620
|
+
let declined = false;
|
|
621
|
+
for (const [k, v] of Object.entries(vals)) {
|
|
622
|
+
if (!DECLINE_KEYS.test(k))
|
|
623
|
+
continue;
|
|
624
|
+
if ((typeof v === 'boolean' && v && /preLoginOnly|decline|skip/i.test(k)) || (typeof v === 'string' && DECLINE_TEXT.test(v.trim()))) {
|
|
625
|
+
declined = true;
|
|
626
|
+
delete vals[k];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return declined;
|
|
630
|
+
}
|
|
222
631
|
/** Map a NeedsInput field name to its canonical Swipium flow variable (P0.5). */
|
|
223
632
|
function inputVarName(field) {
|
|
224
633
|
const f = field.toLowerCase();
|