@sun-asterisk/sungen 3.1.2 → 3.2.0-beta.142
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/README.md +4 -428
- package/dist/capabilities/builtins.d.ts +31 -0
- package/dist/capabilities/builtins.d.ts.map +1 -0
- package/dist/capabilities/builtins.js +84 -0
- package/dist/capabilities/builtins.js.map +1 -0
- package/dist/capabilities/context-router.d.ts +34 -0
- package/dist/capabilities/context-router.d.ts.map +1 -0
- package/dist/capabilities/context-router.js +49 -0
- package/dist/capabilities/context-router.js.map +1 -0
- package/dist/capabilities/context.d.ts +68 -0
- package/dist/capabilities/context.d.ts.map +1 -0
- package/dist/capabilities/context.js +17 -0
- package/dist/capabilities/context.js.map +1 -0
- package/dist/capabilities/discover.d.ts +2 -0
- package/dist/capabilities/discover.d.ts.map +1 -0
- package/dist/capabilities/discover.js +109 -0
- package/dist/capabilities/discover.js.map +1 -0
- package/dist/capabilities/registry.d.ts +92 -0
- package/dist/capabilities/registry.d.ts.map +1 -0
- package/dist/capabilities/registry.js +43 -0
- package/dist/capabilities/registry.js.map +1 -0
- package/dist/capabilities/sensor.d.ts +52 -0
- package/dist/capabilities/sensor.d.ts.map +1 -0
- package/dist/capabilities/sensor.js +3 -0
- package/dist/capabilities/sensor.js.map +1 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +17 -11
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +57 -5
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/context.d.ts +9 -0
- package/dist/cli/commands/context.d.ts.map +1 -0
- package/dist/cli/commands/context.js +91 -0
- package/dist/cli/commands/context.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +42 -30
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +35 -8
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/ledger.d.ts.map +1 -1
- package/dist/cli/commands/ledger.js +15 -5
- package/dist/cli/commands/ledger.js.map +1 -1
- package/dist/cli/commands/manifest.d.ts.map +1 -1
- package/dist/cli/commands/manifest.js +10 -9
- package/dist/cli/commands/manifest.js.map +1 -1
- package/dist/cli/commands/repair.d.ts +8 -0
- package/dist/cli/commands/repair.d.ts.map +1 -0
- package/dist/cli/commands/repair.js +97 -0
- package/dist/cli/commands/repair.js.map +1 -0
- package/dist/cli/commands/script-check.d.ts.map +1 -1
- package/dist/cli/commands/script-check.js +13 -9
- package/dist/cli/commands/script-check.js.map +1 -1
- package/dist/cli/commands/trace.d.ts.map +1 -1
- package/dist/cli/commands/trace.js +7 -4
- package/dist/cli/commands/trace.js.map +1 -1
- package/dist/cli/index.js +14 -1
- 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/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/code-generator.d.ts +18 -9
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +162 -115
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +0 -10
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +10 -47
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/harness/annotation-overrides.d.ts +11 -0
- package/dist/harness/annotation-overrides.d.ts.map +1 -0
- package/dist/harness/annotation-overrides.js +38 -0
- package/dist/harness/annotation-overrides.js.map +1 -0
- package/dist/harness/audit.d.ts +9 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +140 -10
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +14 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +63 -1
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +35 -12
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +23 -0
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts +9 -0
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +13 -6
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/intent.d.ts +6 -0
- package/dist/harness/intent.d.ts.map +1 -1
- package/dist/harness/intent.js +20 -4
- package/dist/harness/intent.js.map +1 -1
- package/dist/harness/ledger.d.ts.map +1 -1
- package/dist/harness/ledger.js +3 -2
- package/dist/harness/ledger.js.map +1 -1
- package/dist/harness/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +3 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts +2 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -4
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.js +1 -1
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/query-catalog.d.ts.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/harness/repair.d.ts +20 -0
- package/dist/harness/repair.d.ts.map +1 -0
- package/dist/harness/repair.js +111 -0
- package/dist/harness/repair.js.map +1 -0
- package/dist/harness/script-check.d.ts +3 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +22 -8
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts +40 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +54 -2
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/trace.d.ts.map +1 -1
- package/dist/harness/trace.js +4 -3
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/unit-paths.d.ts +3 -0
- package/dist/harness/unit-paths.d.ts.map +1 -0
- package/dist/harness/unit-paths.js +52 -0
- package/dist/harness/unit-paths.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +2 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/context-discovery.d.ts +12 -0
- package/dist/orchestrator/context-discovery.d.ts.map +1 -0
- package/dist/orchestrator/context-discovery.js +46 -0
- package/dist/orchestrator/context-discovery.js.map +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +7 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +10 -5
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +18 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-api-design.md +62 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +2 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +10 -5
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +11 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +62 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +2 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -2
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
- package/dist/orchestrator/templates/specs-api.d.ts +55 -0
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-api.js +171 -0
- package/dist/orchestrator/templates/specs-api.js.map +1 -0
- package/dist/orchestrator/templates/specs-api.ts +154 -0
- package/dist/orchestrator/templates/specs-db.d.ts +3 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +78 -1
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +78 -1
- package/dist/orchestrator/templates/specs-test-data.ts +2 -1
- package/package.json +7 -30
- package/src/capabilities/builtins.ts +85 -0
- package/src/capabilities/context-router.ts +66 -0
- package/src/capabilities/context.ts +65 -0
- package/src/capabilities/discover.ts +62 -0
- package/src/capabilities/registry.ts +113 -0
- package/src/capabilities/sensor.ts +47 -0
- package/src/cli/commands/audit.ts +15 -9
- package/src/cli/commands/capability.ts +53 -5
- package/src/cli/commands/context.ts +52 -0
- package/src/cli/commands/delivery.ts +40 -31
- package/src/cli/commands/generate.ts +37 -8
- package/src/cli/commands/ledger.ts +13 -5
- package/src/cli/commands/manifest.ts +9 -7
- package/src/cli/commands/repair.ts +57 -0
- package/src/cli/commands/script-check.ts +12 -8
- package/src/cli/commands/trace.ts +7 -4
- package/src/cli/index.ts +14 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/code-generator.ts +163 -111
- package/src/generators/test-generator/patterns/index.ts +9 -35
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/harness/annotation-overrides.ts +27 -0
- package/src/harness/audit.ts +141 -12
- package/src/harness/capability-plan.ts +51 -1
- package/src/harness/catalog/drivers.yaml +35 -12
- package/src/harness/data-driven-lint.ts +20 -0
- package/src/harness/flow-check.ts +15 -6
- package/src/harness/intent.ts +25 -4
- package/src/harness/ledger.ts +3 -2
- package/src/harness/manifest.ts +3 -2
- package/src/harness/parse.ts +11 -2
- package/src/harness/quality-gates.ts +1 -1
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/repair.ts +75 -0
- package/src/harness/script-check.ts +25 -8
- package/src/harness/sensors.ts +71 -2
- package/src/harness/trace.ts +4 -3
- package/src/harness/unit-paths.ts +14 -0
- package/src/index.ts +32 -0
- package/src/orchestrator/ai-rules-updater.ts +2 -0
- package/src/orchestrator/context-discovery.ts +50 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +7 -1
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +10 -5
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +18 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-api-design.md +62 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +2 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +10 -5
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +11 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +62 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +2 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -2
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
- package/src/orchestrator/templates/specs-api.ts +154 -0
- package/src/orchestrator/templates/specs-db.ts +78 -1
- package/src/orchestrator/templates/specs-test-data.ts +2 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
- package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/database-patterns.js +0 -95
- package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
- package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
- package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
- package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
- package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
- package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
- package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
- package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
- package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
- package/docs/orchestration-spec.md +0 -267
- package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
- package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
- package/src/generators/test-generator/patterns/database-patterns.ts +0 -96
- package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
- package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
- package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
- package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
- package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
- package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
- package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
- package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
|
@@ -6,6 +6,9 @@ user-invocable: false
|
|
|
6
6
|
|
|
7
7
|
## ⚠️ Gotchas — read before generating
|
|
8
8
|
|
|
9
|
+
- **Write incrementally — never emit the whole suite in one response.** Build the `.feature` in batches via successive `Write`/`Edit` (≈10–15 scenarios per call). For **Full coverage**, write tier-by-tier: `Write` Tier 1 → `Edit` append Tier 2 → `Edit` append Tier 3.
|
|
10
|
+
→ One huge `Write` can exceed the model's output-token cap → `API Error: Claude's response exceeded the N output token maximum`. Single-pass full coverage only fits when `CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`; otherwise batch. Batching also lets the audit/reviewer run per batch — higher quality.
|
|
11
|
+
|
|
9
12
|
- `spec_figma.md` exists → read file only, **NEVER** call `mcp__figma__*`
|
|
10
13
|
→ PAT auth flow already done by `sungen-capture` (mode figma-pat); re-calling fails or duplicates work.
|
|
11
14
|
|
|
@@ -265,12 +268,26 @@ Security: [S1 – admin only]
|
|
|
265
268
|
Then User see [Detail Product Name] header with {{selected_product_name}}
|
|
266
269
|
And User see [Detail Product Price] text contains {{selected_product_price}}
|
|
267
270
|
```
|
|
268
|
-
Cross-screen target →
|
|
271
|
+
Cross-screen target → **automate it in the flow** (`/sungen:add-flow`), NOT as a `@manual` screen copy. A single home→target journey runs as one Playwright test, so it is automatable — "needs another screen" is not a reason for `@manual`. The screen keeps its screen-contract scenarios; the flow owns the cross-screen depth.
|
|
269
272
|
- Filter result (category AND brand, separately): `Then User see all [Result Product Name] contain {{selected_category}}` — proves EVERY item belongs, not one.
|
|
270
273
|
|
|
271
274
|
**Depth is a GATE dimension (harness-roadmap P1) — self-raise, never silently go shallow:**
|
|
272
275
|
- For every data-correctness theme the catalog marks `depth.requires: data-assertion`, emit its `depth.template` shape by **default** — don't wait for the repair loop. `sungen audit` measures `businessDepth` (ratio of these scenarios that assert data) against an intent threshold (functional ≥ 0.70); below it the **gate FAILs**.
|
|
273
|
-
- `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape
|
|
276
|
+
- `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape as an **automated flow scenario** (in the flow — do NOT leave a full-step `@manual` duplicate on the screen). `@manual` is **only** for genuine judgment (M6 visual/UX · M8 not-worth · M9 human) or a missing capability (M1–M5/M7), and it **must** carry a reason code (`@manual:Mx`, or a reason comment the planner can infer). A `@manual` scenario that still has full automatable steps (a data assertion, no visual/mock/a11y judgment) is now flagged by `sungen audit` as `MANUAL-AUTOMATABLE`, and business-critical scenarios you defer to `@manual` are reported as `DEPTH-DEFERRED` (they do NOT silently inflate `businessDepth`). Deferring automatable work to `@manual` lowers quality — automate it in the flow instead.
|
|
277
|
+
- **Pick the right `@manual:Mx` code — it decides which driver can later automate the case** (`sungen audit` flags a code↔reason mismatch). Tag the code that matches the **oracle the reason describes**:
|
|
278
|
+
|
|
279
|
+
| The reason needs… | Code | Unblocked by |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| a data state you can't make from the UI (empty list, seeded record, missing-image product) | `M1` | data-factory / db |
|
|
282
|
+
| an **API/DB/persistence** assertion (stored value, parameterized-query / SQLi-safe, server-side effect) | `M2` | **api / db** |
|
|
283
|
+
| network / fault injection (offline, slow, request failure) | `M3` | mock |
|
|
284
|
+
| a stable selector / test-id that doesn't exist | `M4` | — (locator contract) |
|
|
285
|
+
| an external dependency (email, payment gateway, download) | `M5` | mail-file / contract |
|
|
286
|
+
| visual / UX / responsive / a11y judgment | `M6` | — (keep manual) |
|
|
287
|
+
| not worth automating · true human judgment | `M8` / `M9` | — (keep manual) |
|
|
288
|
+
|
|
289
|
+
e.g. "submit a payload then check the subscribers **table**" is an API+DB oracle → `@manual:M2` (NOT `M1`); "seed a DB with zero products" is a data state → `M1`; "throttle the network" → `M3`.
|
|
290
|
+
- **Prefer automation-ready `@requires:<cap>` over prose `@manual`.** When you *can* write the steps for a capability-manual case (an API/DB oracle, a seeded state), write it **automation-ready** — the real `@api`/`@query`/… steps tagged `@requires:<cap>` (e.g. `@requires:db @query:subscriber_row`) — instead of a prose `@manual:M2`. It compiles to a skipped-with-reason stub until `sungen capability add <cap>`, then runs as a real test with **no rewrite**. Reserve prose `@manual:Mx` for cases whose steps genuinely can't be expressed (M6/M8/M9 judgment, or a capability with no driver). `sungen audit` reports these as `AUTOMATION-READY-PENDING` (not a gap, not manual).
|
|
274
291
|
- **If the spec lacks the concrete value** a deep assertion needs (exact message, price, count): still write the deep shape with a `{{var}}` placeholder and leave a `# SPEC-GAP: <field> value not in spec` comment — do **not** downgrade to `see [X] section`. A visible gap is better than a silent shallow pass.
|
|
275
292
|
- **Blind-Spot Memory:** before finishing, run `sungen blindspot list --prompt` (Bash) and make sure the suite satisfies each recorded pattern (e.g. "for any Add/Create action: check success + resulting data state + duplicate/double-submit"). These are gaps QA hit before — don't repeat them.
|
|
276
293
|
|
|
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
|
|
|
69
69
|
- VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
|
|
70
70
|
- VP-SEC = checks access control and malicious input
|
|
71
71
|
|
|
72
|
+
### Domain category codes — required for the coverage-balance gate
|
|
73
|
+
|
|
74
|
+
The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
|
|
75
|
+
|
|
76
|
+
| Bucket | Codes | Use for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
|
|
79
|
+
| presentation | `UI` | layout / visual state |
|
|
80
|
+
| validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
|
|
81
|
+
| behavior | `LOGIC` | action-driven state changes |
|
|
82
|
+
| navigation | `NAV` | landing on / moving between pages |
|
|
83
|
+
|
|
84
|
+
**On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
|
|
85
|
+
|
|
72
86
|
---
|
|
73
87
|
|
|
74
88
|
## Shared Checks
|
|
@@ -18,7 +18,11 @@ You are a **Senior QA Engineer**. You structure test cases by viewpoint categori
|
|
|
18
18
|
|
|
19
19
|
- **name** — ${input:name:screen or flow name (e.g., login, award-submission)}
|
|
20
20
|
|
|
21
|
-
**Auto-detect context**: check if `qa/flows/<name>/` exists → flow mode (base path: `qa/flows/<name>/`). Else
|
|
21
|
+
**Auto-detect context**: check if `qa/api/<name>/` or `qa/api/flows/<name>/` exists → **API unit mode** (below). Else if `qa/flows/<name>/` → flow mode (base path: `qa/flows/<name>/`). Else `qa/screens/<name>/` → screen mode (base path: `qa/screens/<name>/`).
|
|
22
|
+
|
|
23
|
+
## API unit mode (driver-api)
|
|
24
|
+
|
|
25
|
+
If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the design loop differs — **no visual capture, no selectors**; the contract is the named-endpoint catalog. **Follow the `sungen-api-design` skill end-to-end** instead of the screen/flow steps: `sungen context --area <name>` (discover) → API viewpoint overview → generate `@api`/`@cases`/flow/`@concurrent`/`@query` scenarios → **`sungen audit --area <name>` gate + reviewer + repair loop to businessDepth ≥ 0.7** → record + trace. Then recommend `/sungen-run-test <name>`. The capture / viewpoint-group / selector steps do **not** apply.
|
|
22
26
|
|
|
23
27
|
## Steps
|
|
24
28
|
|
|
@@ -26,9 +30,10 @@ You are a **Senior QA Engineer**. You structure test cases by viewpoint categori
|
|
|
26
30
|
**Screen**: Verify `qa/screens/${input:name}/` exists. If not → `/sungen-add-screen` first.
|
|
27
31
|
2. Check if `.feature` already has scenarios.
|
|
28
32
|
- If yes → summarize existing coverage and ask update mode (options depend on which tiers already exist — see `sungen-tc-generation` skill for details).
|
|
29
|
-
- If no → fresh creation. Ask generation scope:
|
|
30
|
-
- **1) Tier 1 — Critical & High priority** — ~10-15 scenarios/section
|
|
31
|
-
- **2) Full coverage
|
|
33
|
+
- If no → fresh creation. **Write the feature file incrementally** (successive writes/edits, ≈10-15 scenarios per call) — never emit the whole suite in one response, or it can exceed the model's output-token cap (`response exceeded the N output token maximum`). Ask generation scope:
|
|
34
|
+
- **1) Tier 1 — Critical & High priority** — ~10-15 scenarios/section: happy paths, core validation, security basics **(Recommended)**
|
|
35
|
+
- **2) Full coverage (incremental)** — Tier 1 + 2 + 3, written tier-by-tier in batches. Safe on any output-token budget.
|
|
36
|
+
- **3) Full coverage (single pass)** — everything in one go (~40-60 scenarios/section). Faster, but **only if you raised your output cap** (`CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`) — otherwise it errors mid-generation. For power users on a high-token model/config.
|
|
32
37
|
3. **Read project context + screen requirements**
|
|
33
38
|
|
|
34
39
|
**Project context** — check `qa/context.md` (project root, not screen-specific):
|
|
@@ -60,7 +65,7 @@ You are a **Senior QA Engineer**. You structure test cases by viewpoint categori
|
|
|
60
65
|
|
|
61
66
|
4. Follow the `sungen-tc-generation` skill for section identification, viewpoint generation, and output format. **For flows**, use the "Flow Test Generation" section in the skill. When requirements exist, use the "Requirements-Driven Generation" strategy. **For Tier 1**, apply the **Lightweight Guard** — verify required fields, validation rules, business rules, security checks, and key state transitions all have TCs after generation. **For Tier 2+**, **MUST** apply the full **Mapping Contract** — walk every `spec.md` section top-to-bottom and produce the indicated TCs per Table 1; handle `test-viewpoint.md` per Table 2. Do not silently skip sections. Present sections as a numbered list and let user pick.
|
|
62
67
|
5. Generate or update `.feature` + `test-data.yaml` following `sungen-gherkin-syntax` and `sungen-tc-generation` skills. **For flows**: use `[Screen:Element]` namespace format, namespace test-data by phase, add `@flow` tag.
|
|
63
|
-
5.5. **Quality gate & repair (harness — always run).** Per `sungen-harness-audit`: run `sungen audit --screen ${input:name}` (structural), THEN do an **independent semantic review inline** using the `sungen-reviewer` criteria (does each scenario's steps PROVE its title/viewpoint? observable Thens? business-critical assertion depth?). Merge both sets of issues; if gate FAILs / findings exist, repair (budget 3) and re-audit — GATE missing theme → generate it (cross-screen →
|
|
68
|
+
5.5. **Quality gate & repair (harness — always run).** Per `sungen-harness-audit`: run `sungen audit --screen ${input:name}` (structural), THEN do an **independent semantic review inline** using the `sungen-reviewer` criteria (does each scenario's steps PROVE its title/viewpoint? observable Thens? business-critical assertion depth?). Merge both sets of issues; if gate FAILs / findings exist, repair (budget 3) and re-audit — GATE missing theme → generate it (cross-screen → **automate it in the flow** via `/sungen:add-flow`, NOT a full `@manual` screen duplicate — `sungen audit` flags an automatable `@manual` as `MANUAL-AUTOMATABLE`; reserve `@manual:Mx` for true judgment/missing-capability); DEPTH → add data assertions; BALANCE → add business-core first; TRACE → align VP ids. Never fake a pass.
|
|
64
69
|
5.6. **Record.** `sungen manifest --screen ${input:name}`. Ledger **each phase** (not just repair) — pick one `runId` at the start and pass it so `trace`/`ledger report` show THIS run, not a mix: `sungen ledger record --screen ${input:name} --run <runId> --step <discovery|viewpoint|gherkin|audit|repair:N> --ms <elapsed>`. On re-run, start with `sungen manifest --screen ${input:name} --diff` and only regenerate changed sections.
|
|
65
70
|
6. **Converge — show the trace.** Run `sungen trace --screen ${input:name}` and present: process map (phases + repair rounds), bottlenecks, **HUMAN-LOOP FOCUS** (@manual to verify), audit score + gate + residual gaps. Then offer next steps based on which tier was just generated:
|
|
66
71
|
|
|
@@ -30,7 +30,16 @@ Count 0 → offer the user:
|
|
|
30
30
|
|
|
31
31
|
Skip when `--env` matches the base locale.
|
|
32
32
|
|
|
33
|
-
**Auto-detect context**: check if `qa/flows/<name>/` exists → flow mode (base path: `qa/flows/<name>/`). Else
|
|
33
|
+
**Auto-detect context**: check if `qa/api/<name>/` or `qa/api/flows/<name>/` exists → **API unit mode** (below). Else if `qa/flows/<name>/` → flow mode (base path: `qa/flows/<name>/`). Else `qa/screens/<name>/` → screen mode (base path: `qa/screens/<name>/`).
|
|
34
|
+
|
|
35
|
+
## API unit mode (driver-api) — no selectors
|
|
36
|
+
|
|
37
|
+
If the unit is **api-first**, skip every selector/capture phase (an API test has no DOM):
|
|
38
|
+
1. **Resolve the datasource** — `base_url` + auth wired in `qa/datasources.yaml` + `.env.qa` (`${X_URL}` from `sungen api init`); a `production` datasource is refused unless `SUNGEN_ALLOW_PROD=1`.
|
|
39
|
+
2. **Compile**: `npx sungen generate --area <name>` → `specs/generated/api/<name>/`.
|
|
40
|
+
3. **Run**: `npx playwright test specs/generated/api/<name>/<name>.spec.ts`.
|
|
41
|
+
4. **Auto-fix** (use `sungen-error-mapping`): 401/403 → `@hybrid`+`@auth` or `Bearer :token` header (`sungen makeauth`); base_url unresolved → set `${X_URL}`; missing param → trace `{{var}}` to test-data/a prior `@api` response; `expect.status` mismatch → reconcile against `apis.yaml` (re-`generate --area`, never hand-edit the spec); **400 "parameter missing" / body ignored → set `encoding: form` (or `multipart`) on the catalog entry, don't mark @manual**; flaky → self-clean + `@concurrent` caps.
|
|
42
|
+
5. **Integrity + trace** — `sungen script-check --area <name>` (1:1; on DRIFT re-`generate --area`, never hand-edit the spec) + `sungen trace --area <name>` (process map + HUMAN-LOOP FOCUS). Report + offer next steps.
|
|
34
43
|
|
|
35
44
|
## Pre-run (phased — per `sungen-selector-fix` skill)
|
|
36
45
|
|
|
@@ -84,6 +93,7 @@ Skip when `--env` matches the base locale.
|
|
|
84
93
|
7. **Phase 3 — Full Run**: Run all tests. Fix only **new** failures (elements unique to `@normal`/`@low`). Max 1 attempt. Don't loop on low-priority failures.
|
|
85
94
|
8. **Phase 4 — Regression**: One final full run. Report results. No more fix loops.
|
|
86
95
|
9. **Integrity & trace (always run after the final run).** `sungen script-check --screen <name>` — verify the spec is a **1:1** of the Gherkin; if **DRIFT**, re-run `sungen generate --screen <name>` (never hand-edit the `.spec.ts` — auto-fix edits `selectors.yaml`). Then `sungen ledger record --screen <name> --step run --ms <elapsed>` and `sungen trace --screen <name>` to show the process map + bottlenecks + **HUMAN-LOOP FOCUS**.
|
|
96
|
+
10. **Capability-pending offer (consent-gated).** If `sungen audit` reports `AUTOMATION-READY-PENDING` (or `@requires:<cap>` tests are skipped "requires …"), offer: *"N scenario(s) are automation-ready — enable `<cap>` to run them? (`sungen capability add <cap>`)"*. Only on the user's yes, run `sungen capability add <cap>` + re-run; on no, leave skipped (not failures, not manual). **Never auto-install.**
|
|
87
97
|
|
|
88
98
|
## Playwright command guidelines
|
|
89
99
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sungen-api-design
|
|
3
|
+
description: The API-first design loop for an api unit (qa/api/<area> or qa/api/flows/<flow>) — discover the catalog, lay out the API viewpoints, generate @api/@cases/flow/@concurrent scenarios, then drive the sungen audit --area gate + reviewer + repair to a high businessDepth (≥0.7). Use when create-test/run-test detects an api unit (no selectors, no visual capture).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API design loop (driver-api · Orchestration + Harness)
|
|
7
|
+
|
|
8
|
+
Use this when the unit is **api-first** — `qa/api/<area>/` or `qa/api/flows/<flow>/`. There are **no selectors and no visual capture**: the contract is the **named-endpoint catalog** (`api/apis.yaml`), referenced by `@api:<name>`. QA writes **no HTTP code**. Full annotation reference: the **API Steps** guide (`@api` / `@cases` / flows / `@concurrent` / `@hybrid`).
|
|
9
|
+
|
|
10
|
+
## The loop (mirror of /sungen:design, API-native)
|
|
11
|
+
|
|
12
|
+
### 1. Discover (no capture)
|
|
13
|
+
Run `sungen context --area <name>` — it reads the catalog and prints the **endpoints** + the **generation units** (one `matrix` unit per endpoint, an `async` unit per mutating endpoint, a `flow` unit for an api flow). Read `qa/api/<name>/requirements/spec.md` if present. No `apis.yaml` yet? → `sungen api import <openapi|csv>` or `sungen api add --area <name>` first.
|
|
14
|
+
|
|
15
|
+
### 2. API viewpoint overview (by method-profile)
|
|
16
|
+
For each endpoint, cover its viewpoints — severity-weighted by method:
|
|
17
|
+
|
|
18
|
+
| Profile | Endpoints | Must cover | Then |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| read | GET, HEAD | `contract` (status + body shape) | `pagination`/`filter` (list), `not-found` (by-id) |
|
|
21
|
+
| mutating | POST/PUT/PATCH/DELETE | `contract`, `error` (validation/4xx/auth) | `idempotency` (`@concurrent`), `side-effect` (`@query`) |
|
|
22
|
+
|
|
23
|
+
Bands: **~70%** success+failure matrix · **~20%** flows (auth/CRUD chains) · **~10%** async/idempotency.
|
|
24
|
+
|
|
25
|
+
### 3. Generate (incremental — never the whole suite in one Write)
|
|
26
|
+
- **Contract**: `@api:<name>` + `expect {{name.status}} is …` **and a body assertion** (`{{name.body.<path>}}`).
|
|
27
|
+
- **Error matrix**: `@api:<name>(p={{p}}) @cases:<dataset>` — one scenario, a dataset of `input → expected status`.
|
|
28
|
+
- **Flow**: ordered `@api` tags threading a prior response (`token={{login.body.token}}` → the catalog `Bearer :token` header; `id={{create.body.id}}` → a path param). Self-clean (delete what you create).
|
|
29
|
+
- **Idempotency**: `@api:<name> @concurrent:N` + `expect {{name.ok_count}} is 1`, cross-checked with `@query` (the DB is the oracle).
|
|
30
|
+
|
|
31
|
+
### 4. Gate + repair (always — businessDepth ≥ 0.7 is the bar)
|
|
32
|
+
Run `sungen audit --area <name>`; read `gateStatus` + `findings`. Then the **semantic reviewer** (sungen-reviewer sub-agent, API criteria). Repair **both** (budget 3 rounds), re-audit until PASS:
|
|
33
|
+
|
|
34
|
+
| Finding | Repair |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `VIEWPOINT-API-CONTRACT` | the endpoint is invoked but its response is never asserted → add `expect {{name.status}}` + a `{{name.body.…}}` check |
|
|
37
|
+
| `VIEWPOINT-API-ERROR` | a mutating endpoint has no failure scenario → add a `@cases` error matrix (or an explicit 4xx) |
|
|
38
|
+
| `VIEWPOINT-API-IDEMPOTENCY` | a mutating endpoint has no race check → add `@concurrent:N` + a `@query` DB cross-check |
|
|
39
|
+
| `VIEWPOINT-API-MANUAL-AUTOMATABLE` | a `@manual` scenario whose endpoint resolves is automatable → drop `@manual`, use `@api` (+ `@cases`); reserve `@manual` for genuine judgment cases |
|
|
40
|
+
| **`DEPTH-FAIL`** (businessDepth < 0.7) | a **mutating success** scenario asserts only `status` → make it **prove the effect**: assert a response **body** field, a **`@query`** side-effect, or a **`@concurrent` `ok_count`** invariant. (An error/`@cases` scenario proving the status is correct — it is *not* depth-required.) |
|
|
41
|
+
|
|
42
|
+
Stop when the gate PASSes + businessDepth ≥ 0.7, or the budget is exhausted → report residual gaps honestly (mark genuinely-unautomatable cases `@manual` with an oracle). Never fake a pass.
|
|
43
|
+
|
|
44
|
+
### 5. Record + converge
|
|
45
|
+
`sungen manifest --area <name>` (reuse) and ledger each phase; show the trace + the HUMAN-LOOP FOCUS. (Integrity `script-check`/`trace` for api: see run-test.)
|
|
46
|
+
|
|
47
|
+
## Taxonomy (label scenarios correctly)
|
|
48
|
+
|
|
49
|
+
| Class | What | Examples |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| **Functional** | single-endpoint behaviour | happy contract · error/validation (`@cases`) · boundary/edge |
|
|
52
|
+
| **Functional — flow/integration** | multi-endpoint journeys | auth/CRUD lifecycle (`create → login → get → delete`), cross-endpoint invariants |
|
|
53
|
+
| **Non-Functional** | performance · reliability · **security** · concurrency/idempotency | `@concurrent` race/idempotency |
|
|
54
|
+
|
|
55
|
+
A flow (`create → login → delete`) is a **Functional integration** test, **not** non-functional — don't file it under "Non-Functional". Reserve non-functional for perf/security/concurrency.
|
|
56
|
+
|
|
57
|
+
## Rules
|
|
58
|
+
- **No HTTP, no selectors** — only `.feature` + the reviewed `apis.yaml` + `test-data`.
|
|
59
|
+
- **Non-prod default** — a `production` datasource is refused unless `SUNGEN_ALLOW_PROD=1`.
|
|
60
|
+
- **The DB is the oracle** for idempotency/side-effects — HTTP status alone can lie; pair `@api` with `@query`.
|
|
61
|
+
- **`@parallel` + mutating endpoints** — give each scenario **isolated data** (a `{{$uuid}}` email, a `@cases` row, or its own created resource) and **self-clean** (delete what it created); shared inputs race under parallel execution.
|
|
62
|
+
- **No dead data** — every `test-data` key must be bound into a scenario (`{{key}}`, a `@cases` dataset, or an override). `sungen audit`/the generate lint flag unreferenced keys.
|
|
@@ -213,6 +213,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
213
213
|
| `@flow` | Mark feature as E2E flow (cross-screen testing) |
|
|
214
214
|
| `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
|
|
215
215
|
| `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
|
|
216
|
+
| `@api:name` | API: run the named request from `api/apis.yaml` (precondition) and bind the response to `{{name}}`; assert with `expect {{name.status}} …` + path access (`{{name.body.<path>}}`). Override params `@api:name(p={{v}})`. Repeatable. (Optional API Driver) |
|
|
216
217
|
|
|
217
218
|
### Data-driven scenarios (`@cases`)
|
|
218
219
|
|
|
@@ -58,7 +58,7 @@ Use these when repairing GATE/DEPTH findings for the hard viewpoints (cart/detai
|
|
|
58
58
|
```
|
|
59
59
|
`see all [X] contain {{v}}` asserts EVERY matching element contains the value → "all displayed products belong to the selected category/brand", not just one.
|
|
60
60
|
|
|
61
|
-
> Cross-screen flows (home → detail/cart):
|
|
61
|
+
> Cross-screen flows (home → detail/cart): **automate the journey as a flow** (`/sungen:add-flow`) — it runs as one test, so it is automatable. Do **not** keep a full `@manual` duplicate of it on the screen (a non-running dead copy that `sungen audit` flags as `MANUAL-AUTOMATABLE` and that inflates nothing — deferred business-critical is reported as `DEPTH-DEFERRED`). The screen keeps its screen-contract; the flow owns the cross-screen depth. `@manual` is for genuine judgment / missing-capability only, tagged `@manual:Mx`.
|
|
62
62
|
|
|
63
63
|
## Repair loop rules
|
|
64
64
|
|
|
@@ -66,6 +66,7 @@ Use these when repairing GATE/DEPTH findings for the hard viewpoints (cart/detai
|
|
|
66
66
|
2. **Stop when** `gateStatus == PASS` AND `findings` empty — or budget exhausted.
|
|
67
67
|
3. **Never fake a pass.** A shallow `see [Cart] page` does not satisfy `cart-correctness`. If a gap is genuinely cross-screen or needs capabilities the DSL lacks (e.g. capture an element value to compare elsewhere), **report it as a residual gap / flow item** instead of forcing a green gate.
|
|
68
68
|
4. **EP/data families are OK.** A `duplicates` cluster with `sameDataLikely=false` is an intentional equivalence-partition family (e.g. many invalid-email cases) — keep it; only collapse `sameDataLikely=true` exact duplicates.
|
|
69
|
+
5. **Advisory findings — surface, don't gate.** `MANUAL-REASON-MISMATCH` → fix the scenario's `@manual:Mx` code (so the planner recommends the right driver) during repair. `CAPABILITY-SUGGESTION` → **present it to the user as a next-step option** (e.g. "N @manual could be automated — `sungen capability add api db`?"), **recommend-only — never auto-install**. Neither fails the gate.
|
|
69
70
|
|
|
70
71
|
## Discovery / fallback tree (when input is limited)
|
|
71
72
|
|
|
@@ -6,6 +6,9 @@ user-invocable: false
|
|
|
6
6
|
|
|
7
7
|
## ⚠️ Gotchas — read before generating
|
|
8
8
|
|
|
9
|
+
- **Write incrementally — never emit the whole suite in one response.** Build the `.feature` in batches via successive `Write`/`Edit` (≈10–15 scenarios per call). For **Full coverage**, write tier-by-tier: `Write` Tier 1 → `Edit` append Tier 2 → `Edit` append Tier 3.
|
|
10
|
+
→ One huge `Write` can exceed the model's output-token cap → `API Error: Claude's response exceeded the N output token maximum`. Single-pass full coverage only fits when `CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`; otherwise batch. Batching also lets the audit/reviewer run per batch — higher quality.
|
|
11
|
+
|
|
9
12
|
- `spec_figma.md` exists → read file only, **NEVER** call `mcp__figma__*`
|
|
10
13
|
→ PAT auth flow already done by `sungen-capture` (mode figma-pat); re-calling fails or duplicates work.
|
|
11
14
|
|
|
@@ -265,12 +268,26 @@ Security: [S1 – admin only]
|
|
|
265
268
|
Then User see [Detail Product Name] header with {{selected_product_name}}
|
|
266
269
|
And User see [Detail Product Price] text contains {{selected_product_price}}
|
|
267
270
|
```
|
|
268
|
-
Cross-screen target →
|
|
271
|
+
Cross-screen target → **automate it in the flow** (`/sungen:add-flow`), NOT as a `@manual` screen copy. A single home→target journey runs as one Playwright test, so it is automatable — "needs another screen" is not a reason for `@manual`. The screen keeps its screen-contract scenarios; the flow owns the cross-screen depth.
|
|
269
272
|
- Filter result (category AND brand, separately): `Then User see all [Result Product Name] contain {{selected_category}}` — proves EVERY item belongs, not one.
|
|
270
273
|
|
|
271
274
|
**Depth is a GATE dimension (harness-roadmap P1) — self-raise, never silently go shallow:**
|
|
272
275
|
- For every data-correctness theme the catalog marks `depth.requires: data-assertion`, emit its `depth.template` shape by **default** — don't wait for the repair loop. `sungen audit` measures `businessDepth` (ratio of these scenarios that assert data) against an intent threshold (functional ≥ 0.70); below it the **gate FAILs**.
|
|
273
|
-
- `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape
|
|
276
|
+
- `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape as an **automated flow scenario** (in the flow — do NOT leave a full-step `@manual` duplicate on the screen). `@manual` is **only** for genuine judgment (M6 visual/UX · M8 not-worth · M9 human) or a missing capability (M1–M5/M7), and it **must** carry a reason code (`@manual:Mx`, or a reason comment the planner can infer). A `@manual` scenario that still has full automatable steps (a data assertion, no visual/mock/a11y judgment) is now flagged by `sungen audit` as `MANUAL-AUTOMATABLE`, and business-critical scenarios you defer to `@manual` are reported as `DEPTH-DEFERRED` (they do NOT silently inflate `businessDepth`). Deferring automatable work to `@manual` lowers quality — automate it in the flow instead.
|
|
277
|
+
- **Pick the right `@manual:Mx` code — it decides which driver can later automate the case** (`sungen audit` flags a code↔reason mismatch). Tag the code that matches the **oracle the reason describes**:
|
|
278
|
+
|
|
279
|
+
| The reason needs… | Code | Unblocked by |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| a data state you can't make from the UI (empty list, seeded record, missing-image product) | `M1` | data-factory / db |
|
|
282
|
+
| an **API/DB/persistence** assertion (stored value, parameterized-query / SQLi-safe, server-side effect) | `M2` | **api / db** |
|
|
283
|
+
| network / fault injection (offline, slow, request failure) | `M3` | mock |
|
|
284
|
+
| a stable selector / test-id that doesn't exist | `M4` | — (locator contract) |
|
|
285
|
+
| an external dependency (email, payment gateway, download) | `M5` | mail-file / contract |
|
|
286
|
+
| visual / UX / responsive / a11y judgment | `M6` | — (keep manual) |
|
|
287
|
+
| not worth automating · true human judgment | `M8` / `M9` | — (keep manual) |
|
|
288
|
+
|
|
289
|
+
e.g. "submit a payload then check the subscribers **table**" is an API+DB oracle → `@manual:M2` (NOT `M1`); "seed a DB with zero products" is a data state → `M1`; "throttle the network" → `M3`.
|
|
290
|
+
- **Prefer automation-ready `@requires:<cap>` over prose `@manual`.** When you *can* write the steps for a capability-manual case (an API/DB oracle, a seeded state), write it **automation-ready** — the real `@api`/`@query`/… steps tagged `@requires:<cap>` (e.g. `@requires:db @query:subscriber_row`) — instead of a prose `@manual:M2`. It compiles to a skipped-with-reason stub until `sungen capability add <cap>`, then runs as a real test with **no rewrite**. Reserve prose `@manual:Mx` for cases whose steps genuinely can't be expressed (M6/M8/M9 judgment, or a capability with no driver). `sungen audit` reports these as `AUTOMATION-READY-PENDING` (not a gap, not manual).
|
|
274
291
|
- **If the spec lacks the concrete value** a deep assertion needs (exact message, price, count): still write the deep shape with a `{{var}}` placeholder and leave a `# SPEC-GAP: <field> value not in spec` comment — do **not** downgrade to `see [X] section`. A visible gap is better than a silent shallow pass.
|
|
275
292
|
- **Blind-Spot Memory:** before finishing, run `sungen blindspot list --prompt` (Bash) and make sure the suite satisfies each recorded pattern (e.g. "for any Add/Create action: check success + resulting data state + duplicate/double-submit"). These are gaps QA hit before — don't repeat them.
|
|
276
293
|
|
|
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
|
|
|
69
69
|
- VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
|
|
70
70
|
- VP-SEC = checks access control and malicious input
|
|
71
71
|
|
|
72
|
+
### Domain category codes — required for the coverage-balance gate
|
|
73
|
+
|
|
74
|
+
The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
|
|
75
|
+
|
|
76
|
+
| Bucket | Codes | Use for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
|
|
79
|
+
| presentation | `UI` | layout / visual state |
|
|
80
|
+
| validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
|
|
81
|
+
| behavior | `LOGIC` | action-driven state changes |
|
|
82
|
+
| navigation | `NAV` | landing on / moving between pages |
|
|
83
|
+
|
|
84
|
+
**On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
|
|
85
|
+
|
|
72
86
|
---
|
|
73
87
|
|
|
74
88
|
## Shared Checks
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Sungen API Driver — runtime helper (auto-generated into specs/api.ts).
|
|
4
|
+
*
|
|
5
|
+
* Runs a catalog-defined HTTP request and returns { status, ok, body, headers } — bound to a
|
|
6
|
+
* `{{name}}` variable by the `@api:<name>` annotation, asserted with `expect {{name.status}} …` /
|
|
7
|
+
* `{{name.body.<path>}}`. Base URL + auth come from a `kind: api` datasource in datasources.yaml,
|
|
8
|
+
* with `${VAR}` resolved from .env.qa / process.env — never inline.
|
|
9
|
+
*
|
|
10
|
+
* Safety: a datasource flagged `env: production` is refused unless SUNGEN_ALLOW_PROD=1.
|
|
11
|
+
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { request, type APIRequestContext } from '@playwright/test';
|
|
16
|
+
|
|
17
|
+
interface ApiDataSource {
|
|
18
|
+
kind?: string;
|
|
19
|
+
base_url?: string;
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
env?: string;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
timeout_ms?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadEnvQa(): void {
|
|
27
|
+
for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
|
|
28
|
+
const p = path.join(process.cwd(), name);
|
|
29
|
+
if (!name.endsWith('.') && fs.existsSync(p)) {
|
|
30
|
+
for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
|
|
31
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
32
|
+
if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadConfig(): Record<string, ApiDataSource> {
|
|
39
|
+
loadEnvQa();
|
|
40
|
+
const file = [path.join(process.cwd(), 'datasources.yaml'), path.join(process.cwd(), 'qa', 'datasources.yaml')].find((f) => fs.existsSync(f));
|
|
41
|
+
if (!file) throw new Error('API Driver: no datasources.yaml found (project root or qa/).');
|
|
42
|
+
const raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
|
|
43
|
+
const { parse } = require('yaml');
|
|
44
|
+
const doc = parse(raw) || {};
|
|
45
|
+
return doc.datasources || {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function substitute(text: string, params: Record<string, any>): string {
|
|
49
|
+
return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => encodeURIComponent(String(params[p] ?? '')));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ApiClient {
|
|
53
|
+
private configs: Record<string, ApiDataSource> | null = null;
|
|
54
|
+
|
|
55
|
+
private cfg(name?: string): { key: string; conf: ApiDataSource } {
|
|
56
|
+
if (!this.configs) this.configs = loadConfig();
|
|
57
|
+
const key = name || Object.keys(this.configs).find((k) => (this.configs![k].kind || 'api') === 'api') || Object.keys(this.configs)[0];
|
|
58
|
+
const conf = this.configs[key];
|
|
59
|
+
if (!conf) throw new Error(`API Driver: datasource "${key}" not found in datasources.yaml`);
|
|
60
|
+
if (conf.env === 'production' && process.env.SUNGEN_ALLOW_PROD !== '1') {
|
|
61
|
+
throw new Error(`API Driver: datasource "${key}" is env: production — refused (set SUNGEN_ALLOW_PROD=1 to override).`);
|
|
62
|
+
}
|
|
63
|
+
return { key, conf };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run a catalog request and return the response. `req` is embedded at compile time; `params` (path
|
|
68
|
+
* `:id`, JSON body `:fields`, and header `:tokens`) bind at runtime. Catalog `headers` layer over the
|
|
69
|
+
* datasource headers and may carry `:param` placeholders — e.g. `authorization: "Bearer :token"` with
|
|
70
|
+
* the dynamic token threaded from a prior response (flow chaining).
|
|
71
|
+
*/
|
|
72
|
+
async call(
|
|
73
|
+
label: string,
|
|
74
|
+
req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; headers?: Record<string, string>; datasource?: string },
|
|
75
|
+
params: Record<string, any> = {},
|
|
76
|
+
opts: { storageState?: string } = {},
|
|
77
|
+
): Promise<{ status: number; ok: boolean; body: any; headers: Record<string, string> }> {
|
|
78
|
+
const { conf } = this.cfg(req.datasource);
|
|
79
|
+
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
80
|
+
if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
81
|
+
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
82
|
+
|
|
83
|
+
const headers: Record<string, string> = { ...(conf.headers || {}) };
|
|
84
|
+
// catalog headers; :param tokens bind at runtime — raw (no URL-encoding, unlike the path)
|
|
85
|
+
for (const [k, v] of Object.entries(req.headers || {}))
|
|
86
|
+
headers[k] = String(v).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => String(params[p] ?? ''));
|
|
87
|
+
// Body: substitute `:param` into the body template (object values), then encode per `encoding`.
|
|
88
|
+
let body: any;
|
|
89
|
+
if (req.body !== undefined && req.body !== null) {
|
|
90
|
+
body = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
91
|
+
}
|
|
92
|
+
// Map the wire format to the right Playwright option (#345): json → data (application/json,
|
|
93
|
+
// default), form → form (application/x-www-form-urlencoded), multipart → multipart (form-data).
|
|
94
|
+
const bodyOpt: Record<string, unknown> = {};
|
|
95
|
+
if (body !== undefined) {
|
|
96
|
+
const enc = req.encoding ?? 'json';
|
|
97
|
+
if (enc === 'form') bodyOpt.form = body;
|
|
98
|
+
else if (enc === 'multipart') bodyOpt.multipart = body;
|
|
99
|
+
else bodyOpt.data = body;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Playwright APIRequestContext: same runner/report/retries as UI tests. @hybrid passes
|
|
103
|
+
// `storageState` (the @auth role's saved session) so the request shares the browser's
|
|
104
|
+
// authenticated cookies. Disposed per call so no request context lingers and hangs the process.
|
|
105
|
+
const ctx: APIRequestContext = await request.newContext({
|
|
106
|
+
baseURL: base,
|
|
107
|
+
extraHTTPHeaders: headers,
|
|
108
|
+
timeout: conf.timeout_ms ?? 15000,
|
|
109
|
+
...(opts.storageState ? { storageState: opts.storageState } : {}),
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
const res = await ctx.fetch(urlPath, { method: req.method, ...bodyOpt });
|
|
113
|
+
const text = await res.text();
|
|
114
|
+
let parsed: any = text;
|
|
115
|
+
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
116
|
+
return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
|
|
117
|
+
} finally {
|
|
118
|
+
await ctx.dispose();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fire the same request N times in parallel (the `@concurrent:N` primitive) and bind aggregates —
|
|
124
|
+
* the idempotency/race oracle. Returns the full `responses` array plus `ok_count`, `status_counts`,
|
|
125
|
+
* and `statuses`, asserted with `expect {{name.ok_count}} is 1` (and cross-checked against the DB via
|
|
126
|
+
* `@query` to prove "exactly one charge"). Path access works on the bound value: `{{name.ok_count}}`,
|
|
127
|
+
* `{{name.status_counts.409}}`, `{{name.responses.count}}`, `{{name.responses[0].body.id}}`.
|
|
128
|
+
*/
|
|
129
|
+
async callN(
|
|
130
|
+
label: string,
|
|
131
|
+
req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; headers?: Record<string, string>; datasource?: string },
|
|
132
|
+
params: Record<string, any> = {},
|
|
133
|
+
n = 1,
|
|
134
|
+
opts: { storageState?: string } = {},
|
|
135
|
+
): Promise<{
|
|
136
|
+
responses: Array<{ status: number; ok: boolean; body: any; headers: Record<string, string> }>;
|
|
137
|
+
ok_count: number;
|
|
138
|
+
status_counts: Record<string, number>;
|
|
139
|
+
statuses: number[];
|
|
140
|
+
}> {
|
|
141
|
+
const count = Math.max(1, Math.floor(n));
|
|
142
|
+
const responses = await Promise.all(Array.from({ length: count }, () => this.call(label, req, params, opts)));
|
|
143
|
+
const status_counts: Record<string, number> = {};
|
|
144
|
+
for (const r of responses) status_counts[String(r.status)] = (status_counts[String(r.status)] || 0) + 1;
|
|
145
|
+
return {
|
|
146
|
+
responses,
|
|
147
|
+
ok_count: responses.filter((r) => r.ok).length,
|
|
148
|
+
status_counts,
|
|
149
|
+
statuses: responses.map((r) => r.status),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const api = new ApiClient();
|
|
@@ -21,12 +21,73 @@ const ident = (s: string): string => {
|
|
|
21
21
|
return s;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
interface SshConfig {
|
|
25
|
+
host: string; // jump host reachable from the runner
|
|
26
|
+
port?: number; // default 22
|
|
27
|
+
user: string;
|
|
28
|
+
private_key?: string; // PEM contents (from ${VAR} in .env.qa) — preferred for CI
|
|
29
|
+
private_key_path?: string; // or a filesystem path (local dev)
|
|
30
|
+
passphrase?: string; // for an encrypted key
|
|
31
|
+
known_host?: string; // base64 of the server's host key to pin (optional; else warn-and-proceed)
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
interface DataSourceConfig {
|
|
25
35
|
engine: 'postgres' | 'mysql' | 'sqlite';
|
|
26
36
|
url: string;
|
|
27
37
|
readonly?: boolean;
|
|
28
38
|
statement_timeout_ms?: number;
|
|
29
39
|
max_rows?: number;
|
|
40
|
+
// Cách B (fallback): tunnel the DB SOCKET through an SSH bastion. DB-only — the browser/E2E
|
|
41
|
+
// still run on the runner; only PG traffic crosses. See docs/spec/sungen_data_driver_ssh_tunnel_spec.md.
|
|
42
|
+
ssh?: SshConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Open a local TCP forward (127.0.0.1:<ephemeral> → ssh bastion → dstHost:dstPort) for a DB socket.
|
|
47
|
+
* Sockets are unref()'d so a dangling tunnel never keeps the test process alive after the run.
|
|
48
|
+
*/
|
|
49
|
+
async function openSshTunnel(ssh: SshConfig, dstHost: string, dstPort: number): Promise<{ host: string; port: number; close: () => void }> {
|
|
50
|
+
const { Client } = require('ssh2');
|
|
51
|
+
const net = require('net');
|
|
52
|
+
const privateKey = ssh.private_key
|
|
53
|
+
? ssh.private_key
|
|
54
|
+
: ssh.private_key_path
|
|
55
|
+
? fs.readFileSync(ssh.private_key_path.replace(/^~(?=\/)/, process.env.HOME || ''), 'utf8')
|
|
56
|
+
: undefined;
|
|
57
|
+
if (!privateKey) throw new Error('Data Driver: datasource `ssh` requires `private_key` or `private_key_path`.');
|
|
58
|
+
|
|
59
|
+
const conn = new Client();
|
|
60
|
+
await new Promise<void>((resolve, reject) => {
|
|
61
|
+
conn.on('ready', resolve).on('error', reject).connect({
|
|
62
|
+
host: ssh.host,
|
|
63
|
+
port: ssh.port ?? 22,
|
|
64
|
+
username: ssh.user,
|
|
65
|
+
privateKey,
|
|
66
|
+
passphrase: ssh.passphrase,
|
|
67
|
+
hostVerifier: (key: Buffer) => {
|
|
68
|
+
const got = Buffer.isBuffer(key) ? key.toString('base64') : String(key);
|
|
69
|
+
if (ssh.known_host) {
|
|
70
|
+
if (got === ssh.known_host.trim()) return true;
|
|
71
|
+
throw new Error(`Data Driver: SSH host-key mismatch for ${ssh.host} — refused (known_host pin).`);
|
|
72
|
+
}
|
|
73
|
+
console.warn(`Data Driver: SSH host key for ${ssh.host} is not pinned (set datasource ssh.known_host to verify). Proceeding (TOFU).`);
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const server = net.createServer((sock: any) => {
|
|
80
|
+
conn.forwardOut(sock.remoteAddress || '127.0.0.1', sock.remotePort || 0, dstHost, dstPort, (err: any, stream: any) => {
|
|
81
|
+
if (err) { sock.destroy(); return; }
|
|
82
|
+
sock.pipe(stream).pipe(sock);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
await new Promise<void>((resolve, reject) => server.on('error', reject).listen(0, '127.0.0.1', () => resolve()));
|
|
86
|
+
const addr = server.address();
|
|
87
|
+
const port = addr && typeof addr === 'object' ? addr.port : 0;
|
|
88
|
+
server.unref(); // don't keep the event loop alive after tests
|
|
89
|
+
try { (conn as any)._sock?.unref?.(); } catch { /* best-effort */ }
|
|
90
|
+
return { host: '127.0.0.1', port, close: () => { try { server.close(); } catch {} try { conn.end(); } catch {} } };
|
|
30
91
|
}
|
|
31
92
|
|
|
32
93
|
function loadEnvQa(): void {
|
|
@@ -64,6 +125,7 @@ type Engine = { query(sql: string, params: any[]): Promise<any[]>; };
|
|
|
64
125
|
class DataSource {
|
|
65
126
|
private configs: Record<string, DataSourceConfig> | null = null;
|
|
66
127
|
private engines = new Map<string, Engine>();
|
|
128
|
+
private tunnels: Array<{ close: () => void }> = [];
|
|
67
129
|
|
|
68
130
|
private cfg(name?: string): { key: string; conf: DataSourceConfig } {
|
|
69
131
|
if (!this.configs) this.configs = loadConfig();
|
|
@@ -79,10 +141,19 @@ class DataSource {
|
|
|
79
141
|
if (!conf.url) throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
|
|
80
142
|
let engine: Engine;
|
|
81
143
|
if (conf.engine === 'postgres') {
|
|
144
|
+
let connectionString = conf.url;
|
|
145
|
+
if (conf.ssh) { // Cách B: tunnel the DB socket through a bastion
|
|
146
|
+
const u = new URL(conf.url);
|
|
147
|
+
const t = await openSshTunnel(conf.ssh, u.hostname, Number(u.port || 5432));
|
|
148
|
+
this.tunnels.push(t);
|
|
149
|
+
u.hostname = t.host; u.port = String(t.port); // rewrite host:port → 127.0.0.1:<tunnel> (keep user/pass/db/query)
|
|
150
|
+
connectionString = u.toString();
|
|
151
|
+
}
|
|
82
152
|
const { Pool } = require('pg');
|
|
83
|
-
const pool = new Pool({ connectionString
|
|
153
|
+
const pool = new Pool({ connectionString, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
|
|
84
154
|
engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
|
|
85
155
|
} else if (conf.engine === 'sqlite') {
|
|
156
|
+
if (conf.ssh) console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
|
|
86
157
|
const Database = require('better-sqlite3');
|
|
87
158
|
const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
|
|
88
159
|
engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
|
|
@@ -93,6 +164,12 @@ class DataSource {
|
|
|
93
164
|
return { engine, conf };
|
|
94
165
|
}
|
|
95
166
|
|
|
167
|
+
/** Close any open SSH tunnels (optional explicit teardown; tunnels are unref'd so the process exits regardless). */
|
|
168
|
+
close(): void {
|
|
169
|
+
for (const t of this.tunnels) t.close();
|
|
170
|
+
this.tunnels = [];
|
|
171
|
+
}
|
|
172
|
+
|
|
96
173
|
private build(table: string, filter: Record<string, any>): { sql: string; params: any[] } {
|
|
97
174
|
const cols = Object.keys(filter);
|
|
98
175
|
const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
|
|
@@ -23,7 +23,8 @@ export class TestDataLoader {
|
|
|
23
23
|
*/
|
|
24
24
|
static load(screenName: string, featureName: string): TestDataLoader {
|
|
25
25
|
let baseDir: string;
|
|
26
|
-
if (screenName.startsWith('flows/')) {
|
|
26
|
+
if (screenName.startsWith('flows/') || screenName.startsWith('api/')) {
|
|
27
|
+
// flows/<flow> · api/<area> · api/flows/<flow> → qa/<screenName>/test-data
|
|
27
28
|
baseDir = path.join(process.cwd(), 'qa', screenName, 'test-data');
|
|
28
29
|
} else {
|
|
29
30
|
baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { StepPattern } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* Assertion patterns: visibility, text content, state, attributes
|
|
4
|
-
* Uses template engine for framework-agnostic code generation
|
|
5
|
-
*/
|
|
6
|
-
export declare const assertionPatterns: StepPattern[];
|
|
7
|
-
//# sourceMappingURL=assertion-patterns.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"assertion-patterns.d.ts","sourceRoot":"","sources":["../../../../src/generators/test-generator/patterns/assertion-patterns.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAoB,MAAM,SAAS,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,WAAW,EA2qB1C,CAAC"}
|