playwright-intelligence-engine 0.0.0-stage → 2.0.1
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/LICENSE +191 -0
- package/README.md +229 -3
- package/bin/pw-intel.js +37 -0
- package/dist/authoring/author.d.ts +83 -0
- package/dist/authoring/author.d.ts.map +1 -0
- package/dist/authoring/generation-contract.d.ts +90 -0
- package/dist/authoring/generation-contract.d.ts.map +1 -0
- package/dist/authoring/hybrid-author.d.ts +6 -0
- package/dist/authoring/hybrid-author.d.ts.map +1 -0
- package/dist/authoring/index.d.ts +7 -0
- package/dist/authoring/index.d.ts.map +1 -0
- package/dist/authoring/llm-author.d.ts +28 -0
- package/dist/authoring/llm-author.d.ts.map +1 -0
- package/dist/authoring/naming/index.d.ts +4 -0
- package/dist/authoring/naming/index.d.ts.map +1 -0
- package/dist/authoring/naming/name-plan.d.ts +45 -0
- package/dist/authoring/naming/name-plan.d.ts.map +1 -0
- package/dist/authoring/naming/name-planner.d.ts +25 -0
- package/dist/authoring/naming/name-planner.d.ts.map +1 -0
- package/dist/authoring/naming/naming-linter.d.ts +20 -0
- package/dist/authoring/naming/naming-linter.d.ts.map +1 -0
- package/dist/authoring/template-author.d.ts +6 -0
- package/dist/authoring/template-author.d.ts.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/collectors/collector-interface.d.ts +7 -0
- package/dist/collectors/collector-interface.d.ts.map +1 -0
- package/dist/collectors/environment-collector.d.ts +12 -0
- package/dist/collectors/environment-collector.d.ts.map +1 -0
- package/dist/collectors/error-collector.d.ts +18 -0
- package/dist/collectors/error-collector.d.ts.map +1 -0
- package/dist/collectors/git-diff-collector.d.ts +20 -0
- package/dist/collectors/git-diff-collector.d.ts.map +1 -0
- package/dist/collectors/live-browser-collector.d.ts +45 -0
- package/dist/collectors/live-browser-collector.d.ts.map +1 -0
- package/dist/collectors/network-collector.d.ts +28 -0
- package/dist/collectors/network-collector.d.ts.map +1 -0
- package/dist/collectors/static-ast-collector.d.ts +32 -0
- package/dist/collectors/static-ast-collector.d.ts.map +1 -0
- package/dist/collectors/trace-collector.d.ts +12 -0
- package/dist/collectors/trace-collector.d.ts.map +1 -0
- package/dist/config/project-config.d.ts +33 -0
- package/dist/config/project-config.d.ts.map +1 -0
- package/dist/core/errors/engine-errors.d.ts +34 -0
- package/dist/core/errors/engine-errors.d.ts.map +1 -0
- package/dist/core/types/diagnostic-context.d.ts +151 -0
- package/dist/core/types/diagnostic-context.d.ts.map +1 -0
- package/dist/core/types/repair-evidence.d.ts +56 -0
- package/dist/core/types/repair-evidence.d.ts.map +1 -0
- package/dist/core/version.d.ts +13 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/diagnostics/diagnostic-contract.d.ts +52 -0
- package/dist/diagnostics/diagnostic-contract.d.ts.map +1 -0
- package/dist/diagnostics/diagnostic-report.d.ts +11 -0
- package/dist/diagnostics/diagnostic-report.d.ts.map +1 -0
- package/dist/diagnostics/evidence-correlator.d.ts +22 -0
- package/dist/diagnostics/evidence-correlator.d.ts.map +1 -0
- package/dist/diagnostics/external-failure-evidence.d.ts +18 -0
- package/dist/diagnostics/external-failure-evidence.d.ts.map +1 -0
- package/dist/diagnostics/failure-category.d.ts +4 -0
- package/dist/diagnostics/failure-category.d.ts.map +1 -0
- package/dist/diagnostics/failure-classifier.d.ts +58 -0
- package/dist/diagnostics/failure-classifier.d.ts.map +1 -0
- package/dist/evaluation/evaluation-scorecard.d.ts +72 -0
- package/dist/evaluation/evaluation-scorecard.d.ts.map +1 -0
- package/dist/evaluation/repository-inspector.d.ts +21 -0
- package/dist/evaluation/repository-inspector.d.ts.map +1 -0
- package/dist/evaluation/repository-runner.d.ts +24 -0
- package/dist/evaluation/repository-runner.d.ts.map +1 -0
- package/dist/evidence/evidence-model.d.ts +88 -0
- package/dist/evidence/evidence-model.d.ts.map +1 -0
- package/dist/evidence/evidence-normalizer.d.ts +19 -0
- package/dist/evidence/evidence-normalizer.d.ts.map +1 -0
- package/dist/evidence/evidence-redactor.d.ts +37 -0
- package/dist/evidence/evidence-redactor.d.ts.map +1 -0
- package/dist/framework/config-adapter.d.ts +28 -0
- package/dist/framework/config-adapter.d.ts.map +1 -0
- package/dist/gate/index.d.ts +21 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/oracle.d.ts +26 -0
- package/dist/gate/oracle.d.ts.map +1 -0
- package/dist/gate/quality.d.ts +27 -0
- package/dist/gate/quality.d.ts.map +1 -0
- package/dist/gate/safety.d.ts +28 -0
- package/dist/gate/safety.d.ts.map +1 -0
- package/dist/generator/architecture-generator.d.ts +55 -0
- package/dist/generator/architecture-generator.d.ts.map +1 -0
- package/dist/generator/assertion-quality-gate.d.ts +18 -0
- package/dist/generator/assertion-quality-gate.d.ts.map +1 -0
- package/dist/generator/candidate-writer.d.ts +34 -0
- package/dist/generator/candidate-writer.d.ts.map +1 -0
- package/dist/generator/test-generator.d.ts +26 -0
- package/dist/generator/test-generator.d.ts.map +1 -0
- package/dist/governance/audit-log.d.ts +23 -0
- package/dist/governance/audit-log.d.ts.map +1 -0
- package/dist/governance/governance-policy.d.ts +24 -0
- package/dist/governance/governance-policy.d.ts.map +1 -0
- package/dist/governance/team-dashboard.d.ts +51 -0
- package/dist/governance/team-dashboard.d.ts.map +1 -0
- package/dist/governance/workflow-audit.d.ts +13 -0
- package/dist/governance/workflow-audit.d.ts.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20318 -0
- package/dist/integrations/ci-summary.d.ts +14 -0
- package/dist/integrations/ci-summary.d.ts.map +1 -0
- package/dist/integrations/playwright-report-adapter.d.ts +22 -0
- package/dist/integrations/playwright-report-adapter.d.ts.map +1 -0
- package/dist/integrations/quality-gate.d.ts +35 -0
- package/dist/integrations/quality-gate.d.ts.map +1 -0
- package/dist/integrations/sarif-report.d.ts +48 -0
- package/dist/integrations/sarif-report.d.ts.map +1 -0
- package/dist/intelligence/autonomous-repair-workflow.d.ts +31 -0
- package/dist/intelligence/autonomous-repair-workflow.d.ts.map +1 -0
- package/dist/intelligence/business-intent.d.ts +58 -0
- package/dist/intelligence/business-intent.d.ts.map +1 -0
- package/dist/intelligence/evidence-bundle.d.ts +364 -0
- package/dist/intelligence/evidence-bundle.d.ts.map +1 -0
- package/dist/intelligence/failure-clustering.d.ts +38 -0
- package/dist/intelligence/failure-clustering.d.ts.map +1 -0
- package/dist/intelligence/generation-agent.d.ts +120 -0
- package/dist/intelligence/generation-agent.d.ts.map +1 -0
- package/dist/intelligence/generation-verifier.d.ts +151 -0
- package/dist/intelligence/generation-verifier.d.ts.map +1 -0
- package/dist/intelligence/llm-proposal.d.ts +377 -0
- package/dist/intelligence/llm-proposal.d.ts.map +1 -0
- package/dist/intelligence/locator-disambiguation-engine.d.ts +38 -0
- package/dist/intelligence/locator-disambiguation-engine.d.ts.map +1 -0
- package/dist/intelligence/model-contract.d.ts +86 -0
- package/dist/intelligence/model-contract.d.ts.map +1 -0
- package/dist/intelligence/model-provider.d.ts +75 -0
- package/dist/intelligence/model-provider.d.ts.map +1 -0
- package/dist/intelligence/project-intelligence.d.ts +22 -0
- package/dist/intelligence/project-intelligence.d.ts.map +1 -0
- package/dist/intelligence/repair-agent.d.ts +46 -0
- package/dist/intelligence/repair-agent.d.ts.map +1 -0
- package/dist/intelligence/repository-profile.d.ts +131 -0
- package/dist/intelligence/repository-profile.d.ts.map +1 -0
- package/dist/intelligence/suite-intelligence.d.ts +59 -0
- package/dist/intelligence/suite-intelligence.d.ts.map +1 -0
- package/dist/intelligence/test-intelligence.d.ts +44 -0
- package/dist/intelligence/test-intelligence.d.ts.map +1 -0
- package/dist/intelligence/trace-proposal-workflow.d.ts +30 -0
- package/dist/intelligence/trace-proposal-workflow.d.ts.map +1 -0
- package/dist/intelligence/verified-llm-repair-workflow.d.ts +21 -0
- package/dist/intelligence/verified-llm-repair-workflow.d.ts.map +1 -0
- package/dist/intelligence/workflow-planner.d.ts +278 -0
- package/dist/intelligence/workflow-planner.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +32 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/memory/stability-ledger.d.ts +46 -0
- package/dist/memory/stability-ledger.d.ts.map +1 -0
- package/dist/perception/app-model.d.ts +55 -0
- package/dist/perception/app-model.d.ts.map +1 -0
- package/dist/perception/crawler.d.ts +18 -0
- package/dist/perception/crawler.d.ts.map +1 -0
- package/dist/perception/index.d.ts +3 -0
- package/dist/perception/index.d.ts.map +1 -0
- package/dist/repair/approval-service.d.ts +139 -0
- package/dist/repair/approval-service.d.ts.map +1 -0
- package/dist/repair/assertion-hasher.d.ts +62 -0
- package/dist/repair/assertion-hasher.d.ts.map +1 -0
- package/dist/repair/ast-patcher.d.ts +36 -0
- package/dist/repair/ast-patcher.d.ts.map +1 -0
- package/dist/repair/browser-operation-executor.d.ts +27 -0
- package/dist/repair/browser-operation-executor.d.ts.map +1 -0
- package/dist/repair/diff-allowlist.d.ts +74 -0
- package/dist/repair/diff-allowlist.d.ts.map +1 -0
- package/dist/repair/intent-validator.d.ts +157 -0
- package/dist/repair/intent-validator.d.ts.map +1 -0
- package/dist/repair/repair-operations.d.ts +40 -0
- package/dist/repair/repair-operations.d.ts.map +1 -0
- package/dist/repair/repair-planner.d.ts +98 -0
- package/dist/repair/repair-planner.d.ts.map +1 -0
- package/dist/repair/repair-workflow.d.ts +42 -0
- package/dist/repair/repair-workflow.d.ts.map +1 -0
- package/dist/repair/safe-operation-compiler.d.ts +15 -0
- package/dist/repair/safe-operation-compiler.d.ts.map +1 -0
- package/dist/repair/source-mapper.d.ts +19 -0
- package/dist/repair/source-mapper.d.ts.map +1 -0
- package/dist/repair/statement-patcher.d.ts +75 -0
- package/dist/repair/statement-patcher.d.ts.map +1 -0
- package/dist/repair/verification-gates.d.ts +25 -0
- package/dist/repair/verification-gates.d.ts.map +1 -0
- package/dist/repair/workflow-plan.d.ts +35 -0
- package/dist/repair/workflow-plan.d.ts.map +1 -0
- package/dist/repair/workspace-promoter.d.ts +43 -0
- package/dist/repair/workspace-promoter.d.ts.map +1 -0
- package/dist/reproduce/diagnostic-rerunner.d.ts +40 -0
- package/dist/reproduce/diagnostic-rerunner.d.ts.map +1 -0
- package/dist/reproduce/direct-browser-investigator.d.ts +41 -0
- package/dist/reproduce/direct-browser-investigator.d.ts.map +1 -0
- package/dist/reproduce/interactive-workflow-runner.d.ts +43 -0
- package/dist/reproduce/interactive-workflow-runner.d.ts.map +1 -0
- package/dist/sandbox/dependency-strategy.d.ts +45 -0
- package/dist/sandbox/dependency-strategy.d.ts.map +1 -0
- package/dist/sandbox/execution-runner.d.ts +81 -0
- package/dist/sandbox/execution-runner.d.ts.map +1 -0
- package/dist/sandbox/worktree-manager.d.ts +51 -0
- package/dist/sandbox/worktree-manager.d.ts.map +1 -0
- package/dist/selector/abstention-policy.d.ts +34 -0
- package/dist/selector/abstention-policy.d.ts.map +1 -0
- package/dist/selector/candidate-extractor.d.ts +71 -0
- package/dist/selector/candidate-extractor.d.ts.map +1 -0
- package/dist/selector/live-locator-verifier.d.ts +28 -0
- package/dist/selector/live-locator-verifier.d.ts.map +1 -0
- package/dist/selector/repair-candidate-ranker.d.ts +10 -0
- package/dist/selector/repair-candidate-ranker.d.ts.map +1 -0
- package/dist/selector/selector-scorer.d.ts +43 -0
- package/dist/selector/selector-scorer.d.ts.map +1 -0
- package/dist/trace/adapters/adapter-registry.d.ts +18 -0
- package/dist/trace/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/trace/adapters/trace-adapter.d.ts +21 -0
- package/dist/trace/adapters/trace-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/unknown-adapter.d.ts +10 -0
- package/dist/trace/adapters/unknown-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v10-adapter.d.ts +10 -0
- package/dist/trace/adapters/v10-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v4-adapter.d.ts +11 -0
- package/dist/trace/adapters/v4-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v8-adapter.d.ts +10 -0
- package/dist/trace/adapters/v8-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v9-adapter.d.ts +10 -0
- package/dist/trace/adapters/v9-adapter.d.ts.map +1 -0
- package/dist/trace/archive-reader.d.ts +44 -0
- package/dist/trace/archive-reader.d.ts.map +1 -0
- package/dist/trace/snapshot-resolver.d.ts +55 -0
- package/dist/trace/snapshot-resolver.d.ts.map +1 -0
- package/dist/trace/trace-parser.d.ts +50 -0
- package/dist/trace/trace-parser.d.ts.map +1 -0
- package/dist/trace/version-detector.d.ts +20 -0
- package/dist/trace/version-detector.d.ts.map +1 -0
- package/package.json +76 -6
- package/skills/README.md +20 -0
- package/skills/pie-failure-investigation/SKILL.md +16 -0
- package/skills/pie-quality-engineer/SKILL.md +66 -0
- package/skills/pie-release-readiness/SKILL.md +22 -0
- package/skills/pie-repair-review/SKILL.md +21 -0
- package/skills/pie-trace-to-repair/SKILL.md +35 -0
- package/src/authoring/author.ts +81 -0
- package/src/authoring/generation-contract.ts +283 -0
- package/src/authoring/hybrid-author.ts +115 -0
- package/src/authoring/index.ts +7 -0
- package/src/authoring/llm-author.ts +282 -0
- package/src/authoring/naming/index.ts +4 -0
- package/src/authoring/naming/name-plan.ts +28 -0
- package/src/authoring/naming/name-planner.ts +339 -0
- package/src/authoring/naming/naming-linter.ts +292 -0
- package/src/authoring/template-author.ts +39 -0
- package/src/cli/index.ts +1172 -0
- package/src/collectors/collector-interface.ts +8 -0
- package/src/collectors/environment-collector.ts +103 -0
- package/src/collectors/error-collector.ts +177 -0
- package/src/collectors/git-diff-collector.ts +190 -0
- package/src/collectors/live-browser-collector.ts +412 -0
- package/src/collectors/network-collector.ts +90 -0
- package/src/collectors/static-ast-collector.ts +226 -0
- package/src/collectors/trace-collector.ts +66 -0
- package/src/config/project-config.ts +114 -0
- package/src/core/errors/engine-errors.ts +90 -0
- package/src/core/types/diagnostic-context.ts +94 -0
- package/src/core/types/repair-evidence.ts +45 -0
- package/src/core/version.ts +25 -0
- package/src/diagnostics/diagnostic-contract.ts +79 -0
- package/src/diagnostics/diagnostic-report.ts +204 -0
- package/src/diagnostics/evidence-correlator.ts +117 -0
- package/src/diagnostics/external-failure-evidence.ts +72 -0
- package/src/diagnostics/failure-category.ts +40 -0
- package/src/diagnostics/failure-classifier.ts +701 -0
- package/src/evaluation/evaluation-scorecard.ts +198 -0
- package/src/evaluation/repository-inspector.ts +143 -0
- package/src/evaluation/repository-runner.ts +197 -0
- package/src/evidence/evidence-model.ts +76 -0
- package/src/evidence/evidence-normalizer.ts +322 -0
- package/src/evidence/evidence-redactor.ts +218 -0
- package/src/framework/config-adapter.ts +131 -0
- package/src/gate/index.ts +43 -0
- package/src/gate/oracle.ts +69 -0
- package/src/gate/quality.ts +101 -0
- package/src/gate/safety.ts +94 -0
- package/src/generator/architecture-generator.ts +582 -0
- package/src/generator/assertion-quality-gate.ts +88 -0
- package/src/generator/candidate-writer.ts +288 -0
- package/src/generator/test-generator.ts +52 -0
- package/src/governance/audit-log.ts +115 -0
- package/src/governance/governance-policy.ts +72 -0
- package/src/governance/team-dashboard.ts +189 -0
- package/src/governance/workflow-audit.ts +72 -0
- package/src/index.ts +107 -0
- package/src/integrations/ci-summary.ts +37 -0
- package/src/integrations/playwright-report-adapter.ts +173 -0
- package/src/integrations/quality-gate.ts +95 -0
- package/src/integrations/sarif-report.ts +110 -0
- package/src/intelligence/autonomous-repair-workflow.ts +139 -0
- package/src/intelligence/business-intent.ts +163 -0
- package/src/intelligence/evidence-bundle.ts +300 -0
- package/src/intelligence/failure-clustering.ts +134 -0
- package/src/intelligence/generation-agent.ts +561 -0
- package/src/intelligence/generation-verifier.ts +1115 -0
- package/src/intelligence/llm-proposal.ts +519 -0
- package/src/intelligence/locator-disambiguation-engine.ts +331 -0
- package/src/intelligence/model-contract.ts +205 -0
- package/src/intelligence/model-provider.ts +348 -0
- package/src/intelligence/project-intelligence.ts +140 -0
- package/src/intelligence/repair-agent.ts +297 -0
- package/src/intelligence/repository-profile.ts +796 -0
- package/src/intelligence/suite-intelligence.ts +305 -0
- package/src/intelligence/test-intelligence.ts +136 -0
- package/src/intelligence/trace-proposal-workflow.ts +318 -0
- package/src/intelligence/verified-llm-repair-workflow.ts +48 -0
- package/src/intelligence/workflow-planner.ts +966 -0
- package/src/mcp/server.ts +970 -0
- package/src/memory/stability-ledger.ts +243 -0
- package/src/perception/app-model.ts +32 -0
- package/src/perception/crawler.ts +263 -0
- package/src/perception/index.ts +3 -0
- package/src/repair/approval-service.ts +541 -0
- package/src/repair/assertion-hasher.ts +386 -0
- package/src/repair/ast-patcher.ts +223 -0
- package/src/repair/browser-operation-executor.ts +263 -0
- package/src/repair/diff-allowlist.ts +612 -0
- package/src/repair/intent-validator.ts +761 -0
- package/src/repair/repair-operations.ts +65 -0
- package/src/repair/repair-planner.ts +467 -0
- package/src/repair/repair-workflow.ts +241 -0
- package/src/repair/safe-operation-compiler.ts +68 -0
- package/src/repair/source-mapper.ts +156 -0
- package/src/repair/statement-patcher.ts +544 -0
- package/src/repair/verification-gates.ts +46 -0
- package/src/repair/workflow-plan.ts +59 -0
- package/src/repair/workspace-promoter.ts +313 -0
- package/src/reproduce/diagnostic-rerunner.ts +287 -0
- package/src/reproduce/direct-browser-investigator.ts +208 -0
- package/src/reproduce/interactive-workflow-runner.ts +828 -0
- package/src/sandbox/dependency-strategy.ts +215 -0
- package/src/sandbox/execution-runner.ts +408 -0
- package/src/sandbox/worktree-manager.ts +287 -0
- package/src/selector/abstention-policy.ts +175 -0
- package/src/selector/candidate-extractor.ts +343 -0
- package/src/selector/live-locator-verifier.ts +139 -0
- package/src/selector/repair-candidate-ranker.ts +92 -0
- package/src/selector/selector-scorer.ts +344 -0
- package/src/trace/adapters/adapter-registry.ts +49 -0
- package/src/trace/adapters/trace-adapter.ts +28 -0
- package/src/trace/adapters/unknown-adapter.ts +50 -0
- package/src/trace/adapters/v10-adapter.ts +39 -0
- package/src/trace/adapters/v4-adapter.ts +31 -0
- package/src/trace/adapters/v8-adapter.ts +42 -0
- package/src/trace/adapters/v9-adapter.ts +39 -0
- package/src/trace/archive-reader.ts +246 -0
- package/src/trace/snapshot-resolver.ts +308 -0
- package/src/trace/trace-parser.ts +467 -0
- package/src/trace/version-detector.ts +81 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { Project, Node, CallExpression } from 'ts-morph';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export interface CompositeIntentFingerprint {
|
|
5
|
+
assertionFingerprint: string;
|
|
6
|
+
testBodyFingerprint: string;
|
|
7
|
+
fixtureFingerprint: string;
|
|
8
|
+
compositeHash: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class AssertionHasher {
|
|
12
|
+
/**
|
|
13
|
+
* Computes a canonical, whitespace-and-formatting-insensitive SHA-256 hash of all expect() assertions.
|
|
14
|
+
* Ensures that code repairs never mutate, delete, or weaken test assertions.
|
|
15
|
+
* Preserves exact whitespace and characters inside string literals.
|
|
16
|
+
*/
|
|
17
|
+
public static computeFingerprint(testFilePath: string): string {
|
|
18
|
+
return AssertionHasher.computeAssertionFingerprint(testFilePath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Discovers assertion identifiers imported from @playwright/test (e.g. `expect`, or aliased `expect as verify`).
|
|
23
|
+
*/
|
|
24
|
+
public static discoverAssertionIdentifiers(sourceNode: Node): Set<string> {
|
|
25
|
+
const identifiers = new Set(['expect']);
|
|
26
|
+
const root = sourceNode.getSourceFile();
|
|
27
|
+
if (root) {
|
|
28
|
+
for (const imp of root.getImportDeclarations()) {
|
|
29
|
+
const spec = imp.getModuleSpecifierValue();
|
|
30
|
+
if (spec === '@playwright/test' || spec.endsWith('/test')) {
|
|
31
|
+
for (const named of imp.getNamedImports()) {
|
|
32
|
+
if (named.getName() === 'expect') {
|
|
33
|
+
const alias = named.getAliasNode()?.getText();
|
|
34
|
+
if (alias) {
|
|
35
|
+
identifiers.add(alias);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return identifiers;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes SHA-256 of all assertion expressions in the file.
|
|
47
|
+
* Identifies assertions semantically (calls to expect(), expect.soft(), expect.poll(), or aliased verify()).
|
|
48
|
+
* Preserves exact string literal contents and argument ordering.
|
|
49
|
+
*/
|
|
50
|
+
public static computeAssertionFingerprint(testFilePath: string): string {
|
|
51
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
52
|
+
const sourceFile = project.addSourceFileAtPath(testFilePath);
|
|
53
|
+
const identifiers = AssertionHasher.discoverAssertionIdentifiers(sourceFile);
|
|
54
|
+
|
|
55
|
+
const assertionRecords: any[] = [];
|
|
56
|
+
sourceFile.forEachDescendant((node) => {
|
|
57
|
+
if (Node.isCallExpression(node)) {
|
|
58
|
+
if (AssertionHasher.isOutermostAssertionCall(node, identifiers)) {
|
|
59
|
+
assertionRecords.push(AssertionHasher.canonicalizeAssertion(node));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const canonicalString = JSON.stringify(assertionRecords);
|
|
65
|
+
return crypto.createHash('sha256').update(canonicalString).digest('hex');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Semantically identifies whether a CallExpression is an assertion API call (or part of an assertion chain).
|
|
70
|
+
*/
|
|
71
|
+
public static isAssertionCall(callExpr: CallExpression, knownIdentifiers?: Set<string>): boolean {
|
|
72
|
+
const identifiers = knownIdentifiers ?? AssertionHasher.discoverAssertionIdentifiers(callExpr);
|
|
73
|
+
const expr = callExpr.getExpression();
|
|
74
|
+
const exprText = expr.getText();
|
|
75
|
+
|
|
76
|
+
for (const id of identifiers) {
|
|
77
|
+
// Matches id(...), id.soft(...), id.poll(...)
|
|
78
|
+
const idPattern = new RegExp(`^${id}(\\.(soft|poll))?$`);
|
|
79
|
+
if (idPattern.test(exprText)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// Matches chained matchers like id(...).toBe(...) or await id.soft(...).toHaveText(...)
|
|
83
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
84
|
+
const rootText = expr.getText();
|
|
85
|
+
if (
|
|
86
|
+
rootText.startsWith(`${id}(`) ||
|
|
87
|
+
rootText.startsWith(`${id}.soft(`) ||
|
|
88
|
+
rootText.startsWith(`${id}.poll(`)
|
|
89
|
+
) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Determines whether a CallExpression is the outermost assertion call in a chain.
|
|
100
|
+
* Prevents recording duplicate partial assertion calls (e.g. both `expect(x)` and `expect(x).toBe(y)`).
|
|
101
|
+
*/
|
|
102
|
+
public static isOutermostAssertionCall(callExpr: CallExpression, knownIdentifiers?: Set<string>): boolean {
|
|
103
|
+
const identifiers = knownIdentifiers ?? AssertionHasher.discoverAssertionIdentifiers(callExpr);
|
|
104
|
+
if (!AssertionHasher.isAssertionCall(callExpr, identifiers)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if the parent is a PropertyAccessExpression that is itself invoked by a parent CallExpression
|
|
109
|
+
const parent = callExpr.getParent();
|
|
110
|
+
if (parent && Node.isPropertyAccessExpression(parent)) {
|
|
111
|
+
const grandParent = parent.getParent();
|
|
112
|
+
if (grandParent && Node.isCallExpression(grandParent)) {
|
|
113
|
+
if (AssertionHasher.isAssertionCall(grandParent, identifiers)) {
|
|
114
|
+
return false; // This is an inner receiver in a chained assertion call
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Computes SHA-256 structural fingerprint of test body statements, control flow, conditions and operators.
|
|
124
|
+
* Ensures statement sequence, logical operators, condition expressions, and argument structure remain invariant.
|
|
125
|
+
*/
|
|
126
|
+
public static computeTestBodyFingerprint(testFilePath: string): string {
|
|
127
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
128
|
+
const sourceFile = project.addSourceFileAtPath(testFilePath);
|
|
129
|
+
const identifiers = AssertionHasher.discoverAssertionIdentifiers(sourceFile);
|
|
130
|
+
|
|
131
|
+
const structuralTokens: any[] = [];
|
|
132
|
+
|
|
133
|
+
sourceFile.forEachDescendant((node) => {
|
|
134
|
+
// 1. Control flow statements
|
|
135
|
+
if (
|
|
136
|
+
Node.isIfStatement(node) ||
|
|
137
|
+
Node.isForStatement(node) ||
|
|
138
|
+
Node.isForOfStatement(node) ||
|
|
139
|
+
Node.isForInStatement(node) ||
|
|
140
|
+
Node.isWhileStatement(node) ||
|
|
141
|
+
Node.isDoStatement(node) ||
|
|
142
|
+
Node.isTryStatement(node) ||
|
|
143
|
+
Node.isSwitchStatement(node) ||
|
|
144
|
+
Node.isReturnStatement(node)
|
|
145
|
+
) {
|
|
146
|
+
const conditionText = Node.isIfStatement(node) || Node.isWhileStatement(node)
|
|
147
|
+
? node.getExpression().getText().trim()
|
|
148
|
+
: '';
|
|
149
|
+
structuralTokens.push({
|
|
150
|
+
kind: node.getKindName(),
|
|
151
|
+
childCount: node.getChildCount(),
|
|
152
|
+
condition: conditionText,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// 2. Expression statements & binary/unary expressions (captures operators like !user.isActive vs user.isActive)
|
|
156
|
+
else if (Node.isBinaryExpression(node)) {
|
|
157
|
+
structuralTokens.push({
|
|
158
|
+
kind: 'BinaryExpression',
|
|
159
|
+
operator: node.getOperatorToken().getText(),
|
|
160
|
+
left: node.getLeft().getText().trim(),
|
|
161
|
+
right: node.getRight().getText().trim(),
|
|
162
|
+
});
|
|
163
|
+
} else if (Node.isPrefixUnaryExpression(node)) {
|
|
164
|
+
structuralTokens.push({
|
|
165
|
+
kind: 'PrefixUnaryExpression',
|
|
166
|
+
operator: node.getOperatorToken(),
|
|
167
|
+
operand: node.getOperand().getText().trim(),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// 3. Call expressions (non-assertion calls)
|
|
171
|
+
else if (Node.isCallExpression(node)) {
|
|
172
|
+
if (!AssertionHasher.isAssertionCall(node, identifiers)) {
|
|
173
|
+
structuralTokens.push({
|
|
174
|
+
kind: 'CallExpression',
|
|
175
|
+
expr: node.getExpression().getText().trim(),
|
|
176
|
+
argCount: node.getArguments().length,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const canonical = JSON.stringify(structuralTokens);
|
|
183
|
+
return crypto.createHash('sha256').update(canonical).digest('hex');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Computes SHA-256 of test fixtures, parameters, and input literals.
|
|
188
|
+
* Preserves exact ordering and associations (e.g. object property key-value pairs).
|
|
189
|
+
* Does NOT sort away key-value associations.
|
|
190
|
+
*/
|
|
191
|
+
public static computeFixtureFingerprint(testFilePath: string): string {
|
|
192
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
193
|
+
const sourceFile = project.addSourceFileAtPath(testFilePath);
|
|
194
|
+
|
|
195
|
+
const fixtureTokens: string[] = [];
|
|
196
|
+
|
|
197
|
+
sourceFile.forEachDescendant((node) => {
|
|
198
|
+
// 1. Property assignments: key + value preserved together (e.g. username: 'admin')
|
|
199
|
+
if (Node.isPropertyAssignment(node)) {
|
|
200
|
+
const key = node.getName();
|
|
201
|
+
const initializer = node.getInitializer();
|
|
202
|
+
const value = initializer ? initializer.getText() : '';
|
|
203
|
+
fixtureTokens.push(`Prop:${key}=${value}`);
|
|
204
|
+
}
|
|
205
|
+
// 2. Standalone literals not inside property assignment
|
|
206
|
+
else if (
|
|
207
|
+
(Node.isStringLiteral(node) ||
|
|
208
|
+
Node.isNumericLiteral(node) ||
|
|
209
|
+
Node.isTrueLiteral(node) ||
|
|
210
|
+
Node.isFalseLiteral(node)) &&
|
|
211
|
+
!Node.isPropertyAssignment(node.getParent())
|
|
212
|
+
) {
|
|
213
|
+
fixtureTokens.push(`Literal:${node.getKindName()}:${node.getText()}`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Sequence preserved (do not sort, to capture reordering of data properties)
|
|
218
|
+
const canonical = fixtureTokens.join(';;');
|
|
219
|
+
return crypto.createHash('sha256').update(canonical).digest('hex');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Computes full composite intent fingerprint combining assertions, test structure, and fixtures.
|
|
224
|
+
*/
|
|
225
|
+
public static computeCompositeFingerprint(testFilePath: string): CompositeIntentFingerprint {
|
|
226
|
+
const assertionFingerprint = AssertionHasher.computeAssertionFingerprint(testFilePath);
|
|
227
|
+
const testBodyFingerprint = AssertionHasher.computeTestBodyFingerprint(testFilePath);
|
|
228
|
+
const fixtureFingerprint = AssertionHasher.computeFixtureFingerprint(testFilePath);
|
|
229
|
+
|
|
230
|
+
const compositeHash = crypto
|
|
231
|
+
.createHash('sha256')
|
|
232
|
+
.update(`${assertionFingerprint}:${testBodyFingerprint}:${fixtureFingerprint}`)
|
|
233
|
+
.digest('hex');
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
assertionFingerprint,
|
|
237
|
+
testBodyFingerprint,
|
|
238
|
+
fixtureFingerprint,
|
|
239
|
+
compositeHash,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Extracts canonical assertion records from in-memory source.
|
|
245
|
+
*/
|
|
246
|
+
public static extractAssertionRecordsFromSource(sourceText: string, fileName = 'in-memory.spec.ts'): any[] {
|
|
247
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
248
|
+
const sourceFile = project.createSourceFile(fileName, sourceText, { overwrite: true });
|
|
249
|
+
const identifiers = AssertionHasher.discoverAssertionIdentifiers(sourceFile);
|
|
250
|
+
const records: any[] = [];
|
|
251
|
+
sourceFile.forEachDescendant((node) => {
|
|
252
|
+
if (Node.isCallExpression(node) && AssertionHasher.isOutermostAssertionCall(node, identifiers)) {
|
|
253
|
+
records.push(AssertionHasher.canonicalizeAssertion(node));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return records;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Computes composite intent fingerprint directly from in-memory source code.
|
|
261
|
+
*/
|
|
262
|
+
public static computeCompositeFingerprintFromSource(
|
|
263
|
+
sourceText: string,
|
|
264
|
+
fileName = 'in-memory.spec.ts'
|
|
265
|
+
): CompositeIntentFingerprint {
|
|
266
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
267
|
+
const sourceFile = project.createSourceFile(fileName, sourceText, { overwrite: true });
|
|
268
|
+
const identifiers = AssertionHasher.discoverAssertionIdentifiers(sourceFile);
|
|
269
|
+
|
|
270
|
+
// Assertions
|
|
271
|
+
const assertionRecords: any[] = [];
|
|
272
|
+
sourceFile.forEachDescendant((node) => {
|
|
273
|
+
if (Node.isCallExpression(node) && AssertionHasher.isOutermostAssertionCall(node, identifiers)) {
|
|
274
|
+
assertionRecords.push(AssertionHasher.canonicalizeAssertion(node));
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
const assertionFingerprint = crypto.createHash('sha256').update(JSON.stringify(assertionRecords)).digest('hex');
|
|
278
|
+
|
|
279
|
+
// Test body
|
|
280
|
+
const structuralTokens: any[] = [];
|
|
281
|
+
sourceFile.forEachDescendant((node) => {
|
|
282
|
+
if (
|
|
283
|
+
Node.isIfStatement(node) ||
|
|
284
|
+
Node.isForStatement(node) ||
|
|
285
|
+
Node.isForOfStatement(node) ||
|
|
286
|
+
Node.isForInStatement(node) ||
|
|
287
|
+
Node.isWhileStatement(node) ||
|
|
288
|
+
Node.isDoStatement(node) ||
|
|
289
|
+
Node.isTryStatement(node) ||
|
|
290
|
+
Node.isSwitchStatement(node) ||
|
|
291
|
+
Node.isReturnStatement(node)
|
|
292
|
+
) {
|
|
293
|
+
const conditionText = Node.isIfStatement(node) || Node.isWhileStatement(node)
|
|
294
|
+
? node.getExpression().getText().trim()
|
|
295
|
+
: '';
|
|
296
|
+
structuralTokens.push({
|
|
297
|
+
kind: node.getKindName(),
|
|
298
|
+
childCount: node.getChildCount(),
|
|
299
|
+
condition: conditionText,
|
|
300
|
+
});
|
|
301
|
+
} else if (Node.isBinaryExpression(node)) {
|
|
302
|
+
structuralTokens.push({
|
|
303
|
+
kind: 'BinaryExpression',
|
|
304
|
+
operator: node.getOperatorToken().getText(),
|
|
305
|
+
left: node.getLeft().getText().trim(),
|
|
306
|
+
right: node.getRight().getText().trim(),
|
|
307
|
+
});
|
|
308
|
+
} else if (Node.isPrefixUnaryExpression(node)) {
|
|
309
|
+
structuralTokens.push({
|
|
310
|
+
kind: 'PrefixUnaryExpression',
|
|
311
|
+
operator: node.getOperatorToken(),
|
|
312
|
+
operand: node.getOperand().getText().trim(),
|
|
313
|
+
});
|
|
314
|
+
} else if (Node.isCallExpression(node)) {
|
|
315
|
+
if (!AssertionHasher.isAssertionCall(node, identifiers)) {
|
|
316
|
+
structuralTokens.push({
|
|
317
|
+
kind: 'CallExpression',
|
|
318
|
+
expr: node.getExpression().getText().trim(),
|
|
319
|
+
argCount: node.getArguments().length,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
const testBodyFingerprint = crypto.createHash('sha256').update(JSON.stringify(structuralTokens)).digest('hex');
|
|
325
|
+
|
|
326
|
+
// Fixtures
|
|
327
|
+
const fixtureTokens: string[] = [];
|
|
328
|
+
sourceFile.forEachDescendant((node) => {
|
|
329
|
+
if (Node.isPropertyAssignment(node)) {
|
|
330
|
+
const key = node.getName();
|
|
331
|
+
const initializer = node.getInitializer();
|
|
332
|
+
const value = initializer ? initializer.getText() : '';
|
|
333
|
+
fixtureTokens.push(`Prop:${key}=${value}`);
|
|
334
|
+
} else if (
|
|
335
|
+
(Node.isStringLiteral(node) ||
|
|
336
|
+
Node.isNumericLiteral(node) ||
|
|
337
|
+
Node.isTrueLiteral(node) ||
|
|
338
|
+
Node.isFalseLiteral(node)) &&
|
|
339
|
+
!Node.isPropertyAssignment(node.getParent())
|
|
340
|
+
) {
|
|
341
|
+
fixtureTokens.push(`Literal:${node.getKindName()}:${node.getText()}`);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
const fixtureFingerprint = crypto.createHash('sha256').update(fixtureTokens.join(';;')).digest('hex');
|
|
345
|
+
|
|
346
|
+
const compositeHash = crypto
|
|
347
|
+
.createHash('sha256')
|
|
348
|
+
.update(`${assertionFingerprint}:${testBodyFingerprint}:${fixtureFingerprint}`)
|
|
349
|
+
.digest('hex');
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
assertionFingerprint,
|
|
353
|
+
testBodyFingerprint,
|
|
354
|
+
fixtureFingerprint,
|
|
355
|
+
compositeHash,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Canonicalizes an assertion node while strictly preserving string literals exactly.
|
|
361
|
+
*/
|
|
362
|
+
private static canonicalizeAssertion(callExpr: CallExpression): any {
|
|
363
|
+
// Normalizes formatting whitespace around tokens WITHOUT modifying string literal contents
|
|
364
|
+
const normalizeExpression = (str: string) =>
|
|
365
|
+
str.replace(/\s+/g, ' ').replace(/\s*([().,{}:])\s*/g, '$1').trim();
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
kind: callExpr.getKindName(),
|
|
369
|
+
expression: normalizeExpression(callExpr.getExpression().getText()),
|
|
370
|
+
arguments: callExpr.getArguments().map((arg) => {
|
|
371
|
+
if (Node.isStringLiteral(arg)) {
|
|
372
|
+
// Preserve EXACT text and whitespace inside string literals
|
|
373
|
+
return {
|
|
374
|
+
kind: 'StringLiteral',
|
|
375
|
+
value: arg.getLiteralValue(),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
kind: arg.getKindName(),
|
|
380
|
+
text: normalizeExpression(arg.getText()),
|
|
381
|
+
};
|
|
382
|
+
}),
|
|
383
|
+
typeArguments: callExpr.getTypeArguments().map((t) => normalizeExpression(t.getText())),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Project, SyntaxKind, Node } from 'ts-morph';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { AbstentionError } from '../core/errors/engine-errors.js';
|
|
5
|
+
import { DiffAllowlistValidator } from './diff-allowlist.js';
|
|
6
|
+
|
|
7
|
+
export interface PatchTarget {
|
|
8
|
+
filePath: string;
|
|
9
|
+
className: string;
|
|
10
|
+
memberName: string;
|
|
11
|
+
replacementExpression: string; // e.g. "this.page.getByRole('button', { name: 'Submit' })"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SupportedPOMPattern =
|
|
15
|
+
| 'property-initializer'
|
|
16
|
+
| 'getter-return'
|
|
17
|
+
| 'constructor-assignment'
|
|
18
|
+
| 'parameterless-factory'
|
|
19
|
+
| 'locator-constant';
|
|
20
|
+
|
|
21
|
+
export interface PatchResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
pattern: SupportedPOMPattern;
|
|
24
|
+
originalExpression: string;
|
|
25
|
+
replacementExpression: string;
|
|
26
|
+
patchedSource?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class AstPatcher {
|
|
30
|
+
private project: Project;
|
|
31
|
+
|
|
32
|
+
constructor(tsConfigFilePath?: string) {
|
|
33
|
+
if (tsConfigFilePath) {
|
|
34
|
+
this.project = new Project({
|
|
35
|
+
tsConfigFilePath,
|
|
36
|
+
skipAddingFilesFromTsConfig: false,
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
this.project = new Project({
|
|
40
|
+
skipAddingFilesFromTsConfig: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static generateDiff(originalText: string, patchedText: string, fileName = 'target.ts'): string {
|
|
46
|
+
const origLines = originalText.split('\n');
|
|
47
|
+
const patchedLines = patchedText.split('\n');
|
|
48
|
+
const diffLines: string[] = [`--- a/${fileName}`, `+++ b/${fileName}`];
|
|
49
|
+
for (let i = 0; i < Math.max(origLines.length, patchedLines.length); i++) {
|
|
50
|
+
const orig = origLines[i];
|
|
51
|
+
const patch = patchedLines[i];
|
|
52
|
+
if (orig !== patch) {
|
|
53
|
+
if (orig !== undefined) diffLines.push(`- ${orig}`);
|
|
54
|
+
if (patch !== undefined) diffLines.push(`+ ${patch}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return diffLines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Surgical AST modification targeting Page Object locator definitions in-memory.
|
|
62
|
+
* Does NOT write to disk, enabling sandboxed validation and atomic diff checking.
|
|
63
|
+
*/
|
|
64
|
+
public patchPageObjectInMemory(sourceText: string, target: Omit<PatchTarget, 'filePath'>): {
|
|
65
|
+
patchedSource: string;
|
|
66
|
+
pattern: SupportedPOMPattern;
|
|
67
|
+
originalExpression: string;
|
|
68
|
+
diff: string;
|
|
69
|
+
} {
|
|
70
|
+
// 1. Validate locator expression grammar upfront
|
|
71
|
+
const grammarCheck = DiffAllowlistValidator.validateLocatorGrammar(target.replacementExpression);
|
|
72
|
+
if (!grammarCheck.isValid) {
|
|
73
|
+
throw new AbstentionError(
|
|
74
|
+
grammarCheck.reason ?? 'Invalid locator grammar',
|
|
75
|
+
'AST_INVALID_REPLACEMENT_EXPRESSION'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const tempProject = new Project({ skipAddingFilesFromTsConfig: true });
|
|
80
|
+
const sourceFile = tempProject.createSourceFile('memory.ts', sourceText);
|
|
81
|
+
const classDecl = sourceFile.getClass(target.className);
|
|
82
|
+
|
|
83
|
+
const createResult = (patchedSource: string, pattern: SupportedPOMPattern, originalExpression: string) => ({
|
|
84
|
+
patchedSource,
|
|
85
|
+
pattern,
|
|
86
|
+
originalExpression,
|
|
87
|
+
diff: AstPatcher.generateDiff(sourceText, patchedSource),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Pattern 5: Top-level constant
|
|
91
|
+
if (!classDecl) {
|
|
92
|
+
const variableDecl = sourceFile.getVariableDeclaration(target.memberName);
|
|
93
|
+
if (variableDecl && variableDecl.getInitializer()) {
|
|
94
|
+
const originalExpression = variableDecl.getInitializer()!.getText();
|
|
95
|
+
variableDecl.setInitializer(target.replacementExpression);
|
|
96
|
+
return createResult(sourceFile.getFullText(), 'locator-constant', originalExpression);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new AbstentionError(
|
|
100
|
+
`Class '${target.className}' not found in source AST`,
|
|
101
|
+
'AST_CLASS_NOT_FOUND'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Pattern 1: Property Initializer
|
|
106
|
+
const property = classDecl.getProperty(target.memberName);
|
|
107
|
+
if (property && property.getInitializer()) {
|
|
108
|
+
const originalExpression = property.getInitializer()!.getText();
|
|
109
|
+
property.setInitializer(target.replacementExpression);
|
|
110
|
+
return createResult(sourceFile.getFullText(), 'property-initializer', originalExpression);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Pattern 2: Getter Method
|
|
114
|
+
const getter = classDecl.getGetAccessor(target.memberName);
|
|
115
|
+
if (getter) {
|
|
116
|
+
const returnStatements = getter.getDescendantsOfKind(SyntaxKind.ReturnStatement);
|
|
117
|
+
if (returnStatements.length !== 1) {
|
|
118
|
+
throw new AbstentionError(
|
|
119
|
+
`Getter '${target.memberName}' contains complex control flow (${returnStatements.length} return statements)`,
|
|
120
|
+
'AST_COMPLEX_CONTROL_FLOW'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const returnExpr = returnStatements[0]!.getExpression();
|
|
124
|
+
if (!returnExpr) {
|
|
125
|
+
throw new AbstentionError(
|
|
126
|
+
`Getter '${target.memberName}' return statement has no expression`,
|
|
127
|
+
'AST_EMPTY_RETURN'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const originalExpression = returnExpr.getText();
|
|
131
|
+
returnExpr.replaceWithText(target.replacementExpression);
|
|
132
|
+
return createResult(sourceFile.getFullText(), 'getter-return', originalExpression);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pattern 3: Constructor Assignment
|
|
136
|
+
for (const ctor of classDecl.getConstructors()) {
|
|
137
|
+
for (const statement of ctor.getStatements()) {
|
|
138
|
+
if (Node.isExpressionStatement(statement)) {
|
|
139
|
+
const expr = statement.getExpression();
|
|
140
|
+
if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
|
|
141
|
+
const leftText = expr.getLeft().getText();
|
|
142
|
+
if (leftText === `this.${target.memberName}`) {
|
|
143
|
+
const originalExpression = expr.getRight().getText();
|
|
144
|
+
expr.getRight().replaceWithText(target.replacementExpression);
|
|
145
|
+
return createResult(sourceFile.getFullText(), 'constructor-assignment', originalExpression);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Pattern 4: Parameterless Locator Factory Method
|
|
153
|
+
const method = classDecl.getMethod(target.memberName);
|
|
154
|
+
if (method) {
|
|
155
|
+
if (method.getParameters().length === 0) {
|
|
156
|
+
const returnStatements = method.getDescendantsOfKind(SyntaxKind.ReturnStatement);
|
|
157
|
+
if (returnStatements.length === 1 && returnStatements[0]?.getExpression()) {
|
|
158
|
+
const returnExpr = returnStatements[0]!.getExpression()!;
|
|
159
|
+
const originalExpression = returnExpr.getText();
|
|
160
|
+
returnExpr.replaceWithText(target.replacementExpression);
|
|
161
|
+
return createResult(sourceFile.getFullText(), 'parameterless-factory', originalExpression);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
throw new AbstentionError(
|
|
165
|
+
`Method '${target.memberName}' has ${method.getParameters().length} parameter(s). Dynamic parameterized locators cannot be safely patched.`,
|
|
166
|
+
'AST_PARAMETERIZED_LOCATOR'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new AbstentionError(
|
|
172
|
+
`Locator member '${target.memberName}' not found in class '${target.className}' across supported patterns.`,
|
|
173
|
+
'AST_UNSUPPORTED_PATTERN'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Surgical AST modification targeting Page Object locator definitions.
|
|
179
|
+
* Performs in-memory patch validation, then atomically writes to disk
|
|
180
|
+
* using a temporary file and atomic rename to guarantee transactional consistency.
|
|
181
|
+
*/
|
|
182
|
+
public patchPageObject(target: PatchTarget): PatchResult {
|
|
183
|
+
const sourceFile = this.project.addSourceFileAtPath(target.filePath);
|
|
184
|
+
const originalText = sourceFile.getFullText();
|
|
185
|
+
|
|
186
|
+
// Patch in-memory first to validate pattern & locator grammar
|
|
187
|
+
const inMem = this.patchPageObjectInMemory(originalText, {
|
|
188
|
+
className: target.className,
|
|
189
|
+
memberName: target.memberName,
|
|
190
|
+
replacementExpression: target.replacementExpression,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Write to a temporary file in the same directory and atomically rename
|
|
194
|
+
const targetDir = path.dirname(target.filePath);
|
|
195
|
+
const tempFile = path.join(
|
|
196
|
+
targetDir,
|
|
197
|
+
`.${path.basename(target.filePath)}.${Date.now()}.${Math.random().toString(36).substring(2, 6)}.tmp`
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
fs.writeFileSync(tempFile, inMem.patchedSource, 'utf8');
|
|
202
|
+
fs.renameSync(tempFile, target.filePath);
|
|
203
|
+
sourceFile.refreshFromFileSystemSync();
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
pattern: inMem.pattern,
|
|
208
|
+
originalExpression: inMem.originalExpression,
|
|
209
|
+
replacementExpression: target.replacementExpression,
|
|
210
|
+
patchedSource: inMem.patchedSource,
|
|
211
|
+
};
|
|
212
|
+
} catch (err: any) {
|
|
213
|
+
if (fs.existsSync(tempFile)) {
|
|
214
|
+
try {
|
|
215
|
+
fs.unlinkSync(tempFile);
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore cleanup failure
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|