@sun-asterisk/sungen 3.0.0-beta.71 → 3.0.0-beta.72
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/dist/cli/commands/capability.d.ts +3 -0
- package/dist/cli/commands/capability.d.ts.map +1 -0
- package/dist/cli/commands/capability.js +196 -0
- package/dist/cli/commands/capability.js.map +1 -0
- package/dist/cli/commands/flow-check.d.ts +3 -0
- package/dist/cli/commands/flow-check.d.ts.map +1 -0
- package/dist/cli/commands/flow-check.js +136 -0
- package/dist/cli/commands/flow-check.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +28 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/script-check.js +1 -1
- package/dist/cli/commands/script-check.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-registry.d.ts +13 -0
- package/dist/generators/test-generator/adapters/adapter-registry.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-registry.js +73 -1
- package/dist/generators/test-generator/adapters/adapter-registry.js.map +1 -1
- package/dist/generators/test-generator/adapters/index.d.ts +1 -1
- package/dist/generators/test-generator/adapters/index.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/index.js +5 -1
- package/dist/generators/test-generator/adapters/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +6 -2
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +3 -1
- package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +4 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +12 -6
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +49 -0
- package/dist/harness/capability-plan.d.ts.map +1 -0
- package/dist/harness/capability-plan.js +215 -0
- package/dist/harness/capability-plan.js.map +1 -0
- package/dist/harness/capability.d.ts +23 -0
- package/dist/harness/capability.d.ts.map +1 -0
- package/dist/harness/capability.js +98 -0
- package/dist/harness/capability.js.map +1 -0
- package/dist/harness/catalog/drivers.yaml +57 -0
- package/dist/harness/flow-check.d.ts +23 -0
- package/dist/harness/flow-check.d.ts.map +1 -0
- package/dist/harness/flow-check.js +132 -0
- package/dist/harness/flow-check.js.map +1 -0
- package/dist/harness/flow-plan.d.ts +23 -0
- package/dist/harness/flow-plan.d.ts.map +1 -0
- package/dist/harness/flow-plan.js +166 -0
- package/dist/harness/flow-plan.js.map +1 -0
- package/dist/harness/script-check.d.ts +23 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +88 -6
- package/dist/harness/script-check.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +5 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +20 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/specs-test-data.ts +11 -6
- package/package.json +3 -2
- package/src/cli/commands/capability.ts +160 -0
- package/src/cli/commands/flow-check.ts +97 -0
- package/src/cli/commands/generate.ts +28 -2
- package/src/cli/commands/script-check.ts +1 -1
- package/src/cli/index.ts +4 -0
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -0
- package/src/generators/test-generator/adapters/adapter-registry.ts +37 -0
- package/src/generators/test-generator/adapters/index.ts +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +6 -0
- package/src/generators/test-generator/code-generator.ts +6 -2
- package/src/generators/test-generator/patterns/form-patterns.ts +3 -1
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +8 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +13 -6
- package/src/harness/capability-plan.ts +180 -0
- package/src/harness/capability.ts +75 -0
- package/src/harness/catalog/drivers.yaml +57 -0
- package/src/harness/flow-check.ts +99 -0
- package/src/harness/flow-plan.ts +135 -0
- package/src/harness/script-check.ts +79 -6
- package/src/orchestrator/project-initializer.ts +23 -0
- package/src/orchestrator/templates/specs-test-data.ts +11 -6
|
@@ -28,10 +28,62 @@ export interface ScriptCheckResult {
|
|
|
28
28
|
extraInSpec: string[]; // test() titles with no scenario
|
|
29
29
|
drift: 'in-sync' | 'drift' | 'no-spec';
|
|
30
30
|
driftHunks: string[]; // sample differing lines (committed vs regenerated)
|
|
31
|
+
// C. Anti-bypass / faithfulness — a present test that proves nothing.
|
|
32
|
+
assertionlessTests: string[]; // non-manual test() with 0 expect()
|
|
33
|
+
hollowSteps: { test: string; step: string }[]; // a step-comment that emitted no code
|
|
34
|
+
bypass: boolean;
|
|
31
35
|
status: 'OK' | 'FAIL';
|
|
32
36
|
findings: string[];
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/** Split a spec into test() blocks (title + body) via brace counting. */
|
|
40
|
+
export function extractTestBlocks(specSrc: string): { title: string; body: string[] }[] {
|
|
41
|
+
const lines = specSrc.split('\n');
|
|
42
|
+
const blocks: { title: string; body: string[] }[] = [];
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const m = lines[i].match(/\btest(?:\.(?:only|skip|fixme))?\(\s*(['"`])([^'"`]+)\1/);
|
|
45
|
+
if (!m) continue;
|
|
46
|
+
let depth = 0, started = false;
|
|
47
|
+
const body: string[] = [];
|
|
48
|
+
for (let j = i; j < lines.length; j++) {
|
|
49
|
+
for (const ch of lines[j]) { if (ch === '{') { depth++; started = true; } else if (ch === '}') depth--; }
|
|
50
|
+
body.push(lines[j]);
|
|
51
|
+
if (started && depth <= 0) break;
|
|
52
|
+
}
|
|
53
|
+
blocks.push({ title: m[2].trim(), body });
|
|
54
|
+
}
|
|
55
|
+
return blocks;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const CODE_LINE = /\b(await|expect|testData|page\.)\b|=\s*[^=]/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Faithfulness / anti-bypass: a non-@manual test that performs actions but proves
|
|
62
|
+
* nothing (0 assertions), or a step-comment that emitted no executable code (hollow
|
|
63
|
+
* — the testcase step is not run). Structural 1:1 can't see these.
|
|
64
|
+
*/
|
|
65
|
+
export function analyzeFaithfulness(specSrc: string, automatedTitles: Set<string>) {
|
|
66
|
+
const assertionlessTests: string[] = [];
|
|
67
|
+
const hollowSteps: { test: string; step: string }[] = [];
|
|
68
|
+
for (const blk of extractTestBlocks(specSrc)) {
|
|
69
|
+
if (!automatedTitles.has(blk.title)) continue; // only non-@manual scenarios
|
|
70
|
+
const body = blk.body;
|
|
71
|
+
if (!body.some((l) => /expect\(/.test(l))) assertionlessTests.push(blk.title);
|
|
72
|
+
// hollow step: a `// step` whose region (until the NEXT step-comment / block end)
|
|
73
|
+
// contains no executable code. The region — not just the next line — is checked,
|
|
74
|
+
// so block-style steps (`// Assert all … { … expect … }`) are correctly counted.
|
|
75
|
+
const stepIdx: number[] = [];
|
|
76
|
+
for (let k = 1; k < body.length; k++) if (body[k].trim().startsWith('//')) stepIdx.push(k);
|
|
77
|
+
for (let s = 0; s < stepIdx.length; s++) {
|
|
78
|
+
const start = stepIdx[s] + 1;
|
|
79
|
+
const end = s + 1 < stepIdx.length ? stepIdx[s + 1] : body.length;
|
|
80
|
+
const hasCode = body.slice(start, end).some((l) => CODE_LINE.test(l));
|
|
81
|
+
if (!hasCode) hollowSteps.push({ test: blk.title, step: body[stepIdx[s]].trim().slice(0, 70) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { assertionlessTests, hollowSteps };
|
|
85
|
+
}
|
|
86
|
+
|
|
35
87
|
function extractTestTitles(specSrc: string): string[] {
|
|
36
88
|
// Count real test cases only: test(...), test.only/.skip/.fixme(...).
|
|
37
89
|
// Exclude test.describe / test.beforeAll / hooks (not test cases).
|
|
@@ -51,8 +103,12 @@ function normalize(src: string): string {
|
|
|
51
103
|
.trim();
|
|
52
104
|
}
|
|
53
105
|
|
|
54
|
-
function findSpec(dir: string,
|
|
55
|
-
//
|
|
106
|
+
function findSpec(dir: string, name: string, flowMode: boolean): string | null {
|
|
107
|
+
// Screens compile to <dir>/<name>/<feature>.spec.ts
|
|
108
|
+
// Flows compile to <dir>/flows/<name>/<feature>.spec.ts
|
|
109
|
+
// Scope the search to THIS target's own subdir — otherwise the first spec of
|
|
110
|
+
// ANY other screen/flow is returned, which (for an uncompiled flow) falsely
|
|
111
|
+
// reports the wrong screen's tests as drift.
|
|
56
112
|
const hits: string[] = [];
|
|
57
113
|
const walk = (d: string) => {
|
|
58
114
|
if (!fs.existsSync(d)) return;
|
|
@@ -62,7 +118,9 @@ function findSpec(dir: string, screen: string): string | null {
|
|
|
62
118
|
else if (e.name.endsWith('.spec.ts')) hits.push(p);
|
|
63
119
|
}
|
|
64
120
|
};
|
|
65
|
-
|
|
121
|
+
const scoped = flowMode ? path.join(dir, 'flows', name) : path.join(dir, name);
|
|
122
|
+
if (!fs.existsSync(scoped)) return null; // no spec for this target (e.g. not compiled yet)
|
|
123
|
+
walk(scoped);
|
|
66
124
|
return hits[0] ?? null;
|
|
67
125
|
}
|
|
68
126
|
|
|
@@ -72,7 +130,7 @@ export async function runScriptCheck(screenDir: string, screenName: string, flow
|
|
|
72
130
|
const automated = scenarios.filter((s) => !s.manual);
|
|
73
131
|
const manual = scenarios.filter((s) => s.manual);
|
|
74
132
|
|
|
75
|
-
const committedSpec = findSpec(path.join(process.cwd(), 'specs', 'generated'), screenName);
|
|
133
|
+
const committedSpec = findSpec(path.join(process.cwd(), 'specs', 'generated'), screenName, flowMode);
|
|
76
134
|
|
|
77
135
|
const findings: string[] = [];
|
|
78
136
|
let specTitles: string[] = [];
|
|
@@ -106,7 +164,7 @@ export async function runScriptCheck(screenDir: string, screenName: string, flow
|
|
|
106
164
|
const qaSourceDir = path.join(process.cwd(), 'qa', flowMode ? 'flows' : 'screens');
|
|
107
165
|
const gen = new CodeGenerator({ framework: 'playwright', screenName, runtimeData: true, flowMode });
|
|
108
166
|
await gen.generateAllTests(qaSourceDir, tmp, [featurePath]);
|
|
109
|
-
const fresh = findSpec(tmp, screenName);
|
|
167
|
+
const fresh = findSpec(tmp, screenName, flowMode);
|
|
110
168
|
if (fresh) {
|
|
111
169
|
const a = normalize(specSrc);
|
|
112
170
|
const b = normalize(fs.readFileSync(fresh, 'utf-8'));
|
|
@@ -130,7 +188,19 @@ export async function runScriptCheck(screenDir: string, screenName: string, flow
|
|
|
130
188
|
}
|
|
131
189
|
}
|
|
132
190
|
|
|
133
|
-
|
|
191
|
+
// C. Anti-bypass / faithfulness
|
|
192
|
+
const { assertionlessTests, hollowSteps } = committedSpec
|
|
193
|
+
? analyzeFaithfulness(specSrc, scenTitleSet)
|
|
194
|
+
: { assertionlessTests: [], hollowSteps: [] };
|
|
195
|
+
for (const t of assertionlessTests) {
|
|
196
|
+
findings.push(`BYPASS: test "${t}" has 0 assertions (action-only — proves nothing). The testcase is not really automated.`);
|
|
197
|
+
}
|
|
198
|
+
for (const h of hollowSteps) {
|
|
199
|
+
findings.push(`BYPASS: in "${h.test}", step "${h.step}" emitted no code (hollow — the testcase step is not executed).`);
|
|
200
|
+
}
|
|
201
|
+
const bypass = assertionlessTests.length > 0 || hollowSteps.length > 0;
|
|
202
|
+
|
|
203
|
+
const ok = !!committedSpec && countMatch && missingInSpec.length === 0 && extraInSpec.length === 0 && drift === 'in-sync' && !bypass;
|
|
134
204
|
|
|
135
205
|
return {
|
|
136
206
|
screen: screenName,
|
|
@@ -143,6 +213,9 @@ export async function runScriptCheck(screenDir: string, screenName: string, flow
|
|
|
143
213
|
extraInSpec,
|
|
144
214
|
drift,
|
|
145
215
|
driftHunks,
|
|
216
|
+
assertionlessTests,
|
|
217
|
+
hollowSteps,
|
|
218
|
+
bypass,
|
|
146
219
|
status: ok ? 'OK' : 'FAIL',
|
|
147
220
|
findings,
|
|
148
221
|
};
|
|
@@ -42,6 +42,9 @@ export class ProjectInitializer {
|
|
|
42
42
|
// Create qa/context.md for QA lead to fill project-wide context
|
|
43
43
|
this.createContext();
|
|
44
44
|
|
|
45
|
+
// Create qa/capabilities.yaml — the platform driver profile (default: web/Playwright)
|
|
46
|
+
this.createCapabilities();
|
|
47
|
+
|
|
45
48
|
// Ensure package.json and install Playwright
|
|
46
49
|
await this.setupDependencies();
|
|
47
50
|
|
|
@@ -386,6 +389,26 @@ export class ProjectInitializer {
|
|
|
386
389
|
this.createdItems.push('qa/context.md');
|
|
387
390
|
}
|
|
388
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Create qa/capabilities.yaml — the platform driver profile (Phase 2a).
|
|
394
|
+
* Defaults to `web` (Playwright). Mobile/API projects switch via `sungen capability add`.
|
|
395
|
+
*/
|
|
396
|
+
private createCapabilities(): void {
|
|
397
|
+
const p = path.join(this.cwd, 'qa', 'capabilities.yaml');
|
|
398
|
+
if (fs.existsSync(p)) {
|
|
399
|
+
this.skippedItems.push('qa/capabilities.yaml');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const content =
|
|
403
|
+
'# Sungen capability profile — which runtime/drivers this project uses.\n' +
|
|
404
|
+
'# platform: the runtime adapter — web → Playwright, mobile → Appium.\n' +
|
|
405
|
+
'# enabled: drivers turned on (add more via `sungen capability add <driver>`).\n\n' +
|
|
406
|
+
'platform: web\n' +
|
|
407
|
+
'enabled:\n - web\n';
|
|
408
|
+
fs.writeFileSync(p, content, 'utf-8');
|
|
409
|
+
this.createdItems.push('qa/capabilities.yaml');
|
|
410
|
+
}
|
|
411
|
+
|
|
389
412
|
/**
|
|
390
413
|
* Create specs/base.ts for shared browser context
|
|
391
414
|
*/
|
|
@@ -41,13 +41,18 @@ export class TestDataLoader {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
get(key: string): string {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Captured/runtime vars (set() below) are stored under their literal — possibly
|
|
45
|
+
// dotted — key (e.g. "cart.product_name"), so check the flat key first.
|
|
46
|
+
let current: any = this.data[key];
|
|
47
|
+
if (current === undefined || current === null) {
|
|
48
|
+
// Fall back to nested navigation for YAML-structured keys (e.g. "cart.qty_two").
|
|
49
|
+
current = this.data;
|
|
50
|
+
for (const part of key.split('.')) {
|
|
51
|
+
if (current == null || typeof current !== 'object') {
|
|
52
|
+
throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
|
|
53
|
+
}
|
|
54
|
+
current = current[part];
|
|
49
55
|
}
|
|
50
|
-
current = current[part];
|
|
51
56
|
}
|
|
52
57
|
if (current === undefined || current === null) {
|
|
53
58
|
throw new Error(`Test data key not found: ${key}`);
|