@sun-asterisk/sungen 3.0.0-beta.71 → 3.0.0-beta.73

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.
Files changed (86) hide show
  1. package/dist/cli/commands/capability.d.ts +3 -0
  2. package/dist/cli/commands/capability.d.ts.map +1 -0
  3. package/dist/cli/commands/capability.js +196 -0
  4. package/dist/cli/commands/capability.js.map +1 -0
  5. package/dist/cli/commands/flow-check.d.ts +3 -0
  6. package/dist/cli/commands/flow-check.d.ts.map +1 -0
  7. package/dist/cli/commands/flow-check.js +136 -0
  8. package/dist/cli/commands/flow-check.js.map +1 -0
  9. package/dist/cli/commands/generate.d.ts.map +1 -1
  10. package/dist/cli/commands/generate.js +28 -2
  11. package/dist/cli/commands/generate.js.map +1 -1
  12. package/dist/cli/commands/script-check.js +1 -1
  13. package/dist/cli/commands/script-check.js.map +1 -1
  14. package/dist/cli/index.js +4 -0
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  17. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  18. package/dist/generators/test-generator/adapters/adapter-registry.d.ts +13 -0
  19. package/dist/generators/test-generator/adapters/adapter-registry.d.ts.map +1 -1
  20. package/dist/generators/test-generator/adapters/adapter-registry.js +73 -1
  21. package/dist/generators/test-generator/adapters/adapter-registry.js.map +1 -1
  22. package/dist/generators/test-generator/adapters/index.d.ts +1 -1
  23. package/dist/generators/test-generator/adapters/index.d.ts.map +1 -1
  24. package/dist/generators/test-generator/adapters/index.js +5 -1
  25. package/dist/generators/test-generator/adapters/index.js.map +1 -1
  26. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +6 -0
  27. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  28. package/dist/generators/test-generator/code-generator.js +6 -2
  29. package/dist/generators/test-generator/code-generator.js.map +1 -1
  30. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
  31. package/dist/generators/test-generator/patterns/form-patterns.js +3 -1
  32. package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
  33. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  34. package/dist/generators/test-generator/utils/runtime-data-transformer.js +4 -0
  35. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  36. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  37. package/dist/generators/test-generator/utils/selector-resolver.js +12 -6
  38. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  39. package/dist/harness/capability-plan.d.ts +49 -0
  40. package/dist/harness/capability-plan.d.ts.map +1 -0
  41. package/dist/harness/capability-plan.js +215 -0
  42. package/dist/harness/capability-plan.js.map +1 -0
  43. package/dist/harness/capability.d.ts +23 -0
  44. package/dist/harness/capability.d.ts.map +1 -0
  45. package/dist/harness/capability.js +98 -0
  46. package/dist/harness/capability.js.map +1 -0
  47. package/dist/harness/catalog/drivers.yaml +57 -0
  48. package/dist/harness/flow-check.d.ts +23 -0
  49. package/dist/harness/flow-check.d.ts.map +1 -0
  50. package/dist/harness/flow-check.js +132 -0
  51. package/dist/harness/flow-check.js.map +1 -0
  52. package/dist/harness/flow-plan.d.ts +23 -0
  53. package/dist/harness/flow-plan.d.ts.map +1 -0
  54. package/dist/harness/flow-plan.js +166 -0
  55. package/dist/harness/flow-plan.js.map +1 -0
  56. package/dist/harness/script-check.d.ts +23 -0
  57. package/dist/harness/script-check.d.ts.map +1 -1
  58. package/dist/harness/script-check.js +88 -6
  59. package/dist/harness/script-check.js.map +1 -1
  60. package/dist/orchestrator/project-initializer.d.ts +5 -0
  61. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  62. package/dist/orchestrator/project-initializer.js +20 -0
  63. package/dist/orchestrator/project-initializer.js.map +1 -1
  64. package/dist/orchestrator/templates/specs-test-data.ts +11 -6
  65. package/package.json +3 -2
  66. package/src/cli/commands/capability.ts +160 -0
  67. package/src/cli/commands/flow-check.ts +97 -0
  68. package/src/cli/commands/generate.ts +28 -2
  69. package/src/cli/commands/script-check.ts +1 -1
  70. package/src/cli/index.ts +4 -0
  71. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -0
  72. package/src/generators/test-generator/adapters/adapter-registry.ts +37 -0
  73. package/src/generators/test-generator/adapters/index.ts +4 -1
  74. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +6 -0
  75. package/src/generators/test-generator/code-generator.ts +6 -2
  76. package/src/generators/test-generator/patterns/form-patterns.ts +3 -1
  77. package/src/generators/test-generator/utils/runtime-data-transformer.ts +8 -0
  78. package/src/generators/test-generator/utils/selector-resolver.ts +13 -6
  79. package/src/harness/capability-plan.ts +180 -0
  80. package/src/harness/capability.ts +75 -0
  81. package/src/harness/catalog/drivers.yaml +57 -0
  82. package/src/harness/flow-check.ts +99 -0
  83. package/src/harness/flow-plan.ts +135 -0
  84. package/src/harness/script-check.ts +79 -6
  85. package/src/orchestrator/project-initializer.ts +23 -0
  86. 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, screen: string): string | null {
55
- // generated spec: <dir>/<screen>/<feature>.spec.ts (or nested)
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
- walk(dir);
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
- const ok = !!committedSpec && countMatch && missingInSpec.length === 0 && extraInSpec.length === 0 && drift === 'in-sync';
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
- const parts = key.split('.');
45
- let current: any = this.data;
46
- for (const part of parts) {
47
- if (current == null || typeof current !== 'object') {
48
- throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
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}`);