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/testThis.js
CHANGED
|
@@ -1,823 +1,56 @@
|
|
|
1
|
-
// qa_test_this
|
|
1
|
+
// qa_test_this: the "just test this" entry point. A DETERMINISTIC orchestration
|
|
2
2
|
// state machine so a first run does not depend on the agent's skill or token budget. It resolves
|
|
3
3
|
// the project, finds (or plans a build for) an artifact, picks a target, and returns an ordered
|
|
4
|
-
// plan with the EXACT next tool call to make
|
|
4
|
+
// plan with the EXACT next tool call to make, or a typed blocker / one concise NeedsInput
|
|
5
5
|
// question. It performs the cheap, side-effect-free resolution itself; the heavy device steps
|
|
6
6
|
// (build, boot/install, smoke) are dispatched to the existing one-shot tools via `nextAction`,
|
|
7
7
|
// so an agent reaches real work in one or two calls instead of ten.
|
|
8
8
|
//
|
|
9
9
|
// Honesty (§2.2): the result state is exactly one of ready | needs_input | blocked | unsafe, and
|
|
10
10
|
// every safe fallback Swipium chose is recorded in `workaroundsAttempted` (§11).
|
|
11
|
+
//
|
|
12
|
+
// This file is the MCP registration + schema only; the state machine lives in
|
|
13
|
+
// src/orchestration/testThis/ (plan resolution, execute gate, pipeline, terminal assembly).
|
|
11
14
|
import { z } from 'zod';
|
|
12
|
-
import {
|
|
13
|
-
import { qaFail } from '../oracle/failures.js';
|
|
14
|
-
import { qaNeedsInput, NeedsInput } from '../lib/needsInput.js';
|
|
15
|
-
import { scanProject } from '../context/scan.js';
|
|
16
|
-
import { findMobileApps, classifyDiscovery } from '../context/findApps.js';
|
|
17
|
-
import { resolveGoalFlags } from '../orchestration/goal.js';
|
|
18
|
-
import { buildTerminalEnvelope, typedBlockerFromCode } from '../orchestration/envelope.js';
|
|
19
|
-
import { resolveArtifact } from '../artifacts/resolve.js';
|
|
20
|
-
import { buildPlan } from '../build/plan.js';
|
|
21
|
-
import { adbDevices, listAvds, which, apkPackageId } from '../lib/android.js';
|
|
22
|
-
import { simctlAvailable, listSimulators } from '../lib/simctl.js';
|
|
23
|
-
import { checkWda } from '../lib/wda.js';
|
|
24
|
-
import { loadWdaConfig } from '../lib/wdaConfig.js';
|
|
25
|
-
import { planTarget } from '../core/targetPlan.js';
|
|
26
|
-
import { requireConsent, consumeConsent } from '../consent/consent.js';
|
|
27
|
-
import { executeBuild } from '../services/build.js';
|
|
28
|
-
import { convertAabToApk, universalApkCachePath } from '../artifacts/bundletool.js';
|
|
29
|
-
import { prepareAndroid } from '../services/prepareAndroid.js';
|
|
30
|
-
import { prepareIos } from '../services/prepareIos.js';
|
|
31
|
-
import { runSmoke } from '../services/smoke.js';
|
|
32
|
-
import { runExplore } from '../explore/runner.js';
|
|
33
|
-
import { runFirstRun, resolveFirstRunPolicy, observeScreen } from '../firstRun/firstRunRunner.js';
|
|
34
|
-
import { planFirstRun } from '../firstRun/firstRunPlanner.js';
|
|
35
|
-
import { generateSessionReport } from '../services/report.js';
|
|
36
|
-
import { generateAndCompileSuite } from '../services/suiteGenerate.js';
|
|
37
|
-
import { buildTestThisPreflight } from '../services/preflight.js';
|
|
38
|
-
import { startProgress } from '../session/progress.js';
|
|
39
|
-
import { ensurePrelaunchAppMap } from '../appMap/prelaunch.js';
|
|
40
|
-
import { staticCandidatesForObservation } from '../appMap/screenMatch.js';
|
|
41
|
-
import { DirectDriver } from '../drivers/DirectDriver.js';
|
|
42
|
-
import { log } from '../lib/logger.js';
|
|
43
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
44
|
-
import { sep } from 'node:path';
|
|
45
|
-
import { createHash } from 'node:crypto';
|
|
46
|
-
// Runtime screen purposes that should trigger first-run autonomy even when static auth detection
|
|
47
|
-
// missed them (SWIPIUM-REQ-02 Fix Group 5).
|
|
48
|
-
const FIRST_RUN_TRIGGER_PURPOSES = new Set([
|
|
49
|
-
'login', 'create_account', 'login_or_create_account', 'otp_or_email_verification', 'onboarding', 'permissions_prompt', 'paywall',
|
|
50
|
-
]);
|
|
51
|
-
const selToPlatform = {
|
|
52
|
-
'android-emulator': 'android',
|
|
53
|
-
'android-real': 'android',
|
|
54
|
-
'ios-simulator': 'ios',
|
|
55
|
-
'ios-real': 'ios',
|
|
56
|
-
};
|
|
57
|
-
function isAndroidEmulatorSerial(serial) {
|
|
58
|
-
return /^emulator-\d+/.test(serial);
|
|
59
|
-
}
|
|
15
|
+
import { handleTestThis } from '../orchestration/testThis/plan.js';
|
|
60
16
|
export function registerTestThis(server, sessions) {
|
|
61
17
|
server.registerTool('qa_test_this', {
|
|
62
18
|
title: 'Test this app (autopilot)',
|
|
63
|
-
description: '
|
|
19
|
+
description: 'Autopilot for "test this app": finds or builds an artifact, picks a device/simulator, then plans ' +
|
|
20
|
+
'(mode:"plan", default, no side effects) or executes prepare > smoke > (explore) > report > (suite). execute returns ' +
|
|
21
|
+
'state:"running" + jobId; the terminal state (completed/blocked/unsafe/needs_input) and reportUri are in the ' +
|
|
22
|
+
'qa_job_status result. One combined consent covers boot/install/build. A report in every terminal state. iOS without WDA ' +
|
|
23
|
+
'falls back to a visual-only smoke.',
|
|
64
24
|
inputSchema: {
|
|
65
|
-
sessionId: z.string().optional().describe('Reuse
|
|
25
|
+
sessionId: z.string().optional().describe('Reuse a session; otherwise one is created.'),
|
|
66
26
|
projectRoot: z.string().optional(),
|
|
67
|
-
mode: z
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
27
|
+
mode: z
|
|
28
|
+
.enum(['plan', 'execute', 'interactive'])
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('plan (default) | execute (background job) | interactive (run until the first question).'),
|
|
31
|
+
goal: z
|
|
32
|
+
.enum(['smoke', 'explore', 'create_automation_suite', 'release_gate', 'test_login', 'reproduce_bug'])
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Intent. Omitted: smoke, then tries a suite; "smoke" is fastest. Sets explore/generateSuite/stopOnNeedsInput; flags win.'),
|
|
35
|
+
goalText: z.string().optional().describe('reproduce_bug: the bug/flow to focus on.'),
|
|
36
|
+
fastSmoke: z.boolean().optional().describe('Just launch + smoke; skip suite generation (ignored with goal/generateSuite).'),
|
|
37
|
+
platform: z.enum(['android', 'ios']).optional().describe('Force a platform (default inferred).'),
|
|
72
38
|
device: z.string().optional(),
|
|
73
|
-
preferRealDevice: z.boolean().optional(),
|
|
39
|
+
preferRealDevice: z.boolean().optional().describe('Out of scope: returns PHYSICAL_DEVICE_UNSUPPORTED.'),
|
|
74
40
|
allowOutsideRoot: z.boolean().optional(),
|
|
75
|
-
buildIfNeeded: z.boolean().optional().describe('
|
|
76
|
-
generateSuite: z.boolean().optional().describe('
|
|
77
|
-
explore: z.boolean().optional().describe('
|
|
78
|
-
stopOnNeedsInput: z.boolean().optional().describe('
|
|
79
|
-
waitForCompletion: z.boolean().optional().describe('
|
|
80
|
-
timeoutMs: z.number().optional().describe('
|
|
41
|
+
buildIfNeeded: z.boolean().optional().describe('Build from source when no artifact exists (default true).'),
|
|
42
|
+
generateSuite: z.boolean().optional().describe('execute: also generate a POM suite from the run.'),
|
|
43
|
+
explore: z.boolean().optional().describe('execute: run guided exploration after the smoke.'),
|
|
44
|
+
stopOnNeedsInput: z.boolean().optional().describe('execute: ask for login/test data instead of testing pre-login only.'),
|
|
45
|
+
waitForCompletion: z.boolean().optional().describe('execute: block until done (or timeoutMs) and return the terminal result.'),
|
|
46
|
+
timeoutMs: z.number().optional().describe('waitForCompletion cap (default 120000).'),
|
|
81
47
|
consentId: z.string().optional(),
|
|
82
48
|
approve: z.boolean().optional(),
|
|
49
|
+
responseMode: z
|
|
50
|
+
.enum(['compact', 'normal', 'verbose'])
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Text channel for this session: compact = summary + URIs (structuredContent stays full).'),
|
|
83
53
|
},
|
|
84
|
-
}, async (
|
|
85
|
-
// Goal routing (Milestone C): the goal sets orchestration FLAGS only; explicit booleans win.
|
|
86
|
-
// Default policy = "leave behind automation when possible" (fastSmoke opts out).
|
|
87
|
-
const goalFlags = resolveGoalFlags(goal, { explore, generateSuite, stopOnNeedsInput }, { fastSmoke });
|
|
88
|
-
// ---- resolve / create session ----
|
|
89
|
-
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
90
|
-
if (sessionId && !session) {
|
|
91
|
-
return qaError({ what: `Unknown sessionId "${sessionId}"`, changedState: false, retrySafe: true, nextSteps: ['Omit sessionId to create one, or pass a valid session.'] });
|
|
92
|
-
}
|
|
93
|
-
if (!session) {
|
|
94
|
-
const { resolveProjectRoot } = await import('../context/projectRoot.js');
|
|
95
|
-
const resolved = await resolveProjectRoot(server, projectRoot);
|
|
96
|
-
if (!resolved.root)
|
|
97
|
-
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot="/abs/path".'], clientHint: resolved.hint });
|
|
98
|
-
session = sessions.create(resolved.root, undefined, {});
|
|
99
|
-
}
|
|
100
|
-
let root = session.root;
|
|
101
|
-
const wa = (note) => sessions.addWorkaround(session, note);
|
|
102
|
-
// ---- 1. project scan ----
|
|
103
|
-
let scan = await scanProject(root);
|
|
104
|
-
// What Swipium has actually tried before any pre-execute question/blocker (review item 5):
|
|
105
|
-
// concrete steps, not just the workaround log. Grows as resolution proceeds.
|
|
106
|
-
const preAttempted = [`scanned project root (framework=${scan.framework})`];
|
|
107
|
-
// ---- 1b. low-context app discovery (§3.1/§3.7): the root may be a monorepo or a parent
|
|
108
|
-
// directory rather than the app itself. Discover candidates and either adopt the one
|
|
109
|
-
// strong candidate (recording a workaround) or ask ONE disambiguation question. ----
|
|
110
|
-
if (scan.framework === 'unknown') {
|
|
111
|
-
const disc = findMobileApps(root);
|
|
112
|
-
preAttempted.push(`ran low-context app discovery (searched ${disc.searchedLocations.length} location(s), found ${disc.candidates.length} candidate(s))`);
|
|
113
|
-
const cls = classifyDiscovery(disc);
|
|
114
|
-
if (cls.decision === 'adopt' && cls.chosen) {
|
|
115
|
-
wa(`root is not itself a mobile app — discovered one candidate (${cls.chosen.framework}) at ${cls.chosen.rel}; proceeding with it`);
|
|
116
|
-
root = cls.chosen.path;
|
|
117
|
-
session.root = root; // adopt the app subdir as the effective project root
|
|
118
|
-
sessions.persist(session);
|
|
119
|
-
scan = await scanProject(root);
|
|
120
|
-
}
|
|
121
|
-
else if (cls.decision === 'disambiguate') {
|
|
122
|
-
return qaNeedsInput(NeedsInput.monorepoTarget(cls.choices.map((c) => c.path)), {
|
|
123
|
-
sessionId: session.id,
|
|
124
|
-
state: 'needs_input',
|
|
125
|
-
attempted: [...preAttempted, `identified ${cls.choices.length} plausible app targets — cannot pick safely`],
|
|
126
|
-
candidates: cls.choices.slice(0, 6).map((c) => ({ path: c.path, rel: c.rel, framework: c.framework, score: c.score, reasons: c.reasons })),
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
const empty = scan.readiness === 'blocked' && scan.missing.some((m) => /empty/i.test(m));
|
|
131
|
-
return qaFail(empty ? 'PROJECT_ROOT_EMPTY' : 'NOT_MOBILE_PROJECT', {
|
|
132
|
-
what: `No supported mobile project at ${root}`,
|
|
133
|
-
nextSteps: [`Searched: ${disc.searchedLocations.slice(0, 8).join('; ') || '(root only)'}`, 'Pass projectRoot="/abs/path/to/app" pointing at the app directory.'],
|
|
134
|
-
extra: { sessionId: session.id, scan, searchedLocations: disc.searchedLocations, state: 'blocked' },
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
else if (scan.monorepo) {
|
|
139
|
-
// The root is itself an app but also a monorepo: surface other sibling apps for disambiguation
|
|
140
|
-
// only when there are genuinely multiple recognized targets to choose between.
|
|
141
|
-
const disc = findMobileApps(root);
|
|
142
|
-
preAttempted.push(`detected a monorepo at the root; ran app discovery (${disc.candidates.length} candidate(s))`);
|
|
143
|
-
const others = disc.candidates.filter((c) => c.path !== root);
|
|
144
|
-
if (others.length >= 1) {
|
|
145
|
-
return qaNeedsInput(NeedsInput.monorepoTarget([root, ...others.map((c) => c.path)]), {
|
|
146
|
-
sessionId: session.id,
|
|
147
|
-
state: 'needs_input',
|
|
148
|
-
attempted: [...preAttempted, `the root app plus ${others.length} sibling app(s) are all plausible targets`],
|
|
149
|
-
candidates: disc.candidates.slice(0, 6).map((c) => ({ path: c.path, rel: c.rel, framework: c.framework, score: c.score, reasons: c.reasons })),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
wa('monorepo detected — proceeding with the resolved root; pass projectRoot/target to disambiguate if the wrong app is chosen');
|
|
153
|
-
}
|
|
154
|
-
// ---- 1c. pre-launch app map (Vision Gap Fix 1): load or build the durable STATIC app map
|
|
155
|
-
// BEFORE prepare/smoke so first-run decisions, exploration seeding, automation readiness,
|
|
156
|
-
// and the report are informed by the map at the time they are made — and so the map still
|
|
157
|
-
// exists when no device/artifact is available (every blocker below carries appMapUri). ----
|
|
158
|
-
let prelaunchMap;
|
|
159
|
-
let appMapUri;
|
|
160
|
-
try {
|
|
161
|
-
const at = new Date().toISOString();
|
|
162
|
-
const pre = ensurePrelaunchAppMap(root, {
|
|
163
|
-
at,
|
|
164
|
-
appIdentityHints: {
|
|
165
|
-
androidPackage: scan.framework === 'native-android' || scan.framework === 'expo' || scan.framework === 'bare-react-native' ? scan.appId : null,
|
|
166
|
-
iosBundleId: scan.framework === 'native-ios' ? scan.appId : null,
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
prelaunchMap = pre.map;
|
|
170
|
-
appMapUri = pre.appMapUri;
|
|
171
|
-
preAttempted.push(`${pre.rescanned ? 'built' : 'loaded'} static app map (${pre.reason}; ${pre.map.staticTopology.screens.length} static screen(s)) → ${pre.appMapUri}`);
|
|
172
|
-
if (pre.rescanned)
|
|
173
|
-
wa(`built static app map before launch (${pre.map.staticTopology.screens.length} screen(s)) — durable map drives first-run/exploration/report`);
|
|
174
|
-
}
|
|
175
|
-
catch (e) {
|
|
176
|
-
log('warn', 'test_this pre-launch app map failed', { err: String(e) });
|
|
177
|
-
}
|
|
178
|
-
const buildPref = buildIfNeeded !== false;
|
|
179
|
-
const steps = [];
|
|
180
|
-
// ---- 2. artifact ----
|
|
181
|
-
const art = await resolveArtifact({ projectRoot: root, platform: platform ?? 'any', allowOutsideRoot });
|
|
182
|
-
let artifactPlatform = art.best?.platform;
|
|
183
|
-
let needBuild = false;
|
|
184
|
-
if (art.failureCode === 'ARTIFACT_OUTSIDE_ROOT_REQUIRES_APPROVAL') {
|
|
185
|
-
const outside = art.candidates.find((c) => c.outsideRoot);
|
|
186
|
-
return qaNeedsInput(NeedsInput.artifactOutsideRoot(outside?.path ?? '(outside root)'), {
|
|
187
|
-
sessionId: session.id,
|
|
188
|
-
state: 'needs_input',
|
|
189
|
-
attempted: [...preAttempted, 'resolved candidate artifacts — the best one is outside the project root'],
|
|
190
|
-
candidates: art.candidates.slice(0, 3),
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
const isAab = art.best?.type === 'aab';
|
|
194
|
-
if (art.best) {
|
|
195
|
-
steps.push({ tool: '(resolved)', why: `Using ${art.best.type.toUpperCase()} ${art.best.path}`, status: 'satisfied' });
|
|
196
|
-
if (isAab) {
|
|
197
|
-
// Honest (P0.2): an .aab is not installable — it must be converted first.
|
|
198
|
-
wa('only a .aab is present; qa_test_this will convert it to an installable universal APK with bundletool');
|
|
199
|
-
steps.push({ tool: 'qa_test_this', why: 'Convert the .aab to an installable universal APK before install', args: { sessionId: session.id }, status: 'pending' });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
// No artifact. Build from source if possible (effective-before-exhaustive, §2.1).
|
|
204
|
-
const targetPlatform = (platform ?? artifactPlatform ?? (scan.framework === 'native-ios' ? 'ios' : 'android'));
|
|
205
|
-
if (buildPref) {
|
|
206
|
-
const bp = await buildPlan({ projectRoot: root, platform: targetPlatform });
|
|
207
|
-
if (!bp.failureCode && bp.build && bp.toolchainOk) {
|
|
208
|
-
needBuild = true;
|
|
209
|
-
artifactPlatform = targetPlatform;
|
|
210
|
-
const expoAndroidLocalRun = scan.framework === 'expo' && targetPlatform === 'android';
|
|
211
|
-
wa(expoAndroidLocalRun
|
|
212
|
-
? `no prebuilt APK - Expo Android local run selected (${bp.build.command}); first run can take several minutes, later JS/TS-only work should reuse Metro/dev-build caches`
|
|
213
|
-
: `no prebuilt artifact - will build ${targetPlatform} from source (${bp.build.command})`);
|
|
214
|
-
steps.push({
|
|
215
|
-
tool: 'qa_test_this',
|
|
216
|
-
why: expoAndroidLocalRun ? 'No reusable APK/dev build found; run Expo Android local build/install/Metro path' : `No artifact found; build ${targetPlatform} from source before install`,
|
|
217
|
-
args: { sessionId: session.id },
|
|
218
|
-
status: 'pending',
|
|
219
|
-
produces: ['artifact.path', 'artifact.appId'],
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
return qaFail(bp.failureCode ?? 'NO_BUILD_ARTIFACT', {
|
|
224
|
-
what: `No artifact found under ${root}, and a build is not currently possible`,
|
|
225
|
-
nextSteps: [...(bp.missingToolchain.length ? [`Install: ${bp.missingToolchain.join(', ')}`] : []), ...bp.notes, `Searched: ${art.searchedLocations.slice(0, 6).join('; ')}`, ...(appMapUri ? [`Static app map is still available: qa_app_map_read { projectRoot:"${root}" }`] : [])],
|
|
226
|
-
extra: { sessionId: session.id, buildPlan: bp, searchedLocations: art.searchedLocations, appMapUri, state: 'blocked' },
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
return qaFail('NO_BUILD_ARTIFACT', { what: `No artifact found under ${root}`, nextSteps: ['Build the app with your normal build command, or pass an explicit artifact path.', `Searched: ${art.searchedLocations.slice(0, 6).join('; ')}`, ...(appMapUri ? [`Static app map is still available: qa_app_map_read { projectRoot:"${root}" }`] : [])], extra: { sessionId: session.id, searchedLocations: art.searchedLocations, appMapUri, state: 'blocked' } });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// ---- 3. target ----
|
|
235
|
-
// Public v1 is simulator-only. Real-device-only artifacts and explicit real-device requests
|
|
236
|
-
// are blocked instead of silently routing to non-v1 behavior.
|
|
237
|
-
const iosArtifactRealOnly = !!art.best && art.best.platform === 'ios' && Array.isArray(art.best.installableOn) &&
|
|
238
|
-
art.best.installableOn.length > 0 && art.best.installableOn.every((t) => t === 'ios-real');
|
|
239
|
-
if (preferRealDevice || iosArtifactRealOnly) {
|
|
240
|
-
return qaFail('BACKEND_UNSUPPORTED', {
|
|
241
|
-
what: 'Swipium 1.0.0 supports Android Emulator and iOS Simulator targets only.',
|
|
242
|
-
nextSteps: ['Use an Android emulator build or an iOS Simulator .app for v1.', 'Real-device workflows are outside the public v1 scope.'],
|
|
243
|
-
extra: { sessionId: session.id, appMapUri, state: 'blocked' },
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
const [adbPresent, simPresent] = await Promise.all([which('adb'), simctlAvailable()]);
|
|
247
|
-
const [onlineAll, avds, sims] = await Promise.all([
|
|
248
|
-
adbPresent ? adbDevices() : Promise.resolve([]),
|
|
249
|
-
adbPresent ? listAvds() : Promise.resolve([]),
|
|
250
|
-
simPresent ? listSimulators() : Promise.resolve([]),
|
|
251
|
-
]);
|
|
252
|
-
const online = onlineAll.filter(isAndroidEmulatorSerial);
|
|
253
|
-
if (device && onlineAll.includes(device) && !isAndroidEmulatorSerial(device)) {
|
|
254
|
-
return qaFail('BACKEND_UNSUPPORTED', {
|
|
255
|
-
what: `Android target "${device}" appears to be a real device. Swipium 1.0.0 supports Android Emulator only.`,
|
|
256
|
-
nextSteps: ['Start or create an Android Emulator, then retry without a real-device serial.'],
|
|
257
|
-
extra: { sessionId: session.id, appMapUri, state: 'blocked' },
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
const tInputs = {
|
|
261
|
-
requestedPlatform: platform,
|
|
262
|
-
requestedDevice: device,
|
|
263
|
-
preferRealDevice: false,
|
|
264
|
-
artifactPlatform,
|
|
265
|
-
artifactInstallTargets: art.best?.installableOn,
|
|
266
|
-
android: { online, avds },
|
|
267
|
-
ios: {
|
|
268
|
-
bootedSimulators: sims.filter((s) => s.state === 'Booted').map((s) => ({ udid: s.udid, name: s.name })),
|
|
269
|
-
availableSimulators: sims.filter((s) => s.state !== 'Booted').map((s) => ({ udid: s.udid, name: s.name })),
|
|
270
|
-
realDevices: [],
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
const target = planTarget(tInputs);
|
|
274
|
-
if (target.blocked) {
|
|
275
|
-
return qaFail(target.blocked.failureCode, { what: target.blocked.detail, extra: { sessionId: session.id, targetPlan: target, workaroundsAttempted: session.workarounds, appMapUri, state: 'blocked' } });
|
|
276
|
-
}
|
|
277
|
-
if (target.willBoot)
|
|
278
|
-
wa(`no online ${selToPlatform[target.selected]} target — will boot ${target.bootTarget}`);
|
|
279
|
-
const isAndroid = selToPlatform[target.selected] === 'android';
|
|
280
|
-
const isIosReal = false;
|
|
281
|
-
const requiresStructuredIos = !isAndroid &&
|
|
282
|
-
!isIosReal &&
|
|
283
|
-
(goalFlags.explore || goalFlags.generateSuite || goalFlags.goal === 'test_login' || goalFlags.goal === 'create_automation_suite');
|
|
284
|
-
if (requiresStructuredIos) {
|
|
285
|
-
const wdaConfig = loadWdaConfig(root);
|
|
286
|
-
const wda = await checkWda(wdaConfig.url, 1500);
|
|
287
|
-
if (!wda.reachable || !wda.ready) {
|
|
288
|
-
return qaFail('WDA_UNREACHABLE', {
|
|
289
|
-
what: `iOS ${goalFlags.goal} requires a structured WebDriverAgent backend, but WDA is not ready at ${wdaConfig.url}`,
|
|
290
|
-
nextSteps: [
|
|
291
|
-
'Run qa_wda { action:"doctor" } to inspect WDA setup.',
|
|
292
|
-
'Run qa_wda { action:"build" } and qa_wda { action:"start" }, or attach an external WDA URL.',
|
|
293
|
-
...(appMapUri ? [`Static app map is still available: qa_app_map_read { projectRoot:"${root}" }`] : []),
|
|
294
|
-
],
|
|
295
|
-
extra: {
|
|
296
|
-
sessionId: session.id,
|
|
297
|
-
state: 'blocked',
|
|
298
|
-
goal: goalFlags.goal,
|
|
299
|
-
target,
|
|
300
|
-
wda: { config: { url: wdaConfig.url, mode: wdaConfig.mode, derivedDataPath: wdaConfig.derivedDataPath }, status: wda },
|
|
301
|
-
appMapUri,
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// ---- 4. auth (be effective: proceed pre-login; surface the question as optional) ----
|
|
307
|
-
let optionalQuestion;
|
|
308
|
-
if (scan.likelyAuth) {
|
|
309
|
-
wa('app likely has a login — will test pre-login coverage; authenticated flows are blocked until credentials are provided');
|
|
310
|
-
optionalQuestion = NeedsInput.credentials('Authenticated workflows need a test account.');
|
|
311
|
-
}
|
|
312
|
-
// ---- 5. assemble the ordered plan ----
|
|
313
|
-
// Effective installable artifact path: a converted universal APK for an .aab, else the artifact.
|
|
314
|
-
const effectiveApk = art.best ? (isAab ? universalApkCachePath(root, art.best.path) : art.best.path) : undefined;
|
|
315
|
-
// Milestone E: when a build is pending, the artifact does not exist yet — use SYMBOLIC refs
|
|
316
|
-
// (${artifact.path}/${artifact.appId}) and mark the dependency, never `undefined` args.
|
|
317
|
-
const prepArgs = { sessionId: session.id };
|
|
318
|
-
if (isAndroid) {
|
|
319
|
-
prepArgs.apk = needBuild ? '${artifact.path}' : effectiveApk;
|
|
320
|
-
if (target.device)
|
|
321
|
-
prepArgs.device = target.device;
|
|
322
|
-
if (target.willBoot && target.bootTarget)
|
|
323
|
-
prepArgs.avd = target.bootTarget;
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
prepArgs.app = needBuild ? '${artifact.path}' : art.best?.path;
|
|
327
|
-
prepArgs.bundleId = needBuild ? '${artifact.appId}' : art.best?.appId ?? undefined;
|
|
328
|
-
prepArgs.simulator = target.device;
|
|
329
|
-
prepArgs.attachWda = 'auto';
|
|
330
|
-
if (target.device)
|
|
331
|
-
prepArgs.device = target.device;
|
|
332
|
-
}
|
|
333
|
-
if (isAndroid && scan.metroNeed === 'likely')
|
|
334
|
-
wa('debug RN/Expo build likely needs Metro/dev server serving before launch');
|
|
335
|
-
steps.push({
|
|
336
|
-
tool: isAndroid ? 'qa_prepare_target' : 'qa_prepare_ios_target',
|
|
337
|
-
why: `Install + launch on ${target.selected}`,
|
|
338
|
-
args: prepArgs,
|
|
339
|
-
status: 'pending',
|
|
340
|
-
...(needBuild ? { requires: ['artifact.path', 'artifact.appId'] } : {}),
|
|
341
|
-
});
|
|
342
|
-
steps.push({ tool: 'qa_smoke', why: 'Run a guardrail/smoke pass and capture evidence', args: { sessionId: session.id, profile: scan.recommendedProfile }, status: 'pending' });
|
|
343
|
-
steps.push({ tool: 'qa_report', why: 'Summarize what was tested/blocked with evidence', args: { sessionId: session.id }, status: 'pending' });
|
|
344
|
-
steps.push({ tool: 'qa_suite_generate', why: 'Optional: turn this run into a durable POM suite', args: { sessionId: session.id }, status: 'pending' });
|
|
345
|
-
const nextAction = steps.find((s) => s.status === 'pending') ?? null;
|
|
346
|
-
const state = 'ready';
|
|
347
|
-
// Choice explanations for the uniform envelope (Dev 2 owns the source data; Dev 1 the shape).
|
|
348
|
-
const artifactChoice = art.best
|
|
349
|
-
? { path: art.best.path, why: `${art.best.type.toUpperCase()} selected${art.best.buildType ? ` (${art.best.buildType})` : ''}`, alternatives: art.candidates.filter((c) => c.path !== art.best.path).slice(0, 4).map((c) => c.path) }
|
|
350
|
-
: needBuild
|
|
351
|
-
? { why: 'No prebuilt artifact — building from source', alternatives: [] }
|
|
352
|
-
: null;
|
|
353
|
-
const targetChoice = target.selected
|
|
354
|
-
? { target: `${target.selected}${target.device ? ` (${target.device})` : ''}`, why: target.reason, alternatives: [] }
|
|
355
|
-
: null;
|
|
356
|
-
// ---- EXECUTE / INTERACTIVE modes (P0.1): actually run the path as a job ----
|
|
357
|
-
if (mode === 'execute' || mode === 'interactive') {
|
|
358
|
-
return runExecuteMode(server, sessions, session, {
|
|
359
|
-
mode, scan, art, target, isAndroid, isIosReal, isAab, needBuild, effectiveApk,
|
|
360
|
-
platform, goal: goalFlags.goal, goalText, releaseGate: goalFlags.releaseGate, requiredOutputs: goalFlags.requiredOutputs,
|
|
361
|
-
generateSuite: goalFlags.generateSuite, explore: goalFlags.explore, stopOnNeedsInput: goalFlags.stopOnNeedsInput,
|
|
362
|
-
artifactChoice, targetChoice,
|
|
363
|
-
waitForCompletion: !!waitForCompletion, timeoutMs,
|
|
364
|
-
consentId, approve, optionalQuestion, workaroundLog: () => session.workarounds,
|
|
365
|
-
prelaunchMap, appMapUri,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
const summary = `🚀 test-this plan for ${root} (framework=${scan.framework}, session ${session.id})\n` +
|
|
369
|
-
`target: ${target.selected}${target.device ? ` (${target.device})` : ''} — ${target.reason}\n` +
|
|
370
|
-
(art.best ? `artifact: ${art.best.type.toUpperCase()} ${art.best.path}` : `artifact: none — ${needBuild ? 'will build from source' : 'blocked'}`) + '\n' +
|
|
371
|
-
`plan:\n${steps.map((s, i) => ` ${i + 1}. [${s.status === 'satisfied' ? '✓' : ' '}] ${s.tool} — ${s.why}`).join('\n')}\n` +
|
|
372
|
-
(nextAction ? `→ next: call ${nextAction.tool} ${JSON.stringify(nextAction.args ?? {})}\n` : '') +
|
|
373
|
-
(session.workarounds.length ? `workarounds: ${session.workarounds.length} (see workaroundsAttempted)\n` : '') +
|
|
374
|
-
(optionalQuestion ? `note: ${optionalQuestion.question}` : '');
|
|
375
|
-
return qaOk({
|
|
376
|
-
sessionId: session.id,
|
|
377
|
-
state,
|
|
378
|
-
goal: goalFlags.goal,
|
|
379
|
-
framework: scan.framework,
|
|
380
|
-
appMapUri,
|
|
381
|
-
appIdentity: prelaunchMap?.appIdentity,
|
|
382
|
-
target,
|
|
383
|
-
targetChoice,
|
|
384
|
-
artifact: art.best ?? null,
|
|
385
|
-
artifactChoice,
|
|
386
|
-
needBuild,
|
|
387
|
-
recommendedProfile: scan.recommendedProfile,
|
|
388
|
-
plan: steps,
|
|
389
|
-
nextAction,
|
|
390
|
-
workaroundsAttempted: session.workarounds,
|
|
391
|
-
optionalQuestion: optionalQuestion ?? null,
|
|
392
|
-
searchedLocations: art.best ? undefined : art.searchedLocations,
|
|
393
|
-
}, summary);
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
/** Whether the secure store already has login credentials (so we don't re-ask). */
|
|
397
|
-
function hasCredentials(session) {
|
|
398
|
-
return session.inputs.some((i) => /EMAIL|PASSWORD/.test(i.varName));
|
|
399
|
-
}
|
|
400
|
-
/** Execute / interactive orchestration: handle questions + consent synchronously, then run the
|
|
401
|
-
* build/convert → prepare → smoke → report pipeline as a job. */
|
|
402
|
-
async function runExecuteMode(server, sessions, session, a) {
|
|
403
|
-
// 1. Auth question — interactive (or stopOnNeedsInput / goal:test_login) asks; execute proceeds pre-login.
|
|
404
|
-
if (a.scan.likelyAuth && !hasCredentials(session) && (a.mode === 'interactive' || a.stopOnNeedsInput)) {
|
|
405
|
-
const attempted = [
|
|
406
|
-
`scanned project (framework=${a.scan.framework}; detected likely auth: ${a.scan.authSignals.slice(0, 3).join(', ') || 'login UI'})`,
|
|
407
|
-
`resolved artifact + target (${a.target.selected ?? 'unknown'})`,
|
|
408
|
-
...session.workarounds,
|
|
409
|
-
];
|
|
410
|
-
return qaNeedsInput(NeedsInput.credentials('Authenticated workflows need a test account.'), { sessionId: session.id, state: 'needs_input', attempted, appMapUri: a.appMapUri, resumeWith: 'qa_continue_from_blocker' });
|
|
411
|
-
}
|
|
412
|
-
// 2. Unified execution preflight (Milestone A): execute mode must request the SAME consent the
|
|
413
|
-
// lower-level tools would for boot / external-APK / iOS .app install / build-from-source.
|
|
414
|
-
const buildPlatform = (a.platform ?? (a.scan.framework === 'native-ios' ? 'ios' : 'android'));
|
|
415
|
-
// Resolve the EXACT build command so the consent shows what will run (not just "build_from_source").
|
|
416
|
-
let buildCommand;
|
|
417
|
-
if (a.needBuild) {
|
|
418
|
-
const bp = await buildPlan({ projectRoot: session.root, platform: buildPlatform });
|
|
419
|
-
buildCommand = bp.build?.command;
|
|
420
|
-
}
|
|
421
|
-
// Hash an external (outside-root) APK so the consent shows what is being installed.
|
|
422
|
-
let externalApk;
|
|
423
|
-
if (a.isAndroid && !a.needBuild && a.effectiveApk && existsSync(a.effectiveApk) && !a.effectiveApk.startsWith(session.root + sep)) {
|
|
424
|
-
try {
|
|
425
|
-
externalApk = { path: a.effectiveApk, sha256: createHash('sha256').update(readFileSync(a.effectiveApk)).digest('hex') };
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
/* unreadable — treated as in-root install */
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const iosApp = !a.isAndroid && !a.isIosReal ? a.art.best?.path : undefined;
|
|
432
|
-
const preflight = buildTestThisPreflight({
|
|
433
|
-
isAndroid: a.isAndroid,
|
|
434
|
-
needBuild: a.needBuild,
|
|
435
|
-
buildPlatform,
|
|
436
|
-
buildCommand,
|
|
437
|
-
willBoot: a.target.willBoot,
|
|
438
|
-
bootTarget: a.target.bootTarget,
|
|
439
|
-
isAab: a.isAab,
|
|
440
|
-
apkPath: a.isAndroid ? a.effectiveApk : undefined,
|
|
441
|
-
externalApk,
|
|
442
|
-
iosApp,
|
|
443
|
-
iosAppOutsideRoot: iosApp ? !iosApp.startsWith(session.root + sep) : undefined,
|
|
444
|
-
iosReal: a.isIosReal,
|
|
445
|
-
iosRealUdid: a.isIosReal ? a.target.device : undefined,
|
|
446
|
-
iosRealApp: a.isIosReal ? a.art.best?.path : undefined,
|
|
447
|
-
});
|
|
448
|
-
let mutationConsent;
|
|
449
|
-
let testThisPlanMutation;
|
|
450
|
-
if (preflight.consentRequired) {
|
|
451
|
-
const gate = consumeConsent(a.consentId, a.approve, { action: 'test_this_plan', affects: preflight.consentAffects });
|
|
452
|
-
if (!gate.approved) {
|
|
453
|
-
sessions.recordMutation(session, {
|
|
454
|
-
tool: 'qa_test_this',
|
|
455
|
-
action: 'test_this_plan',
|
|
456
|
-
risk: preflight.risk,
|
|
457
|
-
target: preflight.consentAffects,
|
|
458
|
-
consent: { required: true, approved: false },
|
|
459
|
-
status: 'requested',
|
|
460
|
-
});
|
|
461
|
-
const consentResult = requireConsent({
|
|
462
|
-
action: 'test_this_plan',
|
|
463
|
-
risk: preflight.risk,
|
|
464
|
-
exactCommand: preflight.exactCommand,
|
|
465
|
-
affects: preflight.consentAffects,
|
|
466
|
-
explain: `Running "test this" needs these privileged steps (approved together so they don't re-prompt):\n${preflight.exactCommand}\nApprove to start; Swipium then installs, smokes, reports${a.generateSuite ? ', and generates a suite' : ''}.`,
|
|
467
|
-
});
|
|
468
|
-
// The static app map was already built pre-launch — keep its URI on the consent result (Fix 1).
|
|
469
|
-
if (a.appMapUri && consentResult.structuredContent) {
|
|
470
|
-
consentResult.structuredContent = { ...consentResult.structuredContent, appMapUri: a.appMapUri };
|
|
471
|
-
}
|
|
472
|
-
return consentResult;
|
|
473
|
-
}
|
|
474
|
-
mutationConsent = { required: true, consentId: a.consentId, approved: true };
|
|
475
|
-
testThisPlanMutation = { affects: preflight.consentAffects, risk: preflight.risk };
|
|
476
|
-
sessions.recordMutation(session, {
|
|
477
|
-
tool: 'qa_test_this',
|
|
478
|
-
action: 'test_this_plan',
|
|
479
|
-
risk: preflight.risk,
|
|
480
|
-
target: preflight.consentAffects,
|
|
481
|
-
consent: mutationConsent,
|
|
482
|
-
status: 'approved',
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
const job = sessions.createJob(session, `test_this:${a.mode}`);
|
|
486
|
-
const execArgs = { ...a, mutationConsent, testThisPlanMutation };
|
|
487
|
-
const run = runExecutePipeline(sessions, session, job, execArgs);
|
|
488
|
-
// Optional blocking mode for short paths (Milestone D). Default = return the running job.
|
|
489
|
-
if (a.waitForCompletion) {
|
|
490
|
-
const deadline = Date.now() + (a.timeoutMs ?? 120_000);
|
|
491
|
-
await Promise.race([run, new Promise((r) => setTimeout(r, Math.max(0, deadline - Date.now())))]);
|
|
492
|
-
const cur = session.jobs.get(job.jobId);
|
|
493
|
-
if (cur && cur.status !== 'running') {
|
|
494
|
-
const res = (cur.result ?? {});
|
|
495
|
-
return qaOk({ sessionId: session.id, mode: a.mode, jobId: job.jobId, appMapUri: a.appMapUri, ...res }, cur.resultText ?? `test-this ${a.mode} ${res.state ?? cur.status}.`);
|
|
496
|
-
}
|
|
497
|
-
// Timed out — leave the job running and tell the agent to poll.
|
|
498
|
-
return qaOk({ sessionId: session.id, state: 'running', mode: a.mode, jobId: job.jobId, appMapUri: a.appMapUri, timedOutWaiting: true }, `⏳ test-this ${a.mode} still running after the wait window — poll qa_job_status { sessionId:"${session.id}", jobId:"${job.jobId}" }.`);
|
|
499
|
-
}
|
|
500
|
-
void run;
|
|
501
|
-
return qaOk({ sessionId: session.id, state: 'running', mode: a.mode, jobId: job.jobId, kind: job.kind, appMapUri: a.appMapUri, target: a.target }, `🚀 test-this ${a.mode} started as job ${job.jobId} (${a.isAndroid ? 'Android' : 'iOS'} · ${a.target.selected}). Poll qa_job_status { sessionId:"${session.id}", jobId:"${job.jobId}" } for the terminal result (state: completed | blocked | unsafe).`);
|
|
502
|
-
}
|
|
503
|
-
async function runExecutePipeline(sessions, session, job, a) {
|
|
504
|
-
const signal = sessions.abortSignal(session, job.jobId);
|
|
505
|
-
const upd = (patch) => sessions.updateJobIfRunning(session, job, patch);
|
|
506
|
-
const attempted = [];
|
|
507
|
-
const artifacts = [];
|
|
508
|
-
const firstRunPatches = []; // first-run classifications folded into the durable map (§D)
|
|
509
|
-
// Suite outputs (set in the terminal section) are embedded in the report, not just the job result.
|
|
510
|
-
let suiteForReport;
|
|
511
|
-
// Every terminal state — completed OR blocked — generates a report artifact (Milestone B).
|
|
512
|
-
const finish = async (state, failureCode, summary, extra = {}) => {
|
|
513
|
-
if (a.testThisPlanMutation) {
|
|
514
|
-
sessions.recordMutation(session, {
|
|
515
|
-
tool: 'qa_test_this',
|
|
516
|
-
action: 'test_this_plan',
|
|
517
|
-
risk: a.testThisPlanMutation.risk,
|
|
518
|
-
target: a.testThisPlanMutation.affects,
|
|
519
|
-
consent: a.mutationConsent,
|
|
520
|
-
status: state === 'completed' ? 'executed' : 'blocked',
|
|
521
|
-
detail: failureCode,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
const reportProg = startProgress(sessions, session, job, 'reporting', { statusText: 'Generating the session report.' });
|
|
525
|
-
let reportUri;
|
|
526
|
-
let manifestUri;
|
|
527
|
-
let appVerdict;
|
|
528
|
-
let coverageVerdict;
|
|
529
|
-
let toolVerdict;
|
|
530
|
-
const suiteOpt = suiteForReport
|
|
531
|
-
? { generated: !suiteForReport.skipped, skippedReason: suiteForReport.skippedReason, name: suiteForReport.name, written: suiteForReport.written, compiledFlows: suiteForReport.compiledFlows, suiteRunnable: suiteForReport.suiteRunnable, readinessLabels: suiteForReport.readinessLabels }
|
|
532
|
-
: undefined;
|
|
533
|
-
try {
|
|
534
|
-
const r = await generateSessionReport(sessions, session, {
|
|
535
|
-
format: 'summary', includeCurrentDump: true, suite: suiteOpt,
|
|
536
|
-
});
|
|
537
|
-
reportUri = r.reportUri;
|
|
538
|
-
manifestUri = r.manifestUri;
|
|
539
|
-
if (!artifacts.includes(reportUri))
|
|
540
|
-
artifacts.push(reportUri);
|
|
541
|
-
appVerdict = r.report.appVerdict;
|
|
542
|
-
coverageVerdict = r.report.coverageVerdict;
|
|
543
|
-
toolVerdict = r.report.toolVerdict;
|
|
544
|
-
}
|
|
545
|
-
catch (e) {
|
|
546
|
-
log('warn', 'test_this report generation failed', { err: String(e) });
|
|
547
|
-
}
|
|
548
|
-
reportProg.done('Report ready.');
|
|
549
|
-
const native = session.findings.some((f) => f.layer === 'native' && f.severity === 'high') ? 'error' : 'OK';
|
|
550
|
-
const app = session.findings.some((f) => f.layer === 'app' && f.severity === 'high') ? 'error' : session.findings.some((f) => f.layer === 'app' && f.severity === 'medium') ? 'degraded' : 'OK';
|
|
551
|
-
// The report already exists — point the agent AT it (fetch/open), not back at qa_report.
|
|
552
|
-
const nextRecommendedAction = state === 'completed'
|
|
553
|
-
? reportUri
|
|
554
|
-
? {
|
|
555
|
-
tool: 'qa_get_artifact',
|
|
556
|
-
args: { uri: reportUri },
|
|
557
|
-
why: `Open the generated report (${reportUri})`,
|
|
558
|
-
}
|
|
559
|
-
: { tool: 'qa_report', args: { sessionId: session.id }, why: 'Generate the report' }
|
|
560
|
-
: { tool: 'qa_explain_blocker', args: { failureCode: failureCode ?? 'UNKNOWN' }, why: 'Understand the blocker and how to fix it' };
|
|
561
|
-
// Uniform terminal envelope (Milestone B): summarizable from structured output, no log parsing.
|
|
562
|
-
const envelope = buildTerminalEnvelope({
|
|
563
|
-
state,
|
|
564
|
-
sessionId: session.id,
|
|
565
|
-
jobId: job.jobId,
|
|
566
|
-
summary,
|
|
567
|
-
attempted,
|
|
568
|
-
workaroundsAttempted: session.workarounds,
|
|
569
|
-
artifactChoice: a.artifactChoice,
|
|
570
|
-
targetChoice: a.targetChoice,
|
|
571
|
-
verdicts: { ...(appVerdict ? { app: appVerdict } : {}), ...(coverageVerdict ? { coverage: coverageVerdict } : {}), ...(toolVerdict ? { tool: toolVerdict } : {}) },
|
|
572
|
-
blockers: state === 'completed' || !failureCode ? [] : [typedBlockerFromCode(failureCode)],
|
|
573
|
-
reportUri: reportUri ?? null,
|
|
574
|
-
nextRecommendedAction,
|
|
575
|
-
});
|
|
576
|
-
upd({
|
|
577
|
-
status: state === 'completed' ? 'done' : 'failed',
|
|
578
|
-
progress: state,
|
|
579
|
-
result: {
|
|
580
|
-
...envelope,
|
|
581
|
-
// Pre-launch static app map URI is present in EVERY terminal state — including blocked (Fix 1).
|
|
582
|
-
appMapUri: a.appMapUri,
|
|
583
|
-
goal: a.goal, requiredOutputs: a.requiredOutputs, releaseGateRequested: a.releaseGate,
|
|
584
|
-
failureCode, artifacts, manifestUri,
|
|
585
|
-
health: { native, app },
|
|
586
|
-
inputsProvided: session.inputs.map((i) => i.varName),
|
|
587
|
-
notes: session.notes.length, findings: session.findings.length, ...extra,
|
|
588
|
-
},
|
|
589
|
-
resultText: summary + (reportUri ? `\nreport: ${reportUri}` : ''),
|
|
590
|
-
endedAt: Date.now(),
|
|
591
|
-
});
|
|
592
|
-
};
|
|
593
|
-
try {
|
|
594
|
-
// ---- A. obtain an installable artifact ----
|
|
595
|
-
let apkPath = a.isAndroid ? a.effectiveApk : undefined;
|
|
596
|
-
let appPath = !a.isAndroid ? a.art.best?.path : undefined;
|
|
597
|
-
let appId = a.art.best?.appId ?? a.scan.appId ?? undefined;
|
|
598
|
-
if (a.needBuild) {
|
|
599
|
-
const prog = startProgress(sessions, session, job, a.isAndroid ? 'building_android' : 'building_ios', { statusText: 'Building the app from source.', nextExpected: 'Resolve the produced artifact, then install.' });
|
|
600
|
-
attempted.push('build from source');
|
|
601
|
-
const targetPlatform = (a.platform ?? (a.scan.framework === 'native-ios' ? 'ios' : 'android'));
|
|
602
|
-
const plan = await buildPlan({ projectRoot: session.root, platform: targetPlatform });
|
|
603
|
-
const res = await executeBuild(sessions, session, plan, { signal, onProgress: (p) => prog.event(p, { statusText: `Building: ${p}` }) });
|
|
604
|
-
if (res.logUri)
|
|
605
|
-
artifacts.push(res.logUri);
|
|
606
|
-
if (signal?.aborted)
|
|
607
|
-
return;
|
|
608
|
-
if (!res.ok) {
|
|
609
|
-
const code = res.failureCode ?? 'BUILD_FAILED';
|
|
610
|
-
return await finish('blocked', code, `Build failed (${code}). A build failure is not a test failure. Log: ${res.logUri ?? 'n/a'}`, { buildLog: res.logUri });
|
|
611
|
-
}
|
|
612
|
-
if (!res.artifact) {
|
|
613
|
-
const code = res.failureCode ?? 'BUILD_ARTIFACT_UNRESOLVED_AFTER_SUCCESS';
|
|
614
|
-
const searched = res.searchedLocations?.length ? ` Searched: ${res.searchedLocations.slice(0, 6).join(', ')}` : '';
|
|
615
|
-
return await finish('blocked', code, `Build succeeded, but Swipium could not resolve the produced ${a.isAndroid ? 'APK' : 'simulator app'}. This is not an app build failure. Log: ${res.logUri ?? 'n/a'}.${searched}`, { buildLog: res.logUri });
|
|
616
|
-
}
|
|
617
|
-
prog.done('Build complete.');
|
|
618
|
-
if (a.isAndroid)
|
|
619
|
-
apkPath = res.artifact.path;
|
|
620
|
-
else
|
|
621
|
-
appPath = res.artifact.path;
|
|
622
|
-
appId = res.artifact.appId ?? appId;
|
|
623
|
-
}
|
|
624
|
-
else if (a.isAab && a.art.best) {
|
|
625
|
-
const prog = startProgress(sessions, session, job, 'converting_aab', { statusText: 'Converting .aab → universal APK (bundletool).' });
|
|
626
|
-
attempted.push('aab → apk conversion');
|
|
627
|
-
const conv = await convertAabToApk(a.art.best.path, session.root, { signal });
|
|
628
|
-
if (signal?.aborted)
|
|
629
|
-
return;
|
|
630
|
-
if (!conv.ok) {
|
|
631
|
-
return await finish('blocked', conv.failureCode ?? 'AAB_NEEDS_BUNDLETOOL', `❌ Could not convert the .aab (${conv.failureCode}): ${conv.error}. Install bundletool or build an APK directly.`);
|
|
632
|
-
}
|
|
633
|
-
prog.done('Universal APK ready.');
|
|
634
|
-
apkPath = conv.apkPath;
|
|
635
|
-
sessions.addWorkaround(session, 'converted .aab → universal .apk for install');
|
|
636
|
-
}
|
|
637
|
-
// ---- B. prepare (boot/install/launch) ----
|
|
638
|
-
if (a.isAndroid) {
|
|
639
|
-
if (!apkPath)
|
|
640
|
-
return await finish('blocked', 'NO_BUILD_ARTIFACT', '❌ No installable APK to prepare.');
|
|
641
|
-
if (!appId)
|
|
642
|
-
appId = (await apkPackageId(apkPath)) ?? undefined;
|
|
643
|
-
if (!appId)
|
|
644
|
-
return await finish('blocked', 'BUNDLE_ID_NOT_FOUND', '❌ Could not determine the app id from the APK.');
|
|
645
|
-
const rnDebug = a.scan.metroNeed === 'likely';
|
|
646
|
-
const prog = startProgress(sessions, session, job, 'preparing_android', { statusText: `Installing + launching ${appId}.`, nextExpected: 'Run smoke.' });
|
|
647
|
-
attempted.push('prepare android (boot/install/launch)');
|
|
648
|
-
const driver = session.driver ?? new DirectDriver();
|
|
649
|
-
driver.setSignal?.(signal);
|
|
650
|
-
const res = await prepareAndroid(sessions, session, driver, {
|
|
651
|
-
needBoot: a.target.willBoot, bootTarget: a.target.bootTarget, serial: a.target.device,
|
|
652
|
-
resolvedAppId: appId, apkPath, rnDebug, allowLaunchWithoutMetro: false, mutationConsent: a.mutationConsent,
|
|
653
|
-
}, { signal, onProgress: (p) => prog.event(p) });
|
|
654
|
-
if (res.aborted)
|
|
655
|
-
return;
|
|
656
|
-
if (!res.ok) {
|
|
657
|
-
if (res.failureCode === 'METRO_REQUIRED')
|
|
658
|
-
prog.needsUser('Metro must be serving for this debug build.');
|
|
659
|
-
return await finish('blocked', res.failureCode ?? 'APP_LAUNCH_FAILED', `❌ Prepare failed: ${res.error}`);
|
|
660
|
-
}
|
|
661
|
-
prog.done(res.resultText ?? 'App launched.');
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
const prog = startProgress(sessions, session, job, 'preparing_ios', { statusText: 'Booting simulator + installing/launching .app.', nextExpected: 'Run smoke.' });
|
|
665
|
-
attempted.push('prepare ios (boot/install/launch)');
|
|
666
|
-
const res = await prepareIos(sessions, session, { app: appPath, bundleId: appId, simulator: a.target.device, attachWda: 'auto', mutationConsent: a.mutationConsent }, { onProgress: (p) => prog.event(p) });
|
|
667
|
-
if (!res.ok)
|
|
668
|
-
return await finish('blocked', res.failureCode ?? 'APP_LAUNCH_FAILED', `❌ iOS prepare failed: ${res.error}`);
|
|
669
|
-
prog.done(res.resultText ?? 'iOS app launched.');
|
|
670
|
-
}
|
|
671
|
-
if (signal?.aborted)
|
|
672
|
-
return;
|
|
673
|
-
// ---- C. smoke ----
|
|
674
|
-
const smokeProg = startProgress(sessions, session, job, 'smoke', { statusText: 'Running the smoke pass + saved flows.' });
|
|
675
|
-
attempted.push('smoke');
|
|
676
|
-
const driver = session.driver;
|
|
677
|
-
if (!driver)
|
|
678
|
-
return await finish('blocked', 'NO_DEVICE', '❌ No driver bound after prepare.');
|
|
679
|
-
const smoke = await runSmoke(sessions, session, driver, { variables: sessions.inputVariables(session) });
|
|
680
|
-
for (const art of session.artifacts)
|
|
681
|
-
if (!artifacts.includes(art.uri))
|
|
682
|
-
artifacts.push(art.uri);
|
|
683
|
-
smokeProg.done(`smoke done — flows ${smoke.flowsPassed}/${smoke.flowsTotal}.`);
|
|
684
|
-
// ---- C1.5 first-run autonomy (SWIPIUM-REQ-02): when the app appears gated AND the environment
|
|
685
|
-
// is a disposable test/staging one where generated accounts are policy-safe, progress
|
|
686
|
-
// through auth/onboarding before exploring. Safe-by-default: in an unknown/production-like
|
|
687
|
-
// environment the decision is "not allowed" and this block is skipped (pre-login coverage
|
|
688
|
-
// stands, and the optional credentials question is still surfaced). ----
|
|
689
|
-
if (smoke.baseline.launch?.outcome !== 'fail') {
|
|
690
|
-
try {
|
|
691
|
-
// Fix Group 5: trigger first-run from the ACTUAL first runtime screen, not only the static
|
|
692
|
-
// auth signal. Classify the current screen read-only; if it is auth/onboarding/paywall/
|
|
693
|
-
// permission/verification, route through first-run logic (which itself refuses generated
|
|
694
|
-
// account creation in unknown/production-like environments and surfaces NeedsInput instead).
|
|
695
|
-
const { policy, decision } = resolveFirstRunPolicy(session);
|
|
696
|
-
let runtimePurpose;
|
|
697
|
-
try {
|
|
698
|
-
const obs = await observeScreen(sessions, session, driver);
|
|
699
|
-
// Fix 2: corroborate the observed first screen with static app-map candidates.
|
|
700
|
-
const staticCandidates = a.prelaunchMap ? staticCandidatesForObservation(a.prelaunchMap, { foreground: obs.foreground, visibleText: obs.visibleText }) : undefined;
|
|
701
|
-
runtimePurpose = planFirstRun({ ...obs, screenSignature: obs.screenSignature, appError: obs.appError, staticCandidates }, session, { policy, decision }).classification.purpose;
|
|
702
|
-
}
|
|
703
|
-
catch {
|
|
704
|
-
/* classification best-effort; fall back to the static signal */
|
|
705
|
-
}
|
|
706
|
-
const shouldFirstRun = a.scan.likelyAuth || (runtimePurpose != null && FIRST_RUN_TRIGGER_PURPOSES.has(runtimePurpose));
|
|
707
|
-
if (shouldFirstRun) {
|
|
708
|
-
const frProg = startProgress(sessions, session, job, 'first_run', { statusText: 'First-run autonomy: auth/onboarding with safe test data.' });
|
|
709
|
-
attempted.push(`first-run autonomy (trigger: ${a.scan.likelyAuth ? 'static_auth' : `runtime_${runtimePurpose}`})`);
|
|
710
|
-
const fr = await runFirstRun(sessions, session, driver, { mode: 'until_home', appMap: a.prelaunchMap }, { signal, onProgress: (p) => frProg.event(p) });
|
|
711
|
-
for (const u of fr.evidenceUris)
|
|
712
|
-
if (!artifacts.includes(u))
|
|
713
|
-
artifacts.push(u);
|
|
714
|
-
firstRunPatches.push(...fr.mapUpdates); // folded into the durable app map in §D
|
|
715
|
-
sessions.addWorkaround(session, `first-run: ${fr.pathTaken} path → ${fr.accountOutcome} (${fr.environment.environment} env)`);
|
|
716
|
-
frProg.done(`first-run ${fr.state} — account ${fr.accountOutcome}`);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
catch (e) {
|
|
720
|
-
log('warn', 'test_this first-run autonomy failed', { jobId: job.jobId, err: String(e) });
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
// ---- C2. optional guided exploration (§9.1) ----
|
|
724
|
-
if (a.explore && smoke.baseline.launch?.outcome !== 'fail') {
|
|
725
|
-
const exProg = startProgress(sessions, session, job, 'exploring', { statusText: 'Guided exploration of reachable screens.' });
|
|
726
|
-
attempted.push('guided exploration');
|
|
727
|
-
try {
|
|
728
|
-
const ex = await runExplore(sessions, session, driver, { stopOnAuth: a.stopOnNeedsInput, goal: a.goalText }, { signal, onProgress: (p) => exProg.event(p) });
|
|
729
|
-
const at = Date.now();
|
|
730
|
-
const graphUri = sessions.saveArtifact(session, 'explore', `graph-${at}.json`, JSON.stringify(ex.graph.serialize(new Date(at).toISOString()), null, 2), 'application/json', 'exploration screen graph');
|
|
731
|
-
const graphMdUri = sessions.saveArtifact(session, 'explore', `graph-${at}.md`, ex.graph.toMarkdown(new Date(at).toISOString()), 'text/markdown', 'exploration screen graph (readable)');
|
|
732
|
-
sessions.setExploration(session, { at, graphUri, graphMdUri, state: ex.state, stoppedReason: ex.stoppedReason, summary: ex.summary });
|
|
733
|
-
if (!artifacts.includes(graphUri))
|
|
734
|
-
artifacts.push(graphUri);
|
|
735
|
-
exProg.done(`explored ${ex.summary.screensVisited} screens`);
|
|
736
|
-
}
|
|
737
|
-
catch (e) {
|
|
738
|
-
exProg.done(`exploration error: ${String(e)}`);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
// ---- D. optional suite generation (Milestone C — really writes + compiles, or honest skip) ----
|
|
742
|
-
let suite;
|
|
743
|
-
if (a.generateSuite) {
|
|
744
|
-
const suiteProg = startProgress(sessions, session, job, 'suite_generating', { statusText: 'Generating + compiling a POM suite from the run.' });
|
|
745
|
-
attempted.push('generate suite');
|
|
746
|
-
suite = generateAndCompileSuite(sessions, session, { save: true, compile: true });
|
|
747
|
-
if (suite.skipped) {
|
|
748
|
-
suiteProg.done(`suite skipped — ${suite.skippedReason}`);
|
|
749
|
-
}
|
|
750
|
-
else {
|
|
751
|
-
for (const w of suite.written) {
|
|
752
|
-
const uri = `file://${w}`;
|
|
753
|
-
if (!artifacts.includes(uri))
|
|
754
|
-
artifacts.push(uri);
|
|
755
|
-
}
|
|
756
|
-
sessions.addWorkaround(session, `generated a POM suite (${suite.compiledFlows.filter((c) => c.ok).length} runnable flow(s))`);
|
|
757
|
-
suiteProg.done(`suite ready — runnable=${suite.suiteRunnable}`);
|
|
758
|
-
}
|
|
759
|
-
suiteForReport = suite; // embed suite outputs in the report artifact (item 3)
|
|
760
|
-
}
|
|
761
|
-
// ---- D2. App Knowledge Map (SWIPIUM-REQ-01) — update the durable map after runtime execution.
|
|
762
|
-
// Best-effort: a map failure must never turn a passing run into a blocked one.
|
|
763
|
-
let appMap;
|
|
764
|
-
try {
|
|
765
|
-
const { buildAppMap, summarizeMap, quickCoverage } = await import('../appMap/build.js');
|
|
766
|
-
const at = new Date().toISOString();
|
|
767
|
-
const before = quickCoverage(session.root, at);
|
|
768
|
-
// If guided exploration ran, merge its screen graph too (links runtime → static screens).
|
|
769
|
-
let exploreGraph = null;
|
|
770
|
-
if (session.exploration?.graphUri) {
|
|
771
|
-
const found = sessions.findArtifact(session.exploration.graphUri);
|
|
772
|
-
if (found) {
|
|
773
|
-
try {
|
|
774
|
-
exploreGraph = JSON.parse(readFileSync(found.rec.path, 'utf8'));
|
|
775
|
-
}
|
|
776
|
-
catch {
|
|
777
|
-
/* ignore unreadable graph */
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
// Static topology was already built/refreshed pre-launch (Fix 1) — merge runtime on top. With no
|
|
782
|
-
// existing map (pre-launch failed) runtime_merge still rescans static (doRescan when !existed).
|
|
783
|
-
const built = buildAppMap(session.root, { mode: 'runtime_merge', at, sessionId: session.id, exploreGraph, firstRunPatches, persist: true });
|
|
784
|
-
const after = built.map.coverage;
|
|
785
|
-
appMap = {
|
|
786
|
-
appMapUri: built.save?.resourceUri,
|
|
787
|
-
appMapSummary: summarizeMap(built.map),
|
|
788
|
-
mapCoverageDelta: {
|
|
789
|
-
coveragePercent: after.overallPercent - before.overallPercent,
|
|
790
|
-
runtimeScreens: after.runtimeScreens - before.runtimeScreens,
|
|
791
|
-
staticScreens: after.staticScreens - before.staticScreens,
|
|
792
|
-
},
|
|
793
|
-
};
|
|
794
|
-
if (built.save?.resourceUri && !artifacts.includes(built.save.resourceUri))
|
|
795
|
-
artifacts.push(built.save.resourceUri);
|
|
796
|
-
}
|
|
797
|
-
catch (e) {
|
|
798
|
-
log('warn', 'test_this app map update failed', { jobId: job.jobId, err: String(e) });
|
|
799
|
-
}
|
|
800
|
-
// ---- E. terminal state (report is generated inside finish) ----
|
|
801
|
-
const launchOutcome = smoke.baseline.launch?.outcome;
|
|
802
|
-
const hardFail = session.findings.some((f) => f.severity === 'high') || launchOutcome === 'fail';
|
|
803
|
-
const suiteResult = suite
|
|
804
|
-
? { generated: !suite.skipped, skippedReason: suite.skippedReason, written: suite.written, compiledFlows: suite.compiledFlows, suiteRunnable: suite.suiteRunnable, readinessLabels: suite.readinessLabels, manifestPath: suite.manifestPath ?? null }
|
|
805
|
-
: undefined;
|
|
806
|
-
const summary = `${hardFail ? '⚠️' : '✅'} test-this ${a.mode} ${hardFail ? 'finished with findings' : 'completed'} on ${a.target.selected}.\n` +
|
|
807
|
-
`smoke: launch=${launchOutcome ?? 'unknown'}, flows ${smoke.flowsPassed}/${smoke.flowsTotal}; findings=${session.findings.length}.` +
|
|
808
|
-
(suite ? `\nsuite: ${suite.skipped ? `skipped (${suite.skippedReason})` : `${suite.compiledFlows.filter((c) => c.ok).length}/${suite.compiledFlows.length} runnable flow(s)`}` : '');
|
|
809
|
-
await finish('completed', undefined, summary + (appMap?.appMapUri ? `\nappMap: ${appMap.appMapUri}` : ''), {
|
|
810
|
-
smoke: { launch: launchOutcome ?? 'unknown', flowsPassed: smoke.flowsPassed, flowsTotal: smoke.flowsTotal },
|
|
811
|
-
highFindings: session.findings.filter((f) => f.severity === 'high').length,
|
|
812
|
-
...(suiteResult ? { suite: suiteResult } : {}),
|
|
813
|
-
...(appMap ? { appMapUri: appMap.appMapUri, appMapSummary: appMap.appMapSummary, mapCoverageDelta: appMap.mapCoverageDelta } : {}),
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
catch (e) {
|
|
817
|
-
if (signal?.aborted)
|
|
818
|
-
return;
|
|
819
|
-
log('error', 'test_this execute pipeline failed', { jobId: job.jobId, err: String(e) });
|
|
820
|
-
await finish('blocked', 'UNKNOWN', `❌ Execution error: ${String(e)}`);
|
|
821
|
-
}
|
|
54
|
+
}, async (input) => handleTestThis(server, sessions, input));
|
|
822
55
|
}
|
|
823
56
|
//# sourceMappingURL=testThis.js.map
|