swipium 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +89 -25
- package/THREAT_MODEL.md +1 -1
- package/dist/appMap/automationLink.js +13 -3
- package/dist/appMap/automationLink.js.map +1 -1
- package/dist/appMap/build.js +28 -6
- package/dist/appMap/build.js.map +1 -1
- package/dist/appMap/codeIndex.js +29 -3
- package/dist/appMap/codeIndex.js.map +1 -1
- package/dist/appMap/featureIndex.js +20 -2
- package/dist/appMap/featureIndex.js.map +1 -1
- package/dist/appMap/featureModel.js +42 -6
- package/dist/appMap/featureModel.js.map +1 -1
- package/dist/appMap/firstRunApply.js +22 -4
- package/dist/appMap/firstRunApply.js.map +1 -1
- package/dist/appMap/fsWalk.js +19 -2
- package/dist/appMap/fsWalk.js.map +1 -1
- package/dist/appMap/issues.js +0 -20
- package/dist/appMap/issues.js.map +1 -1
- package/dist/appMap/migrations.js +1 -1
- package/dist/appMap/migrations.js.map +1 -1
- package/dist/appMap/prelaunch.js.map +1 -1
- package/dist/appMap/projectRegistry.js +8 -2
- package/dist/appMap/projectRegistry.js.map +1 -1
- package/dist/appMap/query.js +20 -4
- package/dist/appMap/query.js.map +1 -1
- package/dist/appMap/runtimeMerge.js +9 -2
- package/dist/appMap/runtimeMerge.js.map +1 -1
- package/dist/appMap/schema.js.map +1 -1
- package/dist/appMap/screenMatch.js +5 -1
- package/dist/appMap/screenMatch.js.map +1 -1
- package/dist/appMap/staticScan.js +134 -22
- package/dist/appMap/staticScan.js.map +1 -1
- package/dist/appMap/store.js +4 -79
- package/dist/appMap/store.js.map +1 -1
- package/dist/appMap/tsAstScan.js +31 -9
- package/dist/appMap/tsAstScan.js.map +1 -1
- package/dist/artifacts/bundletool.js +44 -7
- package/dist/artifacts/bundletool.js.map +1 -1
- package/dist/artifacts/resolve.js +33 -6
- package/dist/artifacts/resolve.js.map +1 -1
- package/dist/automation/capabilities.js +109 -27
- package/dist/automation/capabilities.js.map +1 -1
- package/dist/automation/gestures.js +5 -1
- package/dist/automation/gestures.js.map +1 -1
- package/dist/automation/plan.js +65 -17
- package/dist/automation/plan.js.map +1 -1
- package/dist/automation/report.js +12 -2
- package/dist/automation/report.js.map +1 -1
- package/dist/automation/selectors.js +8 -3
- package/dist/automation/selectors.js.map +1 -1
- package/dist/automation/types.js.map +1 -1
- package/dist/automation/waits.js +9 -3
- package/dist/automation/waits.js.map +1 -1
- package/dist/automation/webview.js +7 -1
- package/dist/automation/webview.js.map +1 -1
- package/dist/automationGen/appiumModel.js +3 -1
- package/dist/automationGen/appiumModel.js.map +1 -1
- package/dist/automationGen/ciEmitter.js +1 -1
- package/dist/automationGen/ciEmitter.js.map +1 -1
- package/dist/automationGen/jsEmitter.js +29 -21
- package/dist/automationGen/jsEmitter.js.map +1 -1
- package/dist/automationGen/projectProfile.js +5 -1
- package/dist/automationGen/projectProfile.js.map +1 -1
- package/dist/automationGen/pythonEmitter.js +3 -3
- package/dist/automationGen/pythonEmitter.js.map +1 -1
- package/dist/automationGen/readmeEmitter.js.map +1 -1
- package/dist/automationGen/suitePlan.js +11 -6
- package/dist/automationGen/suitePlan.js.map +1 -1
- package/dist/automationGen/validation.js +33 -5
- package/dist/automationGen/validation.js.map +1 -1
- package/dist/build/parseBuildLog.js +234 -30
- package/dist/build/parseBuildLog.js.map +1 -1
- package/dist/build/plan.js +33 -12
- package/dist/build/plan.js.map +1 -1
- package/dist/ci/preflight.js.map +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/scan.js +1 -1
- package/dist/cli/scan.js.map +1 -1
- package/dist/cli/suite.js +2 -2
- package/dist/cli/suite.js.map +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/context/detect.js +10 -4
- package/dist/context/detect.js.map +1 -1
- package/dist/context/findApps.js +16 -2
- package/dist/context/findApps.js.map +1 -1
- package/dist/context/scan.js.map +1 -1
- package/dist/core/target.js +7 -5
- package/dist/core/target.js.map +1 -1
- package/dist/core/targetPlan.js +135 -17
- package/dist/core/targetPlan.js.map +1 -1
- package/dist/drivers/DirectDriver.js +17 -7
- package/dist/drivers/DirectDriver.js.map +1 -1
- package/dist/drivers/SimctlDriver.js +4 -1
- package/dist/drivers/SimctlDriver.js.map +1 -1
- package/dist/drivers/WdaDriver.js +15 -5
- package/dist/drivers/WdaDriver.js.map +1 -1
- package/dist/explore/candidates.js +5 -1
- package/dist/explore/candidates.js.map +1 -1
- package/dist/explore/graph.js +11 -2
- package/dist/explore/graph.js.map +1 -1
- package/dist/explore/memory.js +4 -1
- package/dist/explore/memory.js.map +1 -1
- package/dist/explore/planner.js +54 -9
- package/dist/explore/planner.js.map +1 -1
- package/dist/explore/policy.js +6 -1
- package/dist/explore/policy.js.map +1 -1
- package/dist/explore/runner.js +179 -23
- package/dist/explore/runner.js.map +1 -1
- package/dist/explore/signatures.js +3 -1
- package/dist/explore/signatures.js.map +1 -1
- package/dist/explore/suite.js +3 -1
- package/dist/explore/suite.js.map +1 -1
- package/dist/featureTesting/executionBootstrap.js +128 -19
- package/dist/featureTesting/executionBootstrap.js.map +1 -1
- package/dist/featureTesting/featureMap.js +4 -1
- package/dist/featureTesting/featureMap.js.map +1 -1
- package/dist/featureTesting/featureScope.js +94 -13
- package/dist/featureTesting/featureScope.js.map +1 -1
- package/dist/featureTesting/mapFeatureScope.js +37 -8
- package/dist/featureTesting/mapFeatureScope.js.map +1 -1
- package/dist/featureTesting/objectiveModel.js +5 -1
- package/dist/featureTesting/objectiveModel.js.map +1 -1
- package/dist/featureTesting/resultMerge.js +10 -2
- package/dist/featureTesting/resultMerge.js.map +1 -1
- package/dist/featureTesting/sources.js +4 -1
- package/dist/featureTesting/sources.js.map +1 -1
- package/dist/featureTesting/suiteBridge.js +7 -1
- package/dist/featureTesting/suiteBridge.js.map +1 -1
- package/dist/featureTesting/synonyms.js +31 -3
- package/dist/featureTesting/synonyms.js.map +1 -1
- package/dist/featureTesting/testCaseFactory.js +16 -4
- package/dist/featureTesting/testCaseFactory.js.map +1 -1
- package/dist/featureTesting/testPlan.js +32 -10
- package/dist/featureTesting/testPlan.js.map +1 -1
- package/dist/firstRun/classifyScreen.js +4 -1
- package/dist/firstRun/classifyScreen.js.map +1 -1
- package/dist/firstRun/firstRunPlanner.js +5 -2
- package/dist/firstRun/firstRunPlanner.js.map +1 -1
- package/dist/firstRun/firstRunRunner.js +84 -23
- package/dist/firstRun/firstRunRunner.js.map +1 -1
- package/dist/firstRun/generatedDataPolicy.js +16 -6
- package/dist/firstRun/generatedDataPolicy.js.map +1 -1
- package/dist/firstRun/inputPlanner.js +40 -6
- package/dist/firstRun/inputPlanner.js.map +1 -1
- package/dist/firstRun/paywallPolicy.js.map +1 -1
- package/dist/fixtures/catalog.js +39 -6
- package/dist/fixtures/catalog.js.map +1 -1
- package/dist/flows/generate.js +53 -12
- package/dist/flows/generate.js.map +1 -1
- package/dist/flows/lint.js +88 -14
- package/dist/flows/lint.js.map +1 -1
- package/dist/flows/pack.js +18 -2
- package/dist/flows/pack.js.map +1 -1
- package/dist/flows/repair.js +71 -20
- package/dist/flows/repair.js.map +1 -1
- package/dist/flows/run.js +164 -51
- package/dist/flows/run.js.map +1 -1
- package/dist/flows/schema.js +57 -20
- package/dist/flows/schema.js.map +1 -1
- package/dist/flows/seedExec.js +5 -1
- package/dist/flows/seedExec.js.map +1 -1
- package/dist/ios/signing.js +32 -3
- package/dist/ios/signing.js.map +1 -1
- package/dist/issues/classify.js +12 -4
- package/dist/issues/classify.js.map +1 -1
- package/dist/issues/fingerprint.js +11 -5
- package/dist/issues/fingerprint.js.map +1 -1
- package/dist/issues/index.js +4 -20
- package/dist/issues/index.js.map +1 -1
- package/dist/issues/metrics.js +2 -1
- package/dist/issues/metrics.js.map +1 -1
- package/dist/issues/recurrence.js.map +1 -1
- package/dist/issues/report.js +8 -4
- package/dist/issues/report.js.map +1 -1
- package/dist/issues/reportBridge.js +36 -5
- package/dist/issues/reportBridge.js.map +1 -1
- package/dist/issues/schema.js.map +1 -1
- package/dist/issues/store.js.map +1 -1
- package/dist/lib/android.js +9 -1
- package/dist/lib/android.js.map +1 -1
- package/dist/lib/device.js +21 -5
- package/dist/lib/device.js.map +1 -1
- package/dist/lib/image.js +11 -2
- package/dist/lib/image.js.map +1 -1
- package/dist/lib/lockfile.js +60 -0
- package/dist/lib/lockfile.js.map +1 -0
- package/dist/lib/needsInput.js +6 -3
- package/dist/lib/needsInput.js.map +1 -1
- package/dist/lib/png.js +2 -1
- package/dist/lib/png.js.map +1 -1
- package/dist/lib/result.js +16 -1
- package/dist/lib/result.js.map +1 -1
- package/dist/lib/schemaHash.js +4 -2
- package/dist/lib/schemaHash.js.map +1 -1
- package/dist/lib/sensitive.js +3 -1
- package/dist/lib/sensitive.js.map +1 -1
- package/dist/lib/simctl.js.map +1 -1
- package/dist/lib/spawn.js +29 -10
- package/dist/lib/spawn.js.map +1 -1
- package/dist/lib/wda.js +27 -10
- package/dist/lib/wda.js.map +1 -1
- package/dist/lib/wdaConfig.js +12 -5
- package/dist/lib/wdaConfig.js.map +1 -1
- package/dist/lib/wdaTune.js +52 -23
- package/dist/lib/wdaTune.js.map +1 -1
- package/dist/mobileAudit/checks.js +141 -19
- package/dist/mobileAudit/checks.js.map +1 -1
- package/dist/mobileAudit/profiles.js +120 -19
- package/dist/mobileAudit/profiles.js.map +1 -1
- package/dist/mobileAudit/results.js +7 -2
- package/dist/mobileAudit/results.js.map +1 -1
- package/dist/mobileAudit/runner.js +144 -19
- package/dist/mobileAudit/runner.js.map +1 -1
- package/dist/oracle/auth.js.map +1 -1
- package/dist/oracle/failures.js +888 -131
- package/dist/oracle/failures.js.map +1 -1
- package/dist/oracle/health.js +90 -14
- package/dist/oracle/health.js.map +1 -1
- package/dist/oracle/locator.js +147 -18
- package/dist/oracle/locator.js.map +1 -1
- package/dist/oracle/record.js.map +1 -1
- package/dist/orchestration/goal.js +3 -9
- package/dist/orchestration/goal.js.map +1 -1
- package/dist/orchestration/testThis/execute.js +126 -0
- package/dist/orchestration/testThis/execute.js.map +1 -0
- package/dist/orchestration/testThis/pipeline.js +324 -0
- package/dist/orchestration/testThis/pipeline.js.map +1 -0
- package/dist/orchestration/testThis/plan.js +430 -0
- package/dist/orchestration/testThis/plan.js.map +1 -0
- package/dist/orchestration/testThis/terminal.js +118 -0
- package/dist/orchestration/testThis/terminal.js.map +1 -0
- package/dist/orchestration/testThis/types.js +12 -0
- package/dist/orchestration/testThis/types.js.map +1 -0
- package/dist/plan/plan.js.map +1 -1
- package/dist/prompts/index.js +3 -3
- package/dist/prompts/index.js.map +1 -1
- package/dist/report/coverage.js +0 -1
- package/dist/report/coverage.js.map +1 -1
- package/dist/report/evidence.js.map +1 -1
- package/dist/report/export.js +59 -37
- package/dist/report/export.js.map +1 -1
- package/dist/report/flake.js +13 -14
- package/dist/report/flake.js.map +1 -1
- package/dist/report/history.js +72 -38
- package/dist/report/history.js.map +1 -1
- package/dist/report/policy.js +11 -3
- package/dist/report/policy.js.map +1 -1
- package/dist/report/qaLevel.js +2 -8
- package/dist/report/qaLevel.js.map +1 -1
- package/dist/report/readiness.js +42 -12
- package/dist/report/readiness.js.map +1 -1
- package/dist/report/summary.js.map +1 -1
- package/dist/report/timing.js +23 -23
- package/dist/report/timing.js.map +1 -1
- package/dist/server.js +42 -37
- package/dist/server.js.map +1 -1
- package/dist/services/automationGenerate.js.map +1 -1
- package/dist/services/build.js +8 -2
- package/dist/services/build.js.map +1 -1
- package/dist/services/preflight.js +38 -6
- package/dist/services/preflight.js.map +1 -1
- package/dist/services/prepareAndroid.js +46 -10
- package/dist/services/prepareAndroid.js.map +1 -1
- package/dist/services/prepareIos.js +72 -10
- package/dist/services/prepareIos.js.map +1 -1
- package/dist/services/report.js +173 -37
- package/dist/services/report.js.map +1 -1
- package/dist/services/smoke.js +36 -6
- package/dist/services/smoke.js.map +1 -1
- package/dist/services/suiteGenerate.js +16 -4
- package/dist/services/suiteGenerate.js.map +1 -1
- package/dist/services/testSuiteKnowledge.js +38 -6
- package/dist/services/testSuiteKnowledge.js.map +1 -1
- package/dist/session/attach.js +16 -0
- package/dist/session/attach.js.map +1 -1
- package/dist/session/processRegistry.js +164 -0
- package/dist/session/processRegistry.js.map +1 -0
- package/dist/session/progress.js +1 -1
- package/dist/session/progress.js.map +1 -1
- package/dist/session/store.js +216 -43
- package/dist/session/store.js.map +1 -1
- package/dist/snapshot/overlays.js +7 -2
- package/dist/snapshot/overlays.js.map +1 -1
- package/dist/snapshot/parse.js +2 -8
- package/dist/snapshot/parse.js.map +1 -1
- package/dist/snapshot/present.js +27 -3
- package/dist/snapshot/present.js.map +1 -1
- package/dist/state/profile.js +36 -9
- package/dist/state/profile.js.map +1 -1
- package/dist/suite/compile.js +7 -2
- package/dist/suite/compile.js.map +1 -1
- package/dist/suite/lint.js +21 -3
- package/dist/suite/lint.js.map +1 -1
- package/dist/suite/pom.js +26 -4
- package/dist/suite/pom.js.map +1 -1
- package/dist/suite/testcase.js +6 -4
- package/dist/suite/testcase.js.map +1 -1
- package/dist/testSuite/exporter.js +6 -2
- package/dist/testSuite/exporter.js.map +1 -1
- package/dist/testSuite/generator.js +31 -6
- package/dist/testSuite/generator.js.map +1 -1
- package/dist/testSuite/issueLinks.js.map +1 -1
- package/dist/testSuite/lint.js +1 -1
- package/dist/testSuite/lint.js.map +1 -1
- package/dist/testSuite/merge.js +8 -2
- package/dist/testSuite/merge.js.map +1 -1
- package/dist/testSuite/schema.js +16 -11
- package/dist/testSuite/schema.js.map +1 -1
- package/dist/testSuite/store.js +9 -1
- package/dist/testSuite/store.js.map +1 -1
- package/dist/tools/act.js +107 -43
- package/dist/tools/act.js.map +1 -1
- package/dist/tools/agent.js +123 -25
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/appControl.js +16 -3
- package/dist/tools/appControl.js.map +1 -1
- package/dist/tools/appMap.js +331 -63
- package/dist/tools/appMap.js.map +1 -1
- package/dist/tools/assertVisual.js +12 -2
- package/dist/tools/assertVisual.js.map +1 -1
- package/dist/tools/automationGenerate.js +276 -241
- package/dist/tools/automationGenerate.js.map +1 -1
- package/dist/tools/build.js +92 -41
- package/dist/tools/build.js.map +1 -1
- package/dist/tools/bundletool.js +139 -28
- package/dist/tools/bundletool.js.map +1 -1
- package/dist/tools/capabilities.js +45 -71
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/clearOverlay.js +45 -8
- package/dist/tools/clearOverlay.js.map +1 -1
- package/dist/tools/detectContext.js +1 -1
- package/dist/tools/detectContext.js.map +1 -1
- package/dist/tools/device.js +44 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/doctor.js +38 -15
- package/dist/tools/doctor.js.map +1 -1
- package/dist/tools/explore.js +96 -22
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/featureTesting.js +153 -114
- package/dist/tools/featureTesting.js.map +1 -1
- package/dist/tools/firstRun.js +95 -60
- package/dist/tools/firstRun.js.map +1 -1
- package/dist/tools/flow.js +236 -101
- package/dist/tools/flow.js.map +1 -1
- package/dist/tools/flowGenerate.js +51 -47
- package/dist/tools/flowGenerate.js.map +1 -1
- package/dist/tools/flowRepair.js +24 -4
- package/dist/tools/flowRepair.js.map +1 -1
- package/dist/tools/generate.js +236 -0
- package/dist/tools/generate.js.map +1 -0
- package/dist/tools/getArtifact.js +21 -3
- package/dist/tools/getArtifact.js.map +1 -1
- package/dist/tools/health.js +3 -1
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/history.js +40 -10
- package/dist/tools/history.js.map +1 -1
- package/dist/tools/ios.js +157 -25
- package/dist/tools/ios.js.map +1 -1
- package/dist/tools/issues.js +19 -150
- package/dist/tools/issues.js.map +1 -1
- package/dist/tools/jobs.js +23 -4
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/locator.js +36 -8
- package/dist/tools/locator.js.map +1 -1
- package/dist/tools/metro.js +61 -16
- package/dist/tools/metro.js.map +1 -1
- package/dist/tools/mobileAudit.js +67 -15
- package/dist/tools/mobileAudit.js.map +1 -1
- package/dist/tools/network.js +6 -1
- package/dist/tools/network.js.map +1 -1
- package/dist/tools/note.js +20 -6
- package/dist/tools/note.js.map +1 -1
- package/dist/tools/permissions.js +31 -6
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/plan.js +5 -2
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/prepareIosTarget.js +31 -8
- package/dist/tools/prepareIosTarget.js.map +1 -1
- package/dist/tools/prepareTarget.js +97 -18
- package/dist/tools/prepareTarget.js.map +1 -1
- package/dist/tools/report.js +24 -5
- package/dist/tools/report.js.map +1 -1
- package/dist/tools/resolveArtifact.js +9 -5
- package/dist/tools/resolveArtifact.js.map +1 -1
- package/dist/tools/resolveTarget.js +11 -2
- package/dist/tools/resolveTarget.js.map +1 -1
- package/dist/tools/screenInfo.js +8 -2
- package/dist/tools/screenInfo.js.map +1 -1
- package/dist/tools/screenRecord.js +74 -11
- package/dist/tools/screenRecord.js.map +1 -1
- package/dist/tools/screenshot.js +23 -6
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seed.js +38 -6
- package/dist/tools/seed.js.map +1 -1
- package/dist/tools/smoke.js +16 -3
- package/dist/tools/smoke.js.map +1 -1
- package/dist/tools/snapshot.js +59 -6
- package/dist/tools/snapshot.js.map +1 -1
- package/dist/tools/startSession.js +54 -9
- package/dist/tools/startSession.js.map +1 -1
- package/dist/tools/state.js +62 -15
- package/dist/tools/state.js.map +1 -1
- package/dist/tools/suite.js +350 -298
- package/dist/tools/suite.js.map +1 -1
- package/dist/tools/testSuite.js +94 -28
- package/dist/tools/testSuite.js.map +1 -1
- package/dist/tools/testThis.js +30 -793
- package/dist/tools/testThis.js.map +1 -1
- package/dist/tools/visual.js +124 -18
- package/dist/tools/visual.js.map +1 -1
- package/dist/tools/visualText.js +51 -6
- package/dist/tools/visualText.js.map +1 -1
- package/dist/tools/wait.js +18 -3
- package/dist/tools/wait.js.map +1 -1
- package/dist/tools/wda.js +157 -29
- package/dist/tools/wda.js.map +1 -1
- package/dist/version.js +17 -49
- package/dist/version.js.map +1 -1
- package/dist/visual/ocr.js +12 -2
- package/dist/visual/ocr.js.map +1 -1
- package/dist/visual/provider.js +7 -3
- package/dist/visual/provider.js.map +1 -1
- package/docs/mcp-server.md +44 -4
- package/docs/tools.md +42 -100
- package/package.json +14 -5
- package/dist/appMap/diff.js +0 -60
- package/dist/appMap/diff.js.map +0 -1
- package/dist/automation/maestroIr.js +0 -282
- package/dist/automation/maestroIr.js.map +0 -1
- package/dist/interop/maestro.js +0 -151
- package/dist/interop/maestro.js.map +0 -1
- package/dist/tools/idling.js +0 -104
- package/dist/tools/idling.js.map +0 -1
- package/dist/tools/inputCapabilities.js +0 -30
- package/dist/tools/inputCapabilities.js.map +0 -1
- package/dist/tools/maestro.js +0 -78
- package/dist/tools/maestro.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// SWIPIUM-REQ-04 — "Automate my app"
|
|
2
|
-
//
|
|
1
|
+
// SWIPIUM-REQ-04 — "Automate my app". Generates a runnable JS/TS or Python Appium POM suite from a
|
|
2
|
+
// session's recorded actions, adapting to the project's language/platform/test stack.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// runAutomationPlan — read-only core for qa_generate target:"appium" mode:"plan":
|
|
5
|
+
// project profile + generation plan + blockers.
|
|
6
|
+
// runAutomationGenerate — core for qa_generate target:"appium": emit + write the JS/Python
|
|
7
|
+
// suite (+ README, optional CI), validate.
|
|
6
8
|
// qa_automation_validate — validate generated code without a device (secrets, durability, syntax).
|
|
7
9
|
//
|
|
8
10
|
// The canonical Swipium YAML/POM remains the intermediate model (non-goal: don't replace it). JS/Python
|
|
@@ -12,7 +14,7 @@ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSy
|
|
|
12
14
|
import { join, relative, dirname } from 'node:path';
|
|
13
15
|
import { qaOk, qaError } from '../lib/result.js';
|
|
14
16
|
import { requireConsent, consumeConsent } from '../consent/consent.js';
|
|
15
|
-
import { assembleAutomationSuite, buildAutomationProfile, writeAutomationFiles, mapPresent
|
|
17
|
+
import { assembleAutomationSuite, buildAutomationProfile, writeAutomationFiles, mapPresent } from '../services/automationGenerate.js';
|
|
16
18
|
import { buildSuitePlan } from '../automationGen/suitePlan.js';
|
|
17
19
|
import { buildProjectProfile } from '../automationGen/projectProfile.js';
|
|
18
20
|
import { validateGeneratedSuite } from '../automationGen/validation.js';
|
|
@@ -27,7 +29,6 @@ import { bootstrapFeatureExecution } from '../featureTesting/executionBootstrap.
|
|
|
27
29
|
import { log } from '../lib/logger.js';
|
|
28
30
|
const languageEnum = z.enum(['auto', 'javascript', 'typescript', 'python']);
|
|
29
31
|
const platformEnum = z.enum(['auto', 'android', 'ios', 'both']);
|
|
30
|
-
const backendEnum = z.enum(['auto', 'appium', 'swipium_flow']);
|
|
31
32
|
/** Merge a prebuilt appMapUri into a tool result's structured content (Fix 6 — a bootstrap blocker
|
|
32
33
|
* still built the static map, so the URI must survive on the returned blocker/consent result). */
|
|
33
34
|
function withAppMapUri(result, appMapUri) {
|
|
@@ -73,256 +74,276 @@ function readGeneratedFromDisk(root, outputDir) {
|
|
|
73
74
|
walk(base);
|
|
74
75
|
return files;
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Core handler for qa_generate target:"appium" mode:"plan". Read-only: inspect the project
|
|
79
|
+
* (language, platform support, existing test stack) and build a plan for an "Automate my app"
|
|
80
|
+
* Appium POM suite — selected automation language, backends, coverage, files planned, blockers.
|
|
81
|
+
*/
|
|
82
|
+
export async function runAutomationPlan(sessions, { sessionId, projectRoot, feature, language, platform, backend, includeCi }) {
|
|
83
|
+
const { session, root } = resolveRoot(sessions, sessionId, projectRoot);
|
|
84
|
+
if (!root)
|
|
85
|
+
return qaError({ what: 'No project root', changedState: false, retrySafe: true, nextSteps: ['Pass sessionId or projectRoot.'] });
|
|
86
|
+
const inputs = profileInputs(language, platform);
|
|
87
|
+
const profile = session ? buildAutomationProfile(session, inputs) : buildAutomationProfileFromRoot(root, inputs);
|
|
88
|
+
let plan;
|
|
89
|
+
if (session && session.recordedActions.length) {
|
|
90
|
+
const assembled = assembleAutomationSuite(session, { ...inputs, includeCi });
|
|
91
|
+
plan = assembled.plan;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
plan = buildSuitePlan(profile, { actionCount: session?.recordedActions.length ?? 0, mapPresent: mapPresent(root), includeCi });
|
|
95
|
+
}
|
|
96
|
+
const backendNote = backend && backend !== 'auto' && backend !== 'appium'
|
|
97
|
+
? `\nNote: backend=${backend} requested — Appium code is generated as an ADDITIONAL layer; existing ${backend} flows are kept.`
|
|
98
|
+
: '';
|
|
99
|
+
const featureNote = feature ? ` (feature focus: ${feature})` : '';
|
|
100
|
+
const summary = `Automation plan${featureNote}: ${profile.automationLanguage} / ${profile.testFramework}, ` +
|
|
101
|
+
`default backend ${profile.defaultBackend}${profile.secondaryBackend ? ` (+${profile.secondaryBackend})` : ''}.\n` +
|
|
102
|
+
`platforms: android=${profile.platforms.android.level}, ios=${profile.platforms.ios.level}\n` +
|
|
103
|
+
(plan.mapCoverage
|
|
104
|
+
? `coverage: ${plan.mapCoverage.actionCount} step(s), ${plan.mapCoverage.screenCount} screen(s); locators ${plan.locatorReadiness} (${plan.mapCoverage.brittlePct}% brittle)`
|
|
105
|
+
: 'coverage: no recorded actions yet') +
|
|
106
|
+
(plan.blockers.length ? `\nblockers: ${plan.blockers.map((b) => b.code).join(', ')}` : '') +
|
|
107
|
+
`\nfiles planned: ${plan.filesPlanned.length}\nnext: ${plan.nextAction}` +
|
|
108
|
+
backendNote;
|
|
109
|
+
return qaOk({ profile, plan, feature: feature ?? null, backend: backend ?? 'auto' }, summary);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Core handler for qa_generate target:"appium". Generates a runnable Appium POM suite from the
|
|
113
|
+
* session's recorded actions (WebdriverIO TS/JS or Python), bootstrapping a device/session/actions
|
|
114
|
+
* from projectRoot when none exist. Never inlines secrets; integrateIntoProject is consent-gated.
|
|
115
|
+
*/
|
|
116
|
+
export async function runAutomationGenerate(server, sessions, { sessionId, projectRoot, bootstrap, feature, device, name, language, platform, save, integrateIntoProject, includeCi, candidateOnly, brittleThreshold, consentId, approve, }) {
|
|
117
|
+
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
118
|
+
if (sessionId && !session)
|
|
119
|
+
return qaError({
|
|
120
|
+
what: `Unknown sessionId ${sessionId}`,
|
|
121
|
+
changedState: false,
|
|
122
|
+
retrySafe: true,
|
|
123
|
+
nextSteps: ['Call qa_start_session first, or omit sessionId to bootstrap from projectRoot.'],
|
|
124
|
+
});
|
|
125
|
+
// Fix 6 — one-call "Automate my app": when no session/actions exist, build the static app map
|
|
126
|
+
// (so it always exists) and bootstrap a device (consent-gated) + record actions via exploration.
|
|
127
|
+
const wantBootstrap = bootstrap !== false && (bootstrap === true || bootstrap === 'auto' || !sessionId);
|
|
128
|
+
let appMapUri;
|
|
129
|
+
const rootForMap = session?.root ?? (projectRoot && projectRoot.trim() ? projectRoot.trim() : undefined);
|
|
130
|
+
if (rootForMap && existsSync(rootForMap)) {
|
|
131
|
+
try {
|
|
132
|
+
appMapUri = ensurePrelaunchAppMap(rootForMap, { at: new Date().toISOString() }).appMapUri;
|
|
103
133
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
: '';
|
|
107
|
-
const featureNote = feature ? ` (feature focus: ${feature})` : '';
|
|
108
|
-
const summary = `Automation plan${featureNote}: ${profile.automationLanguage} / ${profile.testFramework}, ` +
|
|
109
|
-
`default backend ${profile.defaultBackend}${profile.secondaryBackend ? ` (+${profile.secondaryBackend})` : ''}.\n` +
|
|
110
|
-
`platforms: android=${profile.platforms.android.level}, ios=${profile.platforms.ios.level}\n` +
|
|
111
|
-
(plan.mapCoverage
|
|
112
|
-
? `coverage: ${plan.mapCoverage.actionCount} step(s), ${plan.mapCoverage.screenCount} screen(s); locators ${plan.locatorReadiness} (${plan.mapCoverage.brittlePct}% brittle)`
|
|
113
|
-
: 'coverage: no recorded actions yet') +
|
|
114
|
-
(plan.blockers.length ? `\nblockers: ${plan.blockers.map((b) => b.code).join(', ')}` : '') +
|
|
115
|
-
`\nfiles planned: ${plan.filesPlanned.length}\nnext: ${plan.nextAction}` +
|
|
116
|
-
backendNote;
|
|
117
|
-
return qaOk({ profile, plan, feature: feature ?? null, backend: backend ?? 'auto' }, summary);
|
|
118
|
-
});
|
|
119
|
-
// ---- qa_automation_generate ----
|
|
120
|
-
server.registerTool('qa_automation_generate', {
|
|
121
|
-
title: 'Generate an Appium automation suite',
|
|
122
|
-
description: 'Generate a runnable Appium POM suite from this session\'s recorded actions, adapting to the project language: WebdriverIO TS/JS or Python (Appium-Python-Client). Writes page/screen objects (centralized selectors), env-driven capabilities, structured waits, a smoke test, and a README under .swipium/automation/<lang>/ by default. Never inlines secrets; marks brittle/coordinate locators non-release-grade; never emits XPath. integrateIntoProject (consent-gated) writes into the project e2e dir without overwriting existing files.',
|
|
123
|
-
inputSchema: {
|
|
124
|
-
sessionId: z.string().optional().describe('Prepared session with recorded actions. Omit to bootstrap from projectRoot.'),
|
|
125
|
-
projectRoot: z.string().optional().describe('Project root — used to bootstrap a device/session when no sessionId is given (Fix 6 one-call workflow).'),
|
|
126
|
-
bootstrap: z.union([z.boolean(), z.literal('auto')]).optional().describe('Bootstrap the missing map/session/actions safely (smoke+explore) when none exist. Default "auto" when no sessionId is provided.'),
|
|
127
|
-
feature: z.string().optional().describe('Optional focus for the bootstrap exploration.'),
|
|
128
|
-
device: z.string().optional().describe('Specific device/simulator to prepare when bootstrapping.'),
|
|
129
|
-
name: z.string().optional(),
|
|
130
|
-
language: languageEnum.optional(),
|
|
131
|
-
platform: platformEnum.optional(),
|
|
132
|
-
save: z.boolean().optional().describe('Write files (default true).'),
|
|
133
|
-
integrateIntoProject: z.boolean().optional().describe('Write into the project test dir instead of .swipium (consent-gated, never overwrites).'),
|
|
134
|
-
includeCi: z.boolean().optional().describe('Also emit ci.example.yml.'),
|
|
135
|
-
candidateOnly: z.boolean().optional().describe('Label the suite candidate-only so brittle locators do not fail validation.'),
|
|
136
|
-
brittleThreshold: z.number().optional().describe('Max brittle-locator percent before validation fails (default 40).'),
|
|
137
|
-
consentId: z.string().optional(),
|
|
138
|
-
approve: z.boolean().optional(),
|
|
139
|
-
},
|
|
140
|
-
}, async ({ sessionId, projectRoot, bootstrap, feature, device, name, language, platform, save, integrateIntoProject, includeCi, candidateOnly, brittleThreshold, consentId, approve }) => {
|
|
141
|
-
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
142
|
-
if (sessionId && !session)
|
|
143
|
-
return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first, or omit sessionId to bootstrap from projectRoot.'] });
|
|
144
|
-
// Fix 6 — one-call "Automate my app": when no session/actions exist, build the static app map
|
|
145
|
-
// (so it always exists) and bootstrap a device (consent-gated) + record actions via exploration.
|
|
146
|
-
const wantBootstrap = bootstrap !== false && (bootstrap === true || bootstrap === 'auto' || !sessionId);
|
|
147
|
-
let appMapUri;
|
|
148
|
-
const rootForMap = session?.root ?? (projectRoot && projectRoot.trim() ? projectRoot.trim() : undefined);
|
|
149
|
-
if (rootForMap && existsSync(rootForMap)) {
|
|
150
|
-
try {
|
|
151
|
-
appMapUri = ensurePrelaunchAppMap(rootForMap, { at: new Date().toISOString() }).appMapUri;
|
|
152
|
-
}
|
|
153
|
-
catch (e) {
|
|
154
|
-
log('warn', 'automation prelaunch app map failed', { err: String(e) });
|
|
155
|
-
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
log('warn', 'automation prelaunch app map failed', { err: String(e) });
|
|
156
136
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
session = boot.session;
|
|
169
|
-
driver = boot.driver;
|
|
170
|
-
if (!appMapUri && existsSync(session.root)) {
|
|
171
|
-
try {
|
|
172
|
-
appMapUri = ensurePrelaunchAppMap(session.root, { at: new Date().toISOString() }).appMapUri;
|
|
173
|
-
}
|
|
174
|
-
catch { /* best-effort */ }
|
|
175
|
-
}
|
|
137
|
+
}
|
|
138
|
+
if ((!session || !session.recordedActions.length) && wantBootstrap) {
|
|
139
|
+
let driver = session ? (await getDriver(session)).driver : undefined;
|
|
140
|
+
if (!session || !driver) {
|
|
141
|
+
if (!projectRoot?.trim() && !session) {
|
|
142
|
+
return qaError({
|
|
143
|
+
what: 'Need a sessionId or projectRoot to bootstrap an automation suite',
|
|
144
|
+
changedState: false,
|
|
145
|
+
retrySafe: true,
|
|
146
|
+
nextSteps: ['Pass projectRoot="/abs/path" (bootstrap defaults on), or a prepared sessionId.'],
|
|
147
|
+
}, appMapUri ? { appMapUri } : {});
|
|
176
148
|
}
|
|
177
|
-
|
|
178
|
-
|
|
149
|
+
const boot = await bootstrapFeatureExecution({
|
|
150
|
+
server,
|
|
151
|
+
sessions,
|
|
152
|
+
projectRoot: projectRoot?.trim() ?? session.root,
|
|
153
|
+
feature: feature ?? 'create automation suite',
|
|
154
|
+
platform: platform === 'android' || platform === 'ios' ? platform : undefined,
|
|
155
|
+
device,
|
|
156
|
+
consentId,
|
|
157
|
+
approve,
|
|
158
|
+
});
|
|
159
|
+
// Honest target blocker / consent request — but the static app map WAS still built, so the
|
|
160
|
+
// caller can read it even though no device/artifact is available (Fix 6 acceptance).
|
|
161
|
+
if (!boot.ok)
|
|
162
|
+
return withAppMapUri(boot.result, appMapUri);
|
|
163
|
+
session = boot.session;
|
|
164
|
+
driver = boot.driver;
|
|
165
|
+
if (!appMapUri && existsSync(session.root)) {
|
|
179
166
|
try {
|
|
180
|
-
|
|
167
|
+
appMapUri = ensurePrelaunchAppMap(session.root, { at: new Date().toISOString() }).appMapUri;
|
|
181
168
|
}
|
|
182
|
-
catch
|
|
183
|
-
|
|
169
|
+
catch {
|
|
170
|
+
/* best-effort */
|
|
184
171
|
}
|
|
185
172
|
}
|
|
186
173
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
failureCode: 'NO_RECORDED_ACTIONS',
|
|
195
|
-
nextSteps: [
|
|
196
|
-
'Run qa_test_this { goal: "create_automation_suite" } (smoke + explore records actions + builds the app map), then re-run qa_automation_generate.',
|
|
197
|
-
'Or drive the app with qa_act/qa_smoke/qa_explore first.',
|
|
198
|
-
],
|
|
199
|
-
}, appMapUri ? { appMapUri } : {});
|
|
200
|
-
}
|
|
201
|
-
const inputs = profileInputs(language, platform, integrateIntoProject);
|
|
202
|
-
const assembled = assembleAutomationSuite(session, { ...inputs, name, includeCi });
|
|
203
|
-
const validation = validateGeneratedSuite(assembled.files, {
|
|
204
|
-
brittlePct: assembled.model.audit.brittlePct,
|
|
205
|
-
brittleThreshold,
|
|
206
|
-
candidateOnly,
|
|
207
|
-
secrets: assembled.model.secrets,
|
|
208
|
-
});
|
|
209
|
-
const doSave = save !== false;
|
|
210
|
-
let written = [];
|
|
211
|
-
let targetDir = assembled.outputDir;
|
|
212
|
-
let integrated = false;
|
|
213
|
-
let skippedExisting = [];
|
|
214
|
-
if (doSave && integrateIntoProject) {
|
|
215
|
-
// Consent-gated project mutation; never overwrite existing files.
|
|
216
|
-
const projectDir = projectTestDir(session.root, assembled.profile.automationLanguage);
|
|
217
|
-
const affects = { projectDir, files: assembled.files.map((f) => f.path) };
|
|
218
|
-
const gate = consumeConsent(consentId, approve, { action: 'automation_project_write', affects });
|
|
219
|
-
if (!gate.approved) {
|
|
220
|
-
return requireConsent({
|
|
221
|
-
action: 'automation_project_write',
|
|
222
|
-
risk: 'medium',
|
|
223
|
-
exactCommand: `write ${assembled.files.length} files into ${projectDir} (no overwrite)`,
|
|
224
|
-
affects,
|
|
225
|
-
explain: `Write the generated Appium suite into your project at ${projectDir}? Existing files are never overwritten.`,
|
|
226
|
-
});
|
|
174
|
+
// Record durable actions via a bounded, safe exploration so generation has something to emit.
|
|
175
|
+
if (driver && !session.recordedActions.length) {
|
|
176
|
+
try {
|
|
177
|
+
await runExplore(sessions, session, driver, { goal: feature, maxScreens: 6, maxActions: 16, stopOnAuth: true });
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
log('warn', 'automation bootstrap exploration failed', { err: String(e) });
|
|
227
181
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!session)
|
|
185
|
+
return qaError({
|
|
186
|
+
what: 'Need a sessionId or projectRoot',
|
|
187
|
+
changedState: false,
|
|
188
|
+
retrySafe: true,
|
|
189
|
+
nextSteps: ['Pass a prepared sessionId, or projectRoot to bootstrap.'],
|
|
190
|
+
}, appMapUri ? { appMapUri } : {});
|
|
191
|
+
if (!session.recordedActions.length) {
|
|
192
|
+
return qaError({
|
|
193
|
+
what: 'No recorded actions to turn into an automation suite (bootstrap could not record any)',
|
|
194
|
+
changedState: false,
|
|
195
|
+
retrySafe: true,
|
|
196
|
+
failureCode: 'NO_RECORDED_ACTIONS',
|
|
197
|
+
nextSteps: [
|
|
198
|
+
'Run qa_test_this { goal: "create_automation_suite" } (smoke + explore records actions + builds the app map), then re-run qa_generate target:"appium".',
|
|
199
|
+
'Or drive the app with qa_act/qa_smoke/qa_explore first.',
|
|
200
|
+
],
|
|
201
|
+
}, appMapUri ? { appMapUri } : {});
|
|
202
|
+
}
|
|
203
|
+
const inputs = profileInputs(language, platform, integrateIntoProject);
|
|
204
|
+
const assembled = assembleAutomationSuite(session, { ...inputs, name, includeCi });
|
|
205
|
+
const validation = validateGeneratedSuite(assembled.files, {
|
|
206
|
+
brittlePct: assembled.model.audit.brittlePct,
|
|
207
|
+
brittleThreshold,
|
|
208
|
+
candidateOnly,
|
|
209
|
+
secrets: assembled.model.secrets,
|
|
210
|
+
});
|
|
211
|
+
const doSave = save !== false;
|
|
212
|
+
let written = [];
|
|
213
|
+
let targetDir = assembled.outputDir;
|
|
214
|
+
let integrated = false;
|
|
215
|
+
let skippedExisting = [];
|
|
216
|
+
if (doSave && integrateIntoProject) {
|
|
217
|
+
// Consent-gated project mutation; never overwrite existing files.
|
|
218
|
+
const projectDir = projectTestDir(session.root, assembled.profile.automationLanguage);
|
|
219
|
+
const affects = { projectDir, files: assembled.files.map((f) => f.path) };
|
|
220
|
+
const gate = consumeConsent(consentId, approve, { action: 'automation_project_write', affects });
|
|
221
|
+
if (!gate.approved) {
|
|
222
|
+
return requireConsent({
|
|
235
223
|
action: 'automation_project_write',
|
|
236
224
|
risk: 'medium',
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
225
|
+
exactCommand: `write ${assembled.files.length} files into ${projectDir} (no overwrite)`,
|
|
226
|
+
affects,
|
|
227
|
+
explain: `Write the generated Appium suite into your project at ${projectDir}? Existing files are never overwritten.`,
|
|
240
228
|
});
|
|
241
229
|
}
|
|
242
|
-
|
|
243
|
-
|
|
230
|
+
const res = writeProjectFilesNoOverwrite(session.root, projectDir, assembled.files);
|
|
231
|
+
written = res.written;
|
|
232
|
+
skippedExisting = res.skipped;
|
|
233
|
+
targetDir = projectDir;
|
|
234
|
+
integrated = true;
|
|
235
|
+
sessions.recordMutation(session, {
|
|
236
|
+
tool: 'qa_generate',
|
|
237
|
+
action: 'automation_project_write',
|
|
238
|
+
risk: 'medium',
|
|
239
|
+
target: affects,
|
|
240
|
+
consent: { required: true, consentId, approved: true },
|
|
241
|
+
status: 'executed',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else if (doSave) {
|
|
245
|
+
written = writeAutomationFiles(session.root, assembled.outputDir, assembled.files);
|
|
246
|
+
}
|
|
247
|
+
// Fix 7 — link the generated suite back into the durable app map + persistent suite, so future
|
|
248
|
+
// feature/ticket decisions know which flows are automated. Best-effort (warnings, never fatal).
|
|
249
|
+
let suiteDelta;
|
|
250
|
+
let suiteUri;
|
|
251
|
+
const automationWarnings = [];
|
|
252
|
+
if (doSave) {
|
|
253
|
+
const now = new Date().toISOString();
|
|
254
|
+
const screenNames = assembled.model.screens.map((s) => s.pageName || s.className);
|
|
255
|
+
try {
|
|
256
|
+
const fw = detectFramework(session.root);
|
|
257
|
+
const fallback = {
|
|
258
|
+
root: session.root,
|
|
259
|
+
gitRemote: null,
|
|
260
|
+
packageName: null,
|
|
261
|
+
workspaceTarget: null,
|
|
262
|
+
framework: fw,
|
|
263
|
+
platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'],
|
|
264
|
+
};
|
|
265
|
+
const existingMap = loadAppMap(session.root, fallback, now).map;
|
|
266
|
+
const links = existingMap ? deriveAutomationLinks(existingMap, screenNames) : { screenIds: [], featureIds: [] };
|
|
267
|
+
const suiteRef = {
|
|
268
|
+
name: name ?? `swipium-appium-${assembled.profile.automationLanguage}`,
|
|
269
|
+
path: targetDir,
|
|
270
|
+
framework: assembled.profile.automationLanguage === 'python' ? 'appium-python' : 'wdio',
|
|
271
|
+
linkedFeatureIds: links.featureIds,
|
|
272
|
+
linkedScreenIds: links.screenIds,
|
|
273
|
+
};
|
|
274
|
+
const linkRes = linkAutomationSuite(session.root, suiteRef, now);
|
|
275
|
+
if (linkRes.ok)
|
|
276
|
+
appMapUri = linkRes.appMapUri ?? appMapUri;
|
|
277
|
+
else
|
|
278
|
+
automationWarnings.push('app map not found — automation suite not linked into the map');
|
|
244
279
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
let suiteDelta;
|
|
248
|
-
let suiteUri;
|
|
249
|
-
const automationWarnings = [];
|
|
250
|
-
if (doSave) {
|
|
251
|
-
const now = new Date().toISOString();
|
|
252
|
-
const screenNames = assembled.model.screens.map((s) => s.pageName || s.className);
|
|
253
|
-
try {
|
|
254
|
-
const fw = detectFramework(session.root);
|
|
255
|
-
const fallback = { root: session.root, gitRemote: null, packageName: null, workspaceTarget: null, framework: fw, platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'] };
|
|
256
|
-
const existingMap = loadAppMap(session.root, fallback, now).map;
|
|
257
|
-
const links = existingMap ? deriveAutomationLinks(existingMap, screenNames) : { screenIds: [], featureIds: [] };
|
|
258
|
-
const suiteRef = {
|
|
259
|
-
name: name ?? `swipium-appium-${assembled.profile.automationLanguage}`,
|
|
260
|
-
path: targetDir,
|
|
261
|
-
framework: assembled.profile.automationLanguage === 'python' ? 'appium-python' : 'wdio',
|
|
262
|
-
linkedFeatureIds: links.featureIds,
|
|
263
|
-
linkedScreenIds: links.screenIds,
|
|
264
|
-
};
|
|
265
|
-
const linkRes = linkAutomationSuite(session.root, suiteRef, now);
|
|
266
|
-
if (linkRes.ok)
|
|
267
|
-
appMapUri = linkRes.appMapUri ?? appMapUri;
|
|
268
|
-
else
|
|
269
|
-
automationWarnings.push('app map not found — automation suite not linked into the map');
|
|
270
|
-
}
|
|
271
|
-
catch (e) {
|
|
272
|
-
automationWarnings.push(`app-map automation link failed: ${String(e)}`);
|
|
273
|
-
}
|
|
274
|
-
try {
|
|
275
|
-
const fw2 = detectFramework(session.root);
|
|
276
|
-
const fb2 = { root: session.root, gitRemote: null, packageName: null, workspaceTarget: null, framework: fw2, platforms: fw2 === 'native-android' ? ['android'] : fw2 === 'native-ios' ? ['ios'] : ['android', 'ios'] };
|
|
277
|
-
const mapForLinks = loadAppMap(session.root, fb2, now).map;
|
|
278
|
-
const links = mapForLinks ? deriveAutomationLinks(mapForLinks, assembled.model.screens.map((s) => s.pageName || s.className)) : { screenIds: [], featureIds: [] };
|
|
279
|
-
const merge = mergeFromAutomation(session.root, session, { assembled, validationOk: validation.ok, links }, { source: 'generate', now, runId: `automation-${Date.now()}`, sourceUri: appMapUri, appId: session.appId, sessionId: session.id });
|
|
280
|
-
suiteDelta = merge.delta;
|
|
281
|
-
suiteUri = merge.suiteUri;
|
|
282
|
-
if (merge.warning)
|
|
283
|
-
automationWarnings.push(merge.warning);
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
|
-
automationWarnings.push(`suite automation merge failed: ${String(e)}`);
|
|
287
|
-
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
automationWarnings.push(`app-map automation link failed: ${String(e)}`);
|
|
288
282
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
283
|
+
try {
|
|
284
|
+
const fw2 = detectFramework(session.root);
|
|
285
|
+
const fb2 = {
|
|
286
|
+
root: session.root,
|
|
287
|
+
gitRemote: null,
|
|
288
|
+
packageName: null,
|
|
289
|
+
workspaceTarget: null,
|
|
290
|
+
framework: fw2,
|
|
291
|
+
platforms: fw2 === 'native-android' ? ['android'] : fw2 === 'native-ios' ? ['ios'] : ['android', 'ios'],
|
|
292
|
+
};
|
|
293
|
+
const mapForLinks = loadAppMap(session.root, fb2, now).map;
|
|
294
|
+
const links = mapForLinks
|
|
295
|
+
? deriveAutomationLinks(mapForLinks, assembled.model.screens.map((s) => s.pageName || s.className))
|
|
296
|
+
: { screenIds: [], featureIds: [] };
|
|
297
|
+
const merge = mergeFromAutomation(session.root, session, { assembled, validationOk: validation.ok, links }, { source: 'generate', now, runId: `automation-${Date.now()}`, sourceUri: appMapUri, appId: session.appId, sessionId: session.id });
|
|
298
|
+
suiteDelta = merge.delta;
|
|
299
|
+
suiteUri = merge.suiteUri;
|
|
300
|
+
if (merge.warning)
|
|
301
|
+
automationWarnings.push(merge.warning);
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
automationWarnings.push(`suite automation merge failed: ${String(e)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const a = assembled.model.audit;
|
|
308
|
+
const recommendation = integrated
|
|
309
|
+
? undefined
|
|
310
|
+
: assembled.profile.outputMode === 'project_native'
|
|
311
|
+
? 'Copy the suite into your project test dir, or re-run with integrateIntoProject:true (consent-gated).'
|
|
312
|
+
: `Generated under ${assembled.outputDir}. Install deps (${assembled.dependencyPatch.notes[0]}) and run per the README.`;
|
|
313
|
+
const summary = `✅ Generated ${assembled.profile.automationLanguage} Appium POM suite (${assembled.model.screens.length} screen(s), ${assembled.files.length} files).\n` +
|
|
314
|
+
`runner ${assembled.profile.testFramework}; backend ${assembled.profile.defaultBackend}${assembled.profile.secondaryBackend ? ` (+${assembled.profile.secondaryBackend})` : ''}\n` +
|
|
315
|
+
`locators: ${a.durable} durable / ${a.semi} semi / ${a.brittle} brittle (${a.brittlePct}% brittle)\n` +
|
|
316
|
+
`validation: ${validation.ok ? 'passed' : 'FAILED'} (${validation.findings.filter((f) => f.severity === 'error').length} error, ${validation.findings.filter((f) => f.severity === 'warning').length} warn); secrets ${validation.secretsClean ? 'clean' : 'LEAK'}\n` +
|
|
317
|
+
(written.length ? `wrote ${written.length} files under ${join(session.root, targetDir)}` : '(preview — pass save:true)') +
|
|
318
|
+
(skippedExisting.length
|
|
319
|
+
? `\nskipped ${skippedExisting.length} existing file(s) (no overwrite): ${skippedExisting.slice(0, 5).join(', ')}`
|
|
320
|
+
: '') +
|
|
321
|
+
(recommendation ? `\n${recommendation}` : '');
|
|
322
|
+
return qaOk({
|
|
323
|
+
profile: assembled.profile,
|
|
324
|
+
outputDir: targetDir,
|
|
325
|
+
integrated,
|
|
326
|
+
files: assembled.files.map((f) => f.path),
|
|
327
|
+
written,
|
|
328
|
+
skippedExisting,
|
|
329
|
+
screens: assembled.model.screens.map((s) => s.className),
|
|
330
|
+
variables: assembled.model.variables,
|
|
331
|
+
secrets: assembled.model.secrets,
|
|
332
|
+
audit: a,
|
|
333
|
+
validation,
|
|
334
|
+
dependencyPatch: assembled.dependencyPatch,
|
|
335
|
+
plan: assembled.plan,
|
|
336
|
+
appMapUri: appMapUri ?? null,
|
|
337
|
+
suiteDelta: suiteDelta ?? null,
|
|
338
|
+
suiteUri: suiteUri ?? null,
|
|
339
|
+
automationWarnings,
|
|
340
|
+
}, summary);
|
|
341
|
+
}
|
|
342
|
+
export function registerAutomationValidate(server, sessions) {
|
|
322
343
|
// ---- qa_automation_validate ----
|
|
323
344
|
server.registerTool('qa_automation_validate', {
|
|
324
345
|
title: 'Validate a generated automation suite',
|
|
325
|
-
description: 'Validate generated Appium code without a device: no inlined secrets, locator durability within threshold (unless candidate-only), capability config present, balanced JS/TS syntax, no empty files. Pass sessionId to validate the suite from recorded actions, or projectRoot to validate already-written files under .swipium/automation/<lang>/.',
|
|
346
|
+
description: 'Validate generated Appium code without a device: no inlined secrets, locator durability within threshold (unless candidate-only), capability config present, balanced JS/TS syntax, no empty files. Pass sessionId to validate the suite from recorded actions (as qa_generate target:"appium" would emit it), or projectRoot to validate already-written files under .swipium/automation/<lang>/.',
|
|
326
347
|
inputSchema: {
|
|
327
348
|
sessionId: z.string().optional(),
|
|
328
349
|
projectRoot: z.string().optional(),
|
|
@@ -354,7 +375,7 @@ export function registerAutomationGenerate(server, sessions) {
|
|
|
354
375
|
what: `No generated suite found under ${outputDir}`,
|
|
355
376
|
changedState: false,
|
|
356
377
|
retrySafe: true,
|
|
357
|
-
nextSteps: ['Run
|
|
378
|
+
nextSteps: ['Run qa_generate target:"appium" first, or pass a sessionId with recorded actions.'],
|
|
358
379
|
});
|
|
359
380
|
}
|
|
360
381
|
}
|
|
@@ -362,9 +383,23 @@ export function registerAutomationGenerate(server, sessions) {
|
|
|
362
383
|
const errors = validation.findings.filter((f) => f.severity === 'error');
|
|
363
384
|
const summary = `Validation ${validation.ok ? 'passed' : 'FAILED'}: ${errors.length} error(s), ${validation.findings.length - errors.length} warning(s) over ${validation.filesChecked} files.\n` +
|
|
364
385
|
`secrets ${validation.secretsClean ? 'clean' : 'LEAK DETECTED'}; brittle ${validation.brittlePct}% (threshold ${validation.brittleThreshold}%${validation.candidateOnly ? ', candidate-only' : ''})` +
|
|
365
|
-
(validation.findings.length
|
|
386
|
+
(validation.findings.length
|
|
387
|
+
? `\n` +
|
|
388
|
+
validation.findings
|
|
389
|
+
.slice(0, 12)
|
|
390
|
+
.map((f) => ` ${f.severity === 'error' ? '✗' : '⚠'} ${f.code}${f.file ? ` (${f.file})` : ''}: ${f.message}`)
|
|
391
|
+
.join('\n')
|
|
392
|
+
: '');
|
|
366
393
|
const payload = { validation };
|
|
367
|
-
return validation.ok
|
|
394
|
+
return validation.ok
|
|
395
|
+
? qaOk(payload, summary)
|
|
396
|
+
: qaError({
|
|
397
|
+
what: 'Generated suite failed validation',
|
|
398
|
+
changedState: false,
|
|
399
|
+
retrySafe: true,
|
|
400
|
+
failureCode: 'VALIDATION_FAILED',
|
|
401
|
+
nextSteps: errors.map((e) => e.message),
|
|
402
|
+
}, payload);
|
|
368
403
|
});
|
|
369
404
|
}
|
|
370
405
|
// ---- helpers that need root-only (no session) profile ----
|