agentic-qe 3.7.6 → 3.7.8
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/.claude/helpers/statusline-v3.cjs +13 -1
- package/.claude/skills/skills-manifest.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +36 -0
- package/dist/cli/bundle.js +2052 -183
- package/dist/coordination/complexity-composition/index.d.ts +2 -0
- package/dist/coordination/complexity-composition/index.d.ts.map +1 -0
- package/dist/coordination/complexity-composition/index.js +2 -0
- package/dist/coordination/complexity-composition/index.js.map +1 -0
- package/dist/coordination/complexity-composition/team-composer.d.ts +72 -0
- package/dist/coordination/complexity-composition/team-composer.d.ts.map +1 -0
- package/dist/coordination/complexity-composition/team-composer.js +221 -0
- package/dist/coordination/complexity-composition/team-composer.js.map +1 -0
- package/dist/coordination/consensus/consensus-engine.d.ts +10 -1
- package/dist/coordination/consensus/consensus-engine.d.ts.map +1 -1
- package/dist/coordination/consensus/consensus-engine.js +31 -1
- package/dist/coordination/consensus/consensus-engine.js.map +1 -1
- package/dist/coordination/consensus/index.d.ts +1 -0
- package/dist/coordination/consensus/index.d.ts.map +1 -1
- package/dist/coordination/consensus/index.js +4 -0
- package/dist/coordination/consensus/index.js.map +1 -1
- package/dist/coordination/consensus/interfaces.d.ts +5 -0
- package/dist/coordination/consensus/interfaces.d.ts.map +1 -1
- package/dist/coordination/consensus/interfaces.js +1 -0
- package/dist/coordination/consensus/interfaces.js.map +1 -1
- package/dist/coordination/consensus/sycophancy-scorer.d.ts +62 -0
- package/dist/coordination/consensus/sycophancy-scorer.d.ts.map +1 -0
- package/dist/coordination/consensus/sycophancy-scorer.js +200 -0
- package/dist/coordination/consensus/sycophancy-scorer.js.map +1 -0
- package/dist/coordination/fleet-tiers/tier-selector.d.ts +20 -0
- package/dist/coordination/fleet-tiers/tier-selector.d.ts.map +1 -1
- package/dist/coordination/fleet-tiers/tier-selector.js +31 -0
- package/dist/coordination/fleet-tiers/tier-selector.js.map +1 -1
- package/dist/coordination/fleet-tiers/types.d.ts +2 -0
- package/dist/coordination/fleet-tiers/types.d.ts.map +1 -1
- package/dist/domains/test-execution/services/e2e/adaptive-locator-service.d.ts +71 -0
- package/dist/domains/test-execution/services/e2e/adaptive-locator-service.d.ts.map +1 -0
- package/dist/domains/test-execution/services/e2e/adaptive-locator-service.js +456 -0
- package/dist/domains/test-execution/services/e2e/adaptive-locator-service.js.map +1 -0
- package/dist/domains/test-execution/services/e2e/adaptive-locator-types.d.ts +81 -0
- package/dist/domains/test-execution/services/e2e/adaptive-locator-types.d.ts.map +1 -0
- package/dist/domains/test-execution/services/e2e/adaptive-locator-types.js +20 -0
- package/dist/domains/test-execution/services/e2e/adaptive-locator-types.js.map +1 -0
- package/dist/domains/test-execution/services/e2e/browser-orchestrator.d.ts +19 -0
- package/dist/domains/test-execution/services/e2e/browser-orchestrator.d.ts.map +1 -1
- package/dist/domains/test-execution/services/e2e/browser-orchestrator.js +82 -0
- package/dist/domains/test-execution/services/e2e/browser-orchestrator.js.map +1 -1
- package/dist/domains/test-execution/services/e2e/index.d.ts +2 -0
- package/dist/domains/test-execution/services/e2e/index.d.ts.map +1 -1
- package/dist/domains/test-execution/services/e2e/index.js +5 -0
- package/dist/domains/test-execution/services/e2e/index.js.map +1 -1
- package/dist/domains/test-execution/services/e2e/step-executors.d.ts +6 -0
- package/dist/domains/test-execution/services/e2e/step-executors.d.ts.map +1 -1
- package/dist/domains/test-execution/services/e2e/step-executors.js +17 -2
- package/dist/domains/test-execution/services/e2e/step-executors.js.map +1 -1
- package/dist/domains/test-execution/services/e2e/types.d.ts +18 -1
- package/dist/domains/test-execution/services/e2e/types.d.ts.map +1 -1
- package/dist/domains/test-execution/services/e2e/types.js.map +1 -1
- package/dist/domains/test-generation/blind-review/blind-review-orchestrator.d.ts +65 -0
- package/dist/domains/test-generation/blind-review/blind-review-orchestrator.d.ts.map +1 -0
- package/dist/domains/test-generation/blind-review/blind-review-orchestrator.js +189 -0
- package/dist/domains/test-generation/blind-review/blind-review-orchestrator.js.map +1 -0
- package/dist/domains/test-generation/blind-review/index.d.ts +2 -0
- package/dist/domains/test-generation/blind-review/index.d.ts.map +1 -0
- package/dist/domains/test-generation/blind-review/index.js +2 -0
- package/dist/domains/test-generation/blind-review/index.js.map +1 -0
- package/dist/domains/test-generation/gates/index.d.ts +2 -0
- package/dist/domains/test-generation/gates/index.d.ts.map +1 -0
- package/dist/domains/test-generation/gates/index.js +2 -0
- package/dist/domains/test-generation/gates/index.js.map +1 -0
- package/dist/domains/test-generation/gates/test-quality-gate.d.ts +85 -0
- package/dist/domains/test-generation/gates/test-quality-gate.d.ts.map +1 -0
- package/dist/domains/test-generation/gates/test-quality-gate.js +320 -0
- package/dist/domains/test-generation/gates/test-quality-gate.js.map +1 -0
- package/dist/domains/test-generation/interfaces.d.ts +3 -0
- package/dist/domains/test-generation/interfaces.d.ts.map +1 -1
- package/dist/domains/test-generation/pattern-injection/edge-case-injector.d.ts +68 -0
- package/dist/domains/test-generation/pattern-injection/edge-case-injector.d.ts.map +1 -0
- package/dist/domains/test-generation/pattern-injection/edge-case-injector.js +225 -0
- package/dist/domains/test-generation/pattern-injection/edge-case-injector.js.map +1 -0
- package/dist/domains/test-generation/pattern-injection/index.d.ts +2 -0
- package/dist/domains/test-generation/pattern-injection/index.d.ts.map +1 -0
- package/dist/domains/test-generation/pattern-injection/index.js +2 -0
- package/dist/domains/test-generation/pattern-injection/index.js.map +1 -0
- package/dist/domains/test-generation/services/test-generator.d.ts +6 -0
- package/dist/domains/test-generation/services/test-generator.d.ts.map +1 -1
- package/dist/domains/test-generation/services/test-generator.js +29 -0
- package/dist/domains/test-generation/services/test-generator.js.map +1 -1
- package/dist/integrations/agentic-flow/reasoning-bank/experience-replay.d.ts +8 -2
- package/dist/integrations/agentic-flow/reasoning-bank/experience-replay.d.ts.map +1 -1
- package/dist/integrations/agentic-flow/reasoning-bank/experience-replay.js +62 -38
- package/dist/integrations/agentic-flow/reasoning-bank/experience-replay.js.map +1 -1
- package/dist/integrations/browser/client-factory.d.ts +6 -1
- package/dist/integrations/browser/client-factory.d.ts.map +1 -1
- package/dist/integrations/browser/client-factory.js +37 -2
- package/dist/integrations/browser/client-factory.js.map +1 -1
- package/dist/integrations/browser/index.d.ts +5 -1
- package/dist/integrations/browser/index.d.ts.map +1 -1
- package/dist/integrations/browser/index.js +8 -1
- package/dist/integrations/browser/index.js.map +1 -1
- package/dist/integrations/browser/page-pool-types.d.ts +70 -0
- package/dist/integrations/browser/page-pool-types.d.ts.map +1 -0
- package/dist/integrations/browser/page-pool-types.js +19 -0
- package/dist/integrations/browser/page-pool-types.js.map +1 -0
- package/dist/integrations/browser/page-pool.d.ts +79 -0
- package/dist/integrations/browser/page-pool.d.ts.map +1 -0
- package/dist/integrations/browser/page-pool.js +288 -0
- package/dist/integrations/browser/page-pool.js.map +1 -0
- package/dist/integrations/browser/resource-blocking.d.ts +47 -0
- package/dist/integrations/browser/resource-blocking.d.ts.map +1 -0
- package/dist/integrations/browser/resource-blocking.js +195 -0
- package/dist/integrations/browser/resource-blocking.js.map +1 -0
- package/dist/integrations/browser/stealth/index.d.ts +8 -0
- package/dist/integrations/browser/stealth/index.d.ts.map +1 -0
- package/dist/integrations/browser/stealth/index.js +7 -0
- package/dist/integrations/browser/stealth/index.js.map +1 -0
- package/dist/integrations/browser/stealth/stealth-client.d.ts +51 -0
- package/dist/integrations/browser/stealth/stealth-client.d.ts.map +1 -0
- package/dist/integrations/browser/stealth/stealth-client.js +359 -0
- package/dist/integrations/browser/stealth/stealth-client.js.map +1 -0
- package/dist/integrations/browser/stealth/stealth-types.d.ts +35 -0
- package/dist/integrations/browser/stealth/stealth-types.d.ts.map +1 -0
- package/dist/integrations/browser/stealth/stealth-types.js +17 -0
- package/dist/integrations/browser/stealth/stealth-types.js.map +1 -0
- package/dist/integrations/browser/types.d.ts +13 -10
- package/dist/integrations/browser/types.d.ts.map +1 -1
- package/dist/integrations/browser/types.js.map +1 -1
- package/dist/learning/experience-capture.d.ts +12 -2
- package/dist/learning/experience-capture.d.ts.map +1 -1
- package/dist/learning/experience-capture.js +28 -35
- package/dist/learning/experience-capture.js.map +1 -1
- package/dist/learning/experience-consolidation.d.ts +74 -0
- package/dist/learning/experience-consolidation.d.ts.map +1 -0
- package/dist/learning/experience-consolidation.js +403 -0
- package/dist/learning/experience-consolidation.js.map +1 -0
- package/dist/mcp/bundle.js +3050 -341
- package/dist/routing/calibration/ema-calibrator.d.ts +93 -0
- package/dist/routing/calibration/ema-calibrator.d.ts.map +1 -0
- package/dist/routing/calibration/ema-calibrator.js +140 -0
- package/dist/routing/calibration/ema-calibrator.js.map +1 -0
- package/dist/routing/calibration/index.d.ts +2 -0
- package/dist/routing/calibration/index.d.ts.map +1 -0
- package/dist/routing/calibration/index.js +2 -0
- package/dist/routing/calibration/index.js.map +1 -0
- package/dist/routing/escalation/auto-escalation-tracker.d.ts +62 -0
- package/dist/routing/escalation/auto-escalation-tracker.d.ts.map +1 -0
- package/dist/routing/escalation/auto-escalation-tracker.js +116 -0
- package/dist/routing/escalation/auto-escalation-tracker.js.map +1 -0
- package/dist/routing/escalation/index.d.ts +2 -0
- package/dist/routing/escalation/index.d.ts.map +1 -0
- package/dist/routing/escalation/index.js +2 -0
- package/dist/routing/escalation/index.js.map +1 -0
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +4 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/routing-config.d.ts +4 -0
- package/dist/routing/routing-config.d.ts.map +1 -1
- package/dist/routing/routing-config.js +2 -0
- package/dist/routing/routing-config.js.map +1 -1
- package/dist/routing/routing-feedback.d.ts +35 -2
- package/dist/routing/routing-feedback.d.ts.map +1 -1
- package/dist/routing/routing-feedback.js +97 -3
- package/dist/routing/routing-feedback.js.map +1 -1
- package/dist/routing/types.d.ts +2 -0
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/types.js.map +1 -1
- package/dist/workers/workers/learning-consolidation.d.ts.map +1 -1
- package/dist/workers/workers/learning-consolidation.js +32 -0
- package/dist/workers/workers/learning-consolidation.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind Review Orchestrator for Test Generation
|
|
3
|
+
* Inspired by loki-mode blind review pattern
|
|
4
|
+
*
|
|
5
|
+
* Runs N independent test generation passes in parallel with varied
|
|
6
|
+
* temperatures, then deduplicates using Jaccard similarity on tokenized
|
|
7
|
+
* assertions to maximize test diversity.
|
|
8
|
+
*/
|
|
9
|
+
import { ok, err } from '../../../shared/types/index.js';
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
reviewerCount: 3,
|
|
12
|
+
deduplicationThreshold: 0.8,
|
|
13
|
+
timeoutMs: 30000,
|
|
14
|
+
varyTemperatures: true,
|
|
15
|
+
temperatures: [0.2, 0.5, 0.8],
|
|
16
|
+
};
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Tokenization & Jaccard Similarity
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Tokenize code into a set of meaningful tokens.
|
|
22
|
+
* Strips whitespace, splits on non-alphanumeric boundaries,
|
|
23
|
+
* and filters tokens shorter than 3 characters.
|
|
24
|
+
*/
|
|
25
|
+
export function tokenize(code) {
|
|
26
|
+
return new Set(code
|
|
27
|
+
.replace(/\s+/g, ' ')
|
|
28
|
+
.split(/[^a-zA-Z0-9_]+/)
|
|
29
|
+
.filter((t) => t.length > 2));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compute Jaccard similarity between two token sets.
|
|
33
|
+
* Returns 0 if both sets are empty.
|
|
34
|
+
*/
|
|
35
|
+
export function jaccardSimilarity(a, b) {
|
|
36
|
+
if (a.size === 0 && b.size === 0)
|
|
37
|
+
return 0;
|
|
38
|
+
let intersectionSize = 0;
|
|
39
|
+
const smaller = a.size <= b.size ? a : b;
|
|
40
|
+
const larger = a.size <= b.size ? b : a;
|
|
41
|
+
for (const token of smaller) {
|
|
42
|
+
if (larger.has(token)) {
|
|
43
|
+
intersectionSize++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const unionSize = a.size + b.size - intersectionSize;
|
|
47
|
+
return unionSize === 0 ? 0 : intersectionSize / unionSize;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Deduplicate tests using Jaccard similarity on tokenized test code.
|
|
51
|
+
* Groups tests by target file, then within each group keeps the
|
|
52
|
+
* test with the most assertions from each similarity cluster.
|
|
53
|
+
*/
|
|
54
|
+
export function deduplicateTests(tests, threshold) {
|
|
55
|
+
if (tests.length === 0)
|
|
56
|
+
return [];
|
|
57
|
+
// Group by source file
|
|
58
|
+
const bySource = new Map();
|
|
59
|
+
for (const test of tests) {
|
|
60
|
+
const key = test.sourceFile || '__ungrouped__';
|
|
61
|
+
const group = bySource.get(key) || [];
|
|
62
|
+
group.push(test);
|
|
63
|
+
bySource.set(key, group);
|
|
64
|
+
}
|
|
65
|
+
const deduplicated = [];
|
|
66
|
+
for (const group of bySource.values()) {
|
|
67
|
+
const tokenSets = group.map((t) => tokenize(t.testCode));
|
|
68
|
+
const used = new Set();
|
|
69
|
+
for (let i = 0; i < group.length; i++) {
|
|
70
|
+
if (used.has(i))
|
|
71
|
+
continue;
|
|
72
|
+
// Start a cluster with this test
|
|
73
|
+
const cluster = [i];
|
|
74
|
+
used.add(i);
|
|
75
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
76
|
+
if (used.has(j))
|
|
77
|
+
continue;
|
|
78
|
+
const similarity = jaccardSimilarity(tokenSets[i], tokenSets[j]);
|
|
79
|
+
if (similarity >= threshold) {
|
|
80
|
+
cluster.push(j);
|
|
81
|
+
used.add(j);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Keep the test with the most assertions from the cluster
|
|
85
|
+
let best = cluster[0];
|
|
86
|
+
for (const idx of cluster) {
|
|
87
|
+
if (group[idx].assertions > group[best].assertions) {
|
|
88
|
+
best = idx;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
deduplicated.push(group[best]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return deduplicated;
|
|
95
|
+
}
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Orchestrator
|
|
98
|
+
// ============================================================================
|
|
99
|
+
/**
|
|
100
|
+
* Wraps a promise with a timeout. Rejects if the promise does not
|
|
101
|
+
* settle within the given number of milliseconds.
|
|
102
|
+
*/
|
|
103
|
+
function withTimeout(promise, ms) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const timer = setTimeout(() => reject(new Error(`Reviewer timed out after ${ms}ms`)), ms);
|
|
106
|
+
promise.then((value) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolve(value);
|
|
109
|
+
}, (reason) => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
reject(reason);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export class BlindReviewOrchestrator {
|
|
116
|
+
service;
|
|
117
|
+
constructor(service) {
|
|
118
|
+
this.service = service;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run blind review: N independent generation passes in parallel,
|
|
122
|
+
* then deduplicate and merge results.
|
|
123
|
+
*/
|
|
124
|
+
async generateWithBlindReview(request, config) {
|
|
125
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
126
|
+
if (cfg.reviewerCount < 1) {
|
|
127
|
+
return err(new Error('reviewerCount must be at least 1'));
|
|
128
|
+
}
|
|
129
|
+
// Launch N parallel reviewers with varied temperatures
|
|
130
|
+
const reviewerPromises = Array.from({ length: cfg.reviewerCount }, (_, i) => {
|
|
131
|
+
const reviewerId = `reviewer-${i}`;
|
|
132
|
+
const start = Date.now();
|
|
133
|
+
// Vary the request per reviewer: each gets a different temperature
|
|
134
|
+
// to encourage diverse test generation outputs
|
|
135
|
+
const reviewerRequest = { ...request };
|
|
136
|
+
if (cfg.varyTemperatures && cfg.temperatures.length > 0) {
|
|
137
|
+
const temperature = cfg.temperatures[i % cfg.temperatures.length];
|
|
138
|
+
// Attach temperature hint as pattern metadata that the generator can use
|
|
139
|
+
reviewerRequest.patterns = [
|
|
140
|
+
...(request.patterns ?? []),
|
|
141
|
+
`__blind_review_temperature:${temperature}`,
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
const genPromise = this.service
|
|
145
|
+
.generateTests(reviewerRequest)
|
|
146
|
+
.then((result) => {
|
|
147
|
+
const durationMs = Date.now() - start;
|
|
148
|
+
if (result.success) {
|
|
149
|
+
return {
|
|
150
|
+
reviewerId,
|
|
151
|
+
tests: result.value.tests,
|
|
152
|
+
durationMs,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { reviewerId, tests: [], durationMs };
|
|
156
|
+
});
|
|
157
|
+
return withTimeout(genPromise, cfg.timeoutMs).catch(() => ({
|
|
158
|
+
reviewerId,
|
|
159
|
+
tests: [],
|
|
160
|
+
durationMs: Date.now() - start,
|
|
161
|
+
}));
|
|
162
|
+
});
|
|
163
|
+
const reviewerOutputs = await Promise.allSettled(reviewerPromises);
|
|
164
|
+
// Collect fulfilled results
|
|
165
|
+
const outputs = reviewerOutputs
|
|
166
|
+
.filter((r) => r.status === 'fulfilled')
|
|
167
|
+
.map((r) => r.value);
|
|
168
|
+
// Gather all tests
|
|
169
|
+
const allTests = outputs.flatMap((o) => o.tests);
|
|
170
|
+
if (allTests.length === 0) {
|
|
171
|
+
return err(new Error('All reviewers failed to generate tests'));
|
|
172
|
+
}
|
|
173
|
+
// Deduplicate
|
|
174
|
+
const mergedTests = deduplicateTests(allTests, cfg.deduplicationThreshold);
|
|
175
|
+
const totalGenerated = allTests.length;
|
|
176
|
+
const afterDedup = mergedTests.length;
|
|
177
|
+
const uniquenessScore = totalGenerated === 0 ? 0 : afterDedup / totalGenerated;
|
|
178
|
+
return ok({
|
|
179
|
+
mergedTests,
|
|
180
|
+
reviewerOutputs: outputs,
|
|
181
|
+
stats: {
|
|
182
|
+
totalGenerated,
|
|
183
|
+
afterDedup,
|
|
184
|
+
uniquenessScore,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=blind-review-orchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blind-review-orchestrator.js","sourceRoot":"","sources":["../../../../src/domains/test-generation/blind-review/blind-review-orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/domains/test-generation/blind-review/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,gCAAgC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/domains/test-generation/blind-review/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,GAKjB,MAAM,gCAAgC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/domains/test-generation/gates/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/domains/test-generation/gates/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,GAKhB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Quality Gate - Mock Detector & Mutation Detector
|
|
3
|
+
* Inspired by loki-mode Gates 8 & 9
|
|
4
|
+
*
|
|
5
|
+
* Validates generated test code quality by detecting:
|
|
6
|
+
* 1. No source imports - test never imports from the source file
|
|
7
|
+
* 2. Tautological assertions - expect(true).toBe(true), expect(x).toBe(x)
|
|
8
|
+
* 3. Empty test bodies - it('...', () => {}) with no assertions
|
|
9
|
+
* 4. Mirrored assertions - expected values copy-pasted from source literals
|
|
10
|
+
*
|
|
11
|
+
* All detection is regex-based, no LLM calls.
|
|
12
|
+
*/
|
|
13
|
+
export type TestQualityIssueType = 'no-source-import' | 'tautological-assertion' | 'empty-test-body' | 'mirrored-assertion';
|
|
14
|
+
export interface TestQualityIssue {
|
|
15
|
+
type: TestQualityIssueType;
|
|
16
|
+
severity: 'error' | 'warning';
|
|
17
|
+
line?: number;
|
|
18
|
+
description: string;
|
|
19
|
+
suggestion: string;
|
|
20
|
+
}
|
|
21
|
+
export interface TestQualityGateResult {
|
|
22
|
+
passed: boolean;
|
|
23
|
+
issues: TestQualityIssue[];
|
|
24
|
+
score: number;
|
|
25
|
+
}
|
|
26
|
+
export interface TestQualityGateConfig {
|
|
27
|
+
/** Check for missing source imports (default: true) */
|
|
28
|
+
checkSourceImports: boolean;
|
|
29
|
+
/** Check for tautological assertions (default: true) */
|
|
30
|
+
checkTautologicalAssertions: boolean;
|
|
31
|
+
/** Check for empty test bodies (default: true) */
|
|
32
|
+
checkEmptyTestBodies: boolean;
|
|
33
|
+
/** Check for mirrored assertion values (default: true) */
|
|
34
|
+
checkMirroredAssertions: boolean;
|
|
35
|
+
/** Minimum score to pass (default: 60) */
|
|
36
|
+
minPassScore: number;
|
|
37
|
+
}
|
|
38
|
+
export declare class TestQualityGate {
|
|
39
|
+
private readonly config;
|
|
40
|
+
constructor(config?: Partial<TestQualityGateConfig>);
|
|
41
|
+
/**
|
|
42
|
+
* Validate generated test code quality.
|
|
43
|
+
*
|
|
44
|
+
* @param testCode - The generated test source code
|
|
45
|
+
* @param sourceFilePath - Path to the source file under test
|
|
46
|
+
* @param sourceCode - Optional source code content for mirrored assertion check
|
|
47
|
+
* @returns Gate result with pass/fail, issues, and score
|
|
48
|
+
*/
|
|
49
|
+
validate(testCode: string, sourceFilePath: string, sourceCode?: string): TestQualityGateResult;
|
|
50
|
+
/**
|
|
51
|
+
* Check whether the test code imports from the source file.
|
|
52
|
+
* If no import/require statement references the source file basename, flag an error.
|
|
53
|
+
*/
|
|
54
|
+
private detectMissingSourceImports;
|
|
55
|
+
/**
|
|
56
|
+
* Detect tautological assertions where the expected and actual values are identical.
|
|
57
|
+
* Examples: expect(true).toBe(true), expect(x).toBe(x), expect('a').toEqual('a')
|
|
58
|
+
*/
|
|
59
|
+
private detectTautologicalAssertions;
|
|
60
|
+
/**
|
|
61
|
+
* Detect empty test bodies - test/it blocks with no assertions or meaningful code.
|
|
62
|
+
* Matches: it('...', () => {}), test('...', () => { /* comment * / })
|
|
63
|
+
*/
|
|
64
|
+
private detectEmptyTestBodies;
|
|
65
|
+
/**
|
|
66
|
+
* Detect mirrored assertions - expected values that appear to be copy-pasted
|
|
67
|
+
* from source code literals rather than independently computed.
|
|
68
|
+
*/
|
|
69
|
+
private detectMirroredAssertions;
|
|
70
|
+
/**
|
|
71
|
+
* Extract non-trivial string and number literals from source code.
|
|
72
|
+
* Trivial values (true, false, null, undefined, 0, 1, '', "") are excluded.
|
|
73
|
+
*/
|
|
74
|
+
private extractNonTrivialLiterals;
|
|
75
|
+
/**
|
|
76
|
+
* Check if an assertion expected value matches a source literal.
|
|
77
|
+
*/
|
|
78
|
+
private literalMatches;
|
|
79
|
+
/**
|
|
80
|
+
* Calculate quality score from issues.
|
|
81
|
+
* Start at 100, subtract per issue (error: -20, warning: -5), clamp to 0.
|
|
82
|
+
*/
|
|
83
|
+
private calculateScore;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=test-quality-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-quality-gate.d.ts","sourceRoot":"","sources":["../../../../src/domains/test-generation/gates/test-quality-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,MAAM,MAAM,oBAAoB,GAC5B,kBAAkB,GAClB,wBAAwB,GACxB,iBAAiB,GACjB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,oBAAoB,CAAC;IAC3B,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,uDAAuD;IACvD,kBAAkB,EAAE,OAAO,CAAC;IAC5B,wDAAwD;IACxD,2BAA2B,EAAE,OAAO,CAAC;IACrC,kDAAkD;IAClD,oBAAoB,EAAE,OAAO,CAAC;IAC9B,0DAA0D;IAC1D,uBAAuB,EAAE,OAAO,CAAC;IACjC,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;CACtB;AAcD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAEnC,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC;IAInD;;;;;;;OAOG;IACH,QAAQ,CACN,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,qBAAqB;IA6BxB;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAkDlC;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IA2FpC;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAyC7B;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IA0ChC;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IA8BjC;;OAEG;IACH,OAAO,CAAC,cAAc;IAWtB;;;OAGG;IACH,OAAO,CAAC,cAAc;CAavB"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Quality Gate - Mock Detector & Mutation Detector
|
|
3
|
+
* Inspired by loki-mode Gates 8 & 9
|
|
4
|
+
*
|
|
5
|
+
* Validates generated test code quality by detecting:
|
|
6
|
+
* 1. No source imports - test never imports from the source file
|
|
7
|
+
* 2. Tautological assertions - expect(true).toBe(true), expect(x).toBe(x)
|
|
8
|
+
* 3. Empty test bodies - it('...', () => {}) with no assertions
|
|
9
|
+
* 4. Mirrored assertions - expected values copy-pasted from source literals
|
|
10
|
+
*
|
|
11
|
+
* All detection is regex-based, no LLM calls.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
checkSourceImports: true,
|
|
15
|
+
checkTautologicalAssertions: true,
|
|
16
|
+
checkEmptyTestBodies: true,
|
|
17
|
+
checkMirroredAssertions: true,
|
|
18
|
+
minPassScore: 60,
|
|
19
|
+
};
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// TestQualityGate
|
|
22
|
+
// ============================================================================
|
|
23
|
+
export class TestQualityGate {
|
|
24
|
+
config;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate generated test code quality.
|
|
30
|
+
*
|
|
31
|
+
* @param testCode - The generated test source code
|
|
32
|
+
* @param sourceFilePath - Path to the source file under test
|
|
33
|
+
* @param sourceCode - Optional source code content for mirrored assertion check
|
|
34
|
+
* @returns Gate result with pass/fail, issues, and score
|
|
35
|
+
*/
|
|
36
|
+
validate(testCode, sourceFilePath, sourceCode) {
|
|
37
|
+
const issues = [];
|
|
38
|
+
if (this.config.checkSourceImports) {
|
|
39
|
+
issues.push(...this.detectMissingSourceImports(testCode, sourceFilePath));
|
|
40
|
+
}
|
|
41
|
+
if (this.config.checkTautologicalAssertions) {
|
|
42
|
+
issues.push(...this.detectTautologicalAssertions(testCode));
|
|
43
|
+
}
|
|
44
|
+
if (this.config.checkEmptyTestBodies) {
|
|
45
|
+
issues.push(...this.detectEmptyTestBodies(testCode));
|
|
46
|
+
}
|
|
47
|
+
if (this.config.checkMirroredAssertions && sourceCode) {
|
|
48
|
+
issues.push(...this.detectMirroredAssertions(testCode, sourceCode));
|
|
49
|
+
}
|
|
50
|
+
const score = this.calculateScore(issues);
|
|
51
|
+
const passed = score >= this.config.minPassScore;
|
|
52
|
+
return { passed, issues, score };
|
|
53
|
+
}
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Detection Methods
|
|
56
|
+
// ============================================================================
|
|
57
|
+
/**
|
|
58
|
+
* Check whether the test code imports from the source file.
|
|
59
|
+
* If no import/require statement references the source file basename, flag an error.
|
|
60
|
+
*/
|
|
61
|
+
detectMissingSourceImports(testCode, sourceFilePath) {
|
|
62
|
+
// Extract basename without extension for matching
|
|
63
|
+
const parts = sourceFilePath.replace(/\\/g, '/').split('/');
|
|
64
|
+
const fileName = parts[parts.length - 1];
|
|
65
|
+
const baseName = fileName.replace(/\.(ts|js|tsx|jsx|mts|mjs|py)$/, '');
|
|
66
|
+
// Collect all import/require paths from the test code
|
|
67
|
+
const importPaths = [];
|
|
68
|
+
// ES import: import ... from '...'
|
|
69
|
+
const esImportRegex = /(?:import|from)\s+['"]([^'"]+)['"]/g;
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = esImportRegex.exec(testCode)) !== null) {
|
|
72
|
+
importPaths.push(match[1]);
|
|
73
|
+
}
|
|
74
|
+
// CommonJS require: require('...')
|
|
75
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
76
|
+
while ((match = requireRegex.exec(testCode)) !== null) {
|
|
77
|
+
importPaths.push(match[1]);
|
|
78
|
+
}
|
|
79
|
+
// Check if any import path references the source file
|
|
80
|
+
const hasSourceImport = importPaths.some((importPath) => {
|
|
81
|
+
const importBaseName = importPath
|
|
82
|
+
.replace(/\\/g, '/')
|
|
83
|
+
.split('/')
|
|
84
|
+
.pop()
|
|
85
|
+
?.replace(/\.(ts|js|tsx|jsx|mts|mjs)$/, '')
|
|
86
|
+
?.replace(/\.js$/, '');
|
|
87
|
+
return importBaseName === baseName;
|
|
88
|
+
});
|
|
89
|
+
if (!hasSourceImport && importPaths.length >= 0) {
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
type: 'no-source-import',
|
|
93
|
+
severity: 'error',
|
|
94
|
+
description: `Test does not import from source file "${baseName}". Tests that never reference the source under test are likely mock-only or dead code.`,
|
|
95
|
+
suggestion: `Add an import statement that references the source module, e.g.: import { ... } from './${baseName}.js'`,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Detect tautological assertions where the expected and actual values are identical.
|
|
103
|
+
* Examples: expect(true).toBe(true), expect(x).toBe(x), expect('a').toEqual('a')
|
|
104
|
+
*/
|
|
105
|
+
detectTautologicalAssertions(testCode) {
|
|
106
|
+
const issues = [];
|
|
107
|
+
const lines = testCode.split('\n');
|
|
108
|
+
const matchers = ['toBe', 'toEqual', 'toStrictEqual'];
|
|
109
|
+
const matcherPattern = matchers.join('|');
|
|
110
|
+
// Pattern 1: Literal boolean/null/undefined on both sides
|
|
111
|
+
// expect(true).toBe(true), expect(false).toEqual(false), etc.
|
|
112
|
+
const literalPattern = new RegExp(`expect\\s*\\(\\s*(true|false|null|undefined)\\s*\\)\\s*\\.\\s*(?:${matcherPattern})\\s*\\(\\s*\\1\\s*\\)`);
|
|
113
|
+
// Pattern 2: Same numeric literal on both sides
|
|
114
|
+
// expect(1).toBe(1), expect(42).toEqual(42)
|
|
115
|
+
const numericPattern = new RegExp(`expect\\s*\\(\\s*(\\d+(?:\\.\\d+)?)\\s*\\)\\s*\\.\\s*(?:${matcherPattern})\\s*\\(\\s*\\1\\s*\\)`);
|
|
116
|
+
// Pattern 3: Same string literal on both sides (single or double quotes)
|
|
117
|
+
// expect('hello').toBe('hello'), expect("foo").toEqual("foo")
|
|
118
|
+
const singleQuotePattern = new RegExp(`expect\\s*\\(\\s*'([^']*)'\\s*\\)\\s*\\.\\s*(?:${matcherPattern})\\s*\\(\\s*'\\1'\\s*\\)`);
|
|
119
|
+
const doubleQuotePattern = new RegExp(`expect\\s*\\(\\s*"([^"]*)"\\s*\\)\\s*\\.\\s*(?:${matcherPattern})\\s*\\(\\s*"\\1"\\s*\\)`);
|
|
120
|
+
// Pattern 4: Same identifier on both sides
|
|
121
|
+
// expect(x).toBe(x), expect(result).toEqual(result)
|
|
122
|
+
const identifierPattern = new RegExp(`expect\\s*\\(\\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*\\)\\s*\\.\\s*(?:${matcherPattern})\\s*\\(\\s*\\1\\s*\\)`);
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
const lineNum = i + 1;
|
|
126
|
+
if (literalPattern.test(line)) {
|
|
127
|
+
const literalMatch = line.match(literalPattern);
|
|
128
|
+
issues.push({
|
|
129
|
+
type: 'tautological-assertion',
|
|
130
|
+
severity: 'error',
|
|
131
|
+
line: lineNum,
|
|
132
|
+
description: `Tautological assertion: expect(${literalMatch?.[1]}) always equals itself.`,
|
|
133
|
+
suggestion: 'Replace with a meaningful assertion that tests actual behavior, e.g.: expect(myFunction()).toBe(expectedValue)',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if (numericPattern.test(line)) {
|
|
137
|
+
const numMatch = line.match(numericPattern);
|
|
138
|
+
issues.push({
|
|
139
|
+
type: 'tautological-assertion',
|
|
140
|
+
severity: 'error',
|
|
141
|
+
line: lineNum,
|
|
142
|
+
description: `Tautological assertion: expect(${numMatch?.[1]}) always equals itself.`,
|
|
143
|
+
suggestion: 'Replace with a meaningful assertion that tests actual behavior.',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (singleQuotePattern.test(line)) {
|
|
147
|
+
const strMatch = line.match(singleQuotePattern);
|
|
148
|
+
issues.push({
|
|
149
|
+
type: 'tautological-assertion',
|
|
150
|
+
severity: 'error',
|
|
151
|
+
line: lineNum,
|
|
152
|
+
description: `Tautological assertion: expect('${strMatch?.[1]}') always equals itself.`,
|
|
153
|
+
suggestion: 'Replace with a meaningful assertion that tests actual behavior.',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else if (doubleQuotePattern.test(line)) {
|
|
157
|
+
const strMatch = line.match(doubleQuotePattern);
|
|
158
|
+
issues.push({
|
|
159
|
+
type: 'tautological-assertion',
|
|
160
|
+
severity: 'error',
|
|
161
|
+
line: lineNum,
|
|
162
|
+
description: `Tautological assertion: expect("${strMatch?.[1]}") always equals itself.`,
|
|
163
|
+
suggestion: 'Replace with a meaningful assertion that tests actual behavior.',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else if (identifierPattern.test(line)) {
|
|
167
|
+
const idMatch = line.match(identifierPattern);
|
|
168
|
+
// Avoid false positives with common non-tautological patterns
|
|
169
|
+
// like expect(result).toBe(result) where result is a computed value
|
|
170
|
+
// We flag it anyway since same-variable assertions are always suspicious
|
|
171
|
+
issues.push({
|
|
172
|
+
type: 'tautological-assertion',
|
|
173
|
+
severity: 'error',
|
|
174
|
+
line: lineNum,
|
|
175
|
+
description: `Tautological assertion: expect(${idMatch?.[1]}).toBe(${idMatch?.[1]}) compares a value to itself.`,
|
|
176
|
+
suggestion: 'Compute expected value independently from the actual value.',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return issues;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Detect empty test bodies - test/it blocks with no assertions or meaningful code.
|
|
184
|
+
* Matches: it('...', () => {}), test('...', () => { /* comment * / })
|
|
185
|
+
*/
|
|
186
|
+
detectEmptyTestBodies(testCode) {
|
|
187
|
+
const issues = [];
|
|
188
|
+
const lines = testCode.split('\n');
|
|
189
|
+
// Match it(...) or test(...) blocks with empty or comment-only bodies
|
|
190
|
+
// We look for patterns like:
|
|
191
|
+
// it('desc', () => {})
|
|
192
|
+
// it('desc', () => { })
|
|
193
|
+
// it('desc', () => { /* comment */ })
|
|
194
|
+
// it('desc', function() {})
|
|
195
|
+
// test('desc', () => {})
|
|
196
|
+
//
|
|
197
|
+
// Two-phase approach to avoid ReDoS: first match the block structure,
|
|
198
|
+
// then check if the body contains only whitespace and comments.
|
|
199
|
+
const testBlockRegex = /(?:it|test)\s*\(\s*(?:'[^']*'|"[^"]*"|`[^`]*`)\s*,\s*(?:async\s+)?(?:\(\)\s*=>|function\s*\(\))\s*\{([^}]*)\}\s*\)/;
|
|
200
|
+
const isEmptyOrCommentOnly = (body) => {
|
|
201
|
+
// Strip block comments, then line comments, then check if only whitespace remains
|
|
202
|
+
const stripped = body.replace(/\/\*[^*]*\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
203
|
+
return stripped.trim().length === 0;
|
|
204
|
+
};
|
|
205
|
+
for (let i = 0; i < lines.length; i++) {
|
|
206
|
+
// Only check lines that start a test block
|
|
207
|
+
if (!/(?:it|test)\s*\(/.test(lines[i]))
|
|
208
|
+
continue;
|
|
209
|
+
// Build a multi-line window to catch bodies that span 1-3 lines
|
|
210
|
+
const window = lines.slice(i, i + 4).join(' ');
|
|
211
|
+
const match = testBlockRegex.exec(window);
|
|
212
|
+
if (match && isEmptyOrCommentOnly(match[1])) {
|
|
213
|
+
issues.push({
|
|
214
|
+
type: 'empty-test-body',
|
|
215
|
+
severity: 'error',
|
|
216
|
+
line: i + 1,
|
|
217
|
+
description: 'Empty test body: this test has no assertions or meaningful code.',
|
|
218
|
+
suggestion: 'Add assertions that verify the expected behavior of the code under test.',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return issues;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Detect mirrored assertions - expected values that appear to be copy-pasted
|
|
226
|
+
* from source code literals rather than independently computed.
|
|
227
|
+
*/
|
|
228
|
+
detectMirroredAssertions(testCode, sourceCode) {
|
|
229
|
+
const issues = [];
|
|
230
|
+
// Extract non-trivial literals from source code
|
|
231
|
+
const sourceLiterals = this.extractNonTrivialLiterals(sourceCode);
|
|
232
|
+
if (sourceLiterals.length === 0)
|
|
233
|
+
return issues;
|
|
234
|
+
const lines = testCode.split('\n');
|
|
235
|
+
const matcherPattern = /\.(?:toBe|toEqual|toStrictEqual)\s*\(\s*(.+?)\s*\)/;
|
|
236
|
+
for (let i = 0; i < lines.length; i++) {
|
|
237
|
+
const line = lines[i];
|
|
238
|
+
const assertionMatch = line.match(matcherPattern);
|
|
239
|
+
if (!assertionMatch)
|
|
240
|
+
continue;
|
|
241
|
+
const expectedValue = assertionMatch[1].trim();
|
|
242
|
+
// Check if the expected value matches a source literal
|
|
243
|
+
for (const literal of sourceLiterals) {
|
|
244
|
+
if (this.literalMatches(expectedValue, literal)) {
|
|
245
|
+
issues.push({
|
|
246
|
+
type: 'mirrored-assertion',
|
|
247
|
+
severity: 'warning',
|
|
248
|
+
line: i + 1,
|
|
249
|
+
description: `Assertion expected value "${expectedValue}" mirrors a literal from the source code. This may indicate the test was generated by copying source values rather than computing expected results independently.`,
|
|
250
|
+
suggestion: 'Verify the expected value is derived from requirements, not copied from the implementation.',
|
|
251
|
+
});
|
|
252
|
+
break; // One warning per line is enough
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return issues;
|
|
257
|
+
}
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Utility Methods
|
|
260
|
+
// ============================================================================
|
|
261
|
+
/**
|
|
262
|
+
* Extract non-trivial string and number literals from source code.
|
|
263
|
+
* Trivial values (true, false, null, undefined, 0, 1, '', "") are excluded.
|
|
264
|
+
*/
|
|
265
|
+
extractNonTrivialLiterals(sourceCode) {
|
|
266
|
+
const literals = new Set();
|
|
267
|
+
const trivialValues = new Set([
|
|
268
|
+
'true', 'false', 'null', 'undefined',
|
|
269
|
+
'0', '1', '-1', '""', "''", '``',
|
|
270
|
+
]);
|
|
271
|
+
// Extract string literals (single and double quoted)
|
|
272
|
+
const stringRegex = /(?:=|return|:)\s*(['"])(.+?)\1/g;
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = stringRegex.exec(sourceCode)) !== null) {
|
|
275
|
+
const value = match[2];
|
|
276
|
+
if (value.length >= 2 && !trivialValues.has(value)) {
|
|
277
|
+
literals.add(`'${value}'`);
|
|
278
|
+
literals.add(`"${value}"`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Extract number literals (non-trivial: > 1 or decimals)
|
|
282
|
+
const numberRegex = /(?:=|return|:)\s*(\d+(?:\.\d+)?)\b/g;
|
|
283
|
+
while ((match = numberRegex.exec(sourceCode)) !== null) {
|
|
284
|
+
const value = match[1];
|
|
285
|
+
if (!trivialValues.has(value)) {
|
|
286
|
+
literals.add(value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return Array.from(literals);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Check if an assertion expected value matches a source literal.
|
|
293
|
+
*/
|
|
294
|
+
literalMatches(expectedValue, sourceLiteral) {
|
|
295
|
+
// Direct match
|
|
296
|
+
if (expectedValue === sourceLiteral)
|
|
297
|
+
return true;
|
|
298
|
+
// Strip quotes for comparison
|
|
299
|
+
const stripped = expectedValue.replace(/^['"`]|['"`]$/g, '');
|
|
300
|
+
const sourceStripped = sourceLiteral.replace(/^['"`]|['"`]$/g, '');
|
|
301
|
+
return stripped === sourceStripped && stripped.length >= 2;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Calculate quality score from issues.
|
|
305
|
+
* Start at 100, subtract per issue (error: -20, warning: -5), clamp to 0.
|
|
306
|
+
*/
|
|
307
|
+
calculateScore(issues) {
|
|
308
|
+
let score = 100;
|
|
309
|
+
for (const issue of issues) {
|
|
310
|
+
if (issue.severity === 'error') {
|
|
311
|
+
score -= 20;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
score -= 5;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return Math.max(0, score);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=test-quality-gate.js.map
|