@sun-asterisk/sungen 3.2.14 → 3.2.16-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +167 -51
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +3 -46
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/exporters/feature-parser.js +1 -1
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/locale-variants.d.ts +38 -0
- package/dist/exporters/locale-variants.d.ts.map +1 -0
- package/dist/exporters/locale-variants.js +159 -0
- package/dist/exporters/locale-variants.js.map +1 -0
- package/dist/exporters/matrix/build.d.ts +36 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -0
- package/dist/exporters/matrix/build.js +267 -0
- package/dist/exporters/matrix/build.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +47 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -0
- package/dist/exporters/matrix/export.js +136 -0
- package/dist/exporters/matrix/export.js.map +1 -0
- package/dist/exporters/matrix/fingerprint.d.ts +20 -0
- package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
- package/dist/exporters/matrix/fingerprint.js +83 -0
- package/dist/exporters/matrix/fingerprint.js.map +1 -0
- package/dist/exporters/matrix/gates.d.ts +21 -0
- package/dist/exporters/matrix/gates.d.ts.map +1 -0
- package/dist/exporters/matrix/gates.js +258 -0
- package/dist/exporters/matrix/gates.js.map +1 -0
- package/dist/exporters/matrix/map-loader.d.ts +22 -0
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
- package/dist/exporters/matrix/map-loader.js +173 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -0
- package/dist/exporters/matrix/render-csv.d.ts +8 -0
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
- package/dist/exporters/matrix/render-csv.js +69 -0
- package/dist/exporters/matrix/render-csv.js.map +1 -0
- package/dist/exporters/matrix/render-xlsx.d.ts +17 -0
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
- package/dist/exporters/matrix/render-xlsx.js +298 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -0
- package/dist/exporters/matrix/types.d.ts +153 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -0
- package/dist/exporters/matrix/types.js +15 -0
- package/dist/exporters/matrix/types.js.map +1 -0
- package/dist/exporters/scenario-merger.js +1 -1
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/spec-parser.js +1 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +3 -3
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
- package/dist/generators/test-generator/code-generator.js +1 -1
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +4 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +3 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +6 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +41 -2
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/harness/annotation-overrides.d.ts +4 -1
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +5 -2
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +6 -1
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability.d.ts +25 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +44 -2
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +5 -0
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +118 -120
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
- package/dist/orchestrator/templates/specs-test-data.ts +13 -4
- package/package.json +4 -4
- package/src/cli/commands/delivery.ts +199 -71
- package/src/dashboard/snapshot-builder.ts +11 -61
- package/src/dashboard/templates/index.html +1 -1
- package/src/exporters/feature-parser.ts +1 -1
- package/src/exporters/locale-variants.ts +157 -0
- package/src/exporters/matrix/build.ts +294 -0
- package/src/exporters/matrix/export.ts +124 -0
- package/src/exporters/matrix/fingerprint.ts +49 -0
- package/src/exporters/matrix/gates.ts +284 -0
- package/src/exporters/matrix/map-loader.ts +142 -0
- package/src/exporters/matrix/render-csv.ts +71 -0
- package/src/exporters/matrix/render-xlsx.ts +289 -0
- package/src/exporters/matrix/types.ts +176 -0
- package/src/exporters/scenario-merger.ts +1 -1
- package/src/exporters/spec-parser.ts +1 -1
- package/src/exporters/xlsx-report-builder.ts +3 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
- package/src/generators/test-generator/code-generator.ts +1 -1
- package/src/generators/test-generator/diagnostics.ts +4 -1
- package/src/generators/test-generator/step-mapper.ts +43 -2
- package/src/harness/annotation-overrides.ts +5 -2
- package/src/harness/audit.ts +7 -2
- package/src/harness/capability.ts +53 -3
- package/src/harness/quality-gates.ts +4 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +1 -1
- package/src/orchestrator/templates/ai-src/commands/delivery.md +118 -120
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
- package/src/orchestrator/templates/specs-test-data.ts +13 -4
|
@@ -30,7 +30,7 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
|
|
|
30
30
|
| `/sungen-create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
|
|
31
31
|
| `/sungen-review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
|
|
32
32
|
| `/sungen-run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
|
|
33
|
-
| `/sungen-delivery [name...]` | Export
|
|
33
|
+
| `/sungen-delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all screens if no arg) |
|
|
34
34
|
| `/sungen-locale <name> <locale>` | Bootstrap i18n for a screen — audit selectors, detect locale switch, generate overlay (run before `/sungen-run-test --env <locale>`) |
|
|
35
35
|
|
|
36
36
|
**Screen path:** add-screen → create-test → review → run-test → delivery.
|
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sungen-delivery
|
|
3
|
-
description: 'Export
|
|
3
|
+
description: 'Export the Test Case & Coverage Matrix (or the legacy per-scenario CSV) from Gherkin + Playwright results. Auto-loaded by delivery command.'
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Purpose
|
|
8
8
|
|
|
9
|
-
Export test cases from Sungen
|
|
9
|
+
Export test cases from Sungen units to the QA deliverable. Two modes:
|
|
10
10
|
|
|
11
|
-
**
|
|
11
|
+
- **Matrix (default)** — the **Test Case & Coverage Matrix**: one row per *delivery item* (one
|
|
12
|
+
target + one test intent + one oracle family); scenarios / `@cases` rows stay as collapsible
|
|
13
|
+
*coverage variant* sub-rows. Built for review, manual execution, customer hand-off, and
|
|
14
|
+
reporting — no numbered step dumps.
|
|
15
|
+
- **Legacy (`--legacy`/`--full`)** — the classic per-scenario BM-2-901-13 workbook (documented
|
|
16
|
+
in the second half of this skill; unchanged).
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
**This skill delegates all heavy work to the `sungen delivery` CLI.** The CLI is the single source
|
|
19
|
+
of truth for parsing + gating. The AI's only semantic contribution is **proposing the Delivery
|
|
20
|
+
Map** (grouping) — everything else is: invoke the CLI, show output verbatim, help on failures.
|
|
16
21
|
|
|
17
22
|
---
|
|
18
23
|
|
|
24
|
+
## Matrix mode — the Delivery Map
|
|
25
|
+
|
|
26
|
+
`qa/<screens|flows|api>/<unit>/delivery/<unit>.map.yaml` — committed, AI-proposed
|
|
27
|
+
(`review: proposed`), gate-validated, QA-approved (`sungen delivery --approve` stamps scenario
|
|
28
|
+
fingerprints). Schema + grouping rules live in the delivery command instructions; the normative
|
|
29
|
+
spec is `docs/spec/delivery-coverage-matrix-spec.md`.
|
|
30
|
+
|
|
31
|
+
**Gates** (CLI `--check`): A source (VP-ids unique, oracle present, Background setup-only) ·
|
|
32
|
+
B mapping (every scenario in exactly one group XOR one disposition) · C aggregation (mode/layer/
|
|
33
|
+
priority recomputed and equal within a group — heuristic oracle-shape/precondition mismatches are
|
|
34
|
+
review-level, silenced once approved and unchanged) · D executability (precondition · condition+
|
|
35
|
+
data · trigger · oracle all renderable; every `{{var}}` resolves) · E drift (fingerprint mismatch
|
|
36
|
+
→ back to review) · G review state (proposed groups block the official render; `--preview` renders
|
|
37
|
+
a DRAFT watermark).
|
|
38
|
+
|
|
39
|
+
**Workbook**: `Testcases` sheet (parent rows + outline-level-1 variant sub-rows — collapse for the
|
|
40
|
+
customer view, expand to execute; result roll-up failed→blocked→not_run→partial→passed, counts like
|
|
41
|
+
`7/8 Passed · 1 Failed`) + `Coverage` sheet (target × category grid with explicit `—` gaps,
|
|
42
|
+
dispositions, generation manifest). CSV is flat with a `Level` column (`item`/`variant`).
|
|
43
|
+
`delivery_item_count` ≠ progress — variants are the execution metric.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Legacy mode (--legacy / --full)
|
|
48
|
+
|
|
19
49
|
## Architecture
|
|
20
50
|
|
|
21
51
|
```
|
|
@@ -85,7 +115,13 @@ The `.xlsx` leads with an **`Overview`** roll-up sheet, then splits the test cas
|
|
|
85
115
|
- **`Auto`** — automatable test cases (`Auto` + `Not compiled`).
|
|
86
116
|
- **`Manual`** — `@manual` test cases (always present, header-only when there are none).
|
|
87
117
|
|
|
88
|
-
Multi-locale
|
|
118
|
+
Multi-locale: `Overview` + one **`<LOCALE> Auto`** sheet per locale + a single shared **`Manual`** sheet (manual TCs are locale-invariant). The **CSV stays one file with every row** — the `Testcase type` column distinguishes Auto vs Manual. The Overview lists every detail sheet, so each locale's counts appear on its own row.
|
|
119
|
+
|
|
120
|
+
**Which result files become locale sheets — locales are DECLARED, never guessed.** A result-file suffix (`<name>-test-result.<X>.json`) is a locale ONLY when `X` is listed in `qa/capabilities.yaml` `locales:` (written by `/sungen:locale`), or — when delivering with `--env <E>` / `SUNGEN_ENV=<E>` — when `X = <E>-<locale>`. Every delivery mixes results of ONE environment:
|
|
121
|
+
- `sungen delivery login` → base results + bare declared locales (`ja`); `dev`/`stg` suffixes are IGNORED with a hint (`→ sungen delivery login --env dev`)
|
|
122
|
+
- `sungen delivery login --env dev` → `dev` results as base + `dev-ja` sheets; everything else ignored
|
|
123
|
+
- `--env ja` (SUNGEN_ENV=<locale> convention) → that locale's run IS the base, labeled JA
|
|
124
|
+
The base sheet label comes from `base_locale:` (default **EN**). Undeclared env suffixes NEVER fabricate locale sheets — a project without `locales:` always gets one clean `Auto` sheet. Mobile per-OS suffixes (`android`, `ios.ja`, …) are exempt and keep the per-OS files; their Test Data column merges the after-OS tail overlay (`ios.ja` → `<name>.ja.yaml`), matching what the runtime merged.
|
|
89
125
|
|
|
90
126
|
---
|
|
91
127
|
|
|
@@ -94,6 +94,7 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
|
|
|
94
94
|
| `SG-W007` | Browser-alert handler step written AFTER an action — listener registers too late, the dialog auto-dismisses silently | Use the compound form `click [X] button and accept [OK] alert` (preferred), or move the alert step before the trigger |
|
|
95
95
|
| `SG-W008` | `@manual` scenario incomplete in either direction: (a) HAS Gherkin steps but NO `Then` — a dangling `When` is invalid Gherkin, comments can NOT substitute; (b) documentation-style (NO steps) but its comments lack numbered action lines and/or a labeled oracle line — prose paragraphs / rationale-only comments export EMPTY Steps + Expected cells | Two valid shapes only: complete the Gherkin with a `Then`, or write the FULL procedure in the `# Tester verifies:` block as NUMBERED lines — action steps first (`1. <action>`), then labeled `Observable:`/`Oracle:`/`Expected:` lines (the number is what fills Steps, the label is what fills Expected results) |
|
|
96
96
|
| `SG-W009` | test-data references `${QA_<NAME>}` (a SECRET env ref) but the name is declared in neither `.env.qa` nor `.env.qa.example` — the test throws "environment variable not set" the moment the key is used. (Only the `${QA_*}` shape is checked; any other `${...}` is literal data and never flagged.) | Add `QA_<NAME>=<value>` to `.env.qa` (gitignored) and list the NAME in `.env.qa.example` (the committed checklist). See `qa/TEST-DATA-GUIDE.md` |
|
|
97
|
+
| `SG-W010` | A row step's `[Ref]` looks POSITIONAL (`First…`/`Last…`/`Top…`/`nth`) but has no selectors.yaml entry — the label is display-only, so the step checks existence ANYWHERE in the table; an ordering/sort-stability rule would pass even when the row moved | Declare the row in selectors.yaml (a `type: locator` value RELATIVE to the table, e.g. `first contact row:` → `value: 'tbody tr:first-child'`) — the step then asserts `toContainText` on that exact row. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
97
98
|
|
|
98
99
|
### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
|
|
99
100
|
|
|
@@ -127,6 +127,14 @@ User see [Table] table match data:
|
|
|
127
127
|
|
|
128
128
|
Row scope: `see [Ref] row in [Table] table with {{v}}` enters scope. Subsequent `see [Col] column with {{v}}` checks cell in that row. Use `table match data:` for multi-row verification.
|
|
129
129
|
|
|
130
|
+
**[Ref] of a row step is a display label UNLESS declared in selectors.yaml.** Without an entry the step checks the value exists in ANY row — a name like `[First Contact Row]` does NOT position the row, and an ordering rule would pass even when the row moved (SG-W010 warns on positional-looking undeclared labels). To assert a POSITION, declare the row (a `type: locator` value RELATIVE to the table):
|
|
131
|
+
```yaml
|
|
132
|
+
first contact row:
|
|
133
|
+
type: locator
|
|
134
|
+
value: 'tbody tr:first-child'
|
|
135
|
+
```
|
|
136
|
+
→ compiles to `expect(table.locator('tbody tr:first-child')).toContainText(v)` — the exact row must hold the value — and still enters row scope for `[Col] column` checks.
|
|
137
|
+
|
|
130
138
|
### Database verification (optional Data Driver)
|
|
131
139
|
|
|
132
140
|
Read-only DB-state checks. **Prefer named queries** — SQL lives in `qa/screens/<screen>/database/queries.yaml` (reviewed once, parameterized). Invoke with the `@query:<name>` annotation; it binds the result rows to `{{name}}`, then assert with `expect`:
|
|
@@ -66,7 +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
|
+
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**. `VERIFICATION-OUT-OF-SCOPE (api|db)` → the unit uses `@api`/`@query`/`@requires:api|db` verification the project's recorded **test type** never opted into (E2E/UI-only per `capabilities.yaml`, and the viewpoint doesn't ask for it). During repair: **rewrite the oracle to be UI-observable** (or downgrade the deep check to an `(optional deep check: …)` note); only keep the API/DB verification if the project genuinely tests that layer — then record it (`verification: [ui, api]` / `sungen capability add`). Keep API/DB-in-E2E to the ≤20% band. Neither of these fails the gate.
|
|
70
70
|
|
|
71
71
|
## Discovery / fallback tree (when input is limited)
|
|
72
72
|
|
|
@@ -34,6 +34,14 @@ Take a screen/flow whose `selectors/*.yaml` and `.feature` files were authored a
|
|
|
34
34
|
1. `selectors/<feature>.yaml` — hardcoded `name`/`value` replaced with `{{var}}`
|
|
35
35
|
2. `test-data/<feature>.yaml` — base locale, complete with all new keys
|
|
36
36
|
3. `test-data/<feature>.<locale>.yaml` — overlay with only the keys that change
|
|
37
|
+
4. **`qa/capabilities.yaml` — DECLARE the locale** (required since 3.2.15): append the code to `locales:` and set `base_locale:` (language of the base test-data; tool default is `en`). Delivery/dashboard treat ONLY declared codes as locales — undeclared result-file suffixes are assumed to be ENVIRONMENTS (dev/staging) and excluded from locale sheets:
|
|
38
|
+
```yaml
|
|
39
|
+
platform: web
|
|
40
|
+
enabled: [web]
|
|
41
|
+
base_locale: en # or vi/ja — whatever the base test-data is written in
|
|
42
|
+
locales: [ja] # append every bootstrapped locale here
|
|
43
|
+
```
|
|
44
|
+
Env × locale combos use the compound `SUNGEN_ENV=<env>-<locale>` (e.g. `staging-ja`, overlay `<feature>.staging-ja.yaml`) — the locale segment LAST; delivery recognizes it when the tail matches a declared locale.
|
|
37
45
|
4. (Optional) `selectors/<feature>.yaml` Pages block updated when locale uses URL prefix or query param
|
|
38
46
|
|
|
39
47
|
After this skill finishes, `sungen run-test <name> --env <locale>` Just Works.
|
|
@@ -328,6 +328,7 @@ Security: [S1 – admin only]
|
|
|
328
328
|
Gate (`SG-W008`, both directions): a `@manual` with Gherkin steps but no `Then` fires — comments (even labeled `Oracle:` lines) can NOT substitute for the missing `Then`. A `@manual` with NO steps ALSO fires when its comments lack ≥1 numbered action line + ≥1 labeled `Observable:`/`Oracle:`/`Expected:` line — prose paragraphs and rationale-only comments export EMPTY Steps/Expected cells.
|
|
329
329
|
In shape A the observable/oracle lines MUST carry their **label** (`Observable:` / `Oracle:` / `Expected:`) — the delivery parser extracts Expected results **by label**; unlabeled prose all lands in the Steps column and the Expected cell exports EMPTY. And the numbered action steps MUST precede them — without the actions the tester has an oracle but no procedure.
|
|
330
330
|
**Oracle layer must match the unit** — a screen-unit manual defaults to a **UI oracle**: phrase the expected as what the tester SEES (on-screen message, list/table state after reload). If the business effect IS UI-observable, do NOT cite API responses or DB state as the oracle — "server rejects the request" is verified by *the error message on screen + no new row after reload*, not by reading the API response. Only when the effect has NO UI observation (mail content, stored hash, request count, response-body leak) may the oracle use another layer — then: (a) put the **layer label inside the line**: `Oracle: UI — …` / `Oracle: API — …` / `Oracle: DB — …` so QA knows the tooling needed from the deliverable alone; (b) tag `@manual:M2` so the capability planner recommends the api/db driver; (c) prefer the automation-ready `@requires:api`/`@query` form when the steps are expressible. A deep engineering check may ride along as `(optional deep check: …)` appended to the UI oracle — it must never REPLACE a UI-observable oracle.
|
|
331
|
+
**Respect the project's VERIFICATION SCOPE (the recorded test type).** Read `qa/capabilities.yaml` — the effective scope is `verification:` if set, else derived from `enabled` (`ui` always; `api`/`db` only if that driver is on). If the project is **E2E/UI-only** (no `api`/`db`) **and the test-viewpoint does not ask for API/DB verification**, do **NOT** author `@api`/`@query`/`@requires:api|db` verification — keep the oracle **UI-observable**, or leave the deep check as an `(optional deep check: …)` note. Author API/DB verification only when the scope (or an explicit viewpoint) calls for it, and even then keep the API/DB-in-E2E combination to a **≤20% band** of scenarios (the effects with no UI observation) — never the default. `sungen audit` reports `VERIFICATION-OUT-OF-SCOPE` when a unit uses API/DB verify the project never opted into.
|
|
331
332
|
Use this comment shape:
|
|
332
333
|
```gherkin
|
|
333
334
|
@high @manual
|
|
@@ -62,16 +62,25 @@ export class TestDataLoader {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Resolve a key to its RAW, uncoerced value (array/object/number kept as-is) — for binding
|
|
65
|
-
* DB query
|
|
66
|
-
* native type. get() stringifies for Gherkin text; raw()
|
|
67
|
-
*
|
|
65
|
+
* DB query / API request params, where type-strict sinks (MySQL numeric columns, MongoDB,
|
|
66
|
+
* a JSON request body) require the native type. get() stringifies for Gherkin text; raw()
|
|
67
|
+
* must not. A string value still has its embedded `{{ref}}` cross-references resolved (same
|
|
68
|
+
* as get()), so an override like `token={{login.body.token}}` or `id={{prefix}}-001` still
|
|
69
|
+
* interpolates — only the final scalar/array/object type is preserved uncoerced. Arrays and
|
|
70
|
+
* objects are returned as-is; refs embedded inside their elements are not interpolated (unlike
|
|
71
|
+
* get()'s JSON-text pass), since raw() callers bind structured values directly, not text.
|
|
72
|
+
* Throws on missing, mirroring get()'s guard.
|
|
68
73
|
*/
|
|
69
74
|
raw(key: string): any {
|
|
70
75
|
const value = this.resolve(key);
|
|
71
76
|
if (value === undefined || value === null) {
|
|
72
77
|
throw new Error(`Test data key not found: ${key}`);
|
|
73
78
|
}
|
|
74
|
-
if (typeof value === 'string')
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
const result = this.interpolate(value);
|
|
81
|
+
assertNoUnresolvedEnvRef(key, result);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
75
84
|
return value;
|
|
76
85
|
}
|
|
77
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.16-beta.1",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && mkdir -p dist/generators/test-generator/adapters/appium/templates/steps && cp -r src/generators/test-generator/adapters/appium/templates/*.hbs dist/generators/test-generator/adapters/appium/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/appium/templates/steps dist/generators/test-generator/adapters/appium/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
|
|
13
13
|
"build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
|
|
14
14
|
"dev": "tsx src/cli/index.ts",
|
|
15
|
-
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/exporter/delivery-prerun-overview.run.ts && tsx tests/exporter/manual-testcase-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/codegen/steps-base-not-compiled.run.ts && tsx tests/codegen/manual-oracle-gate.run.ts && tsx tests/codegen/locator-value-qualified-assertions.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts && tsx tests/runtime/env-secret-interpolation.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
|
|
15
|
+
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/delivery-matrix.run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/exporter/delivery-prerun-overview.run.ts && tsx tests/exporter/delivery-locale-env-discovery.run.ts && tsx tests/exporter/manual-testcase-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/codegen/steps-base-not-compiled.run.ts && tsx tests/codegen/manual-oracle-gate.run.ts && tsx tests/codegen/locator-value-qualified-assertions.run.ts && tsx tests/codegen/table-match-data-column-mapping.run.ts && tsx tests/codegen/param-binding-native-type.run.ts && tsx tests/codegen/table-row-scope-redeclaration.run.ts && tsx tests/codegen/table-row-label-resolution.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts && tsx tests/runtime/env-secret-interpolation.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
|
|
16
16
|
"test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
|
|
17
17
|
"prepublishOnly": "npm run build:dashboard && npm run build"
|
|
18
18
|
},
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@babel/types": "^7.28.5",
|
|
40
40
|
"@cucumber/gherkin": "^37.0.0",
|
|
41
41
|
"@cucumber/messages": "^31.0.0",
|
|
42
|
-
"@sungen/driver-data-factory": "3.2.
|
|
43
|
-
"@sungen/driver-ui": "3.2.
|
|
42
|
+
"@sungen/driver-data-factory": "3.2.16-beta.1",
|
|
43
|
+
"@sungen/driver-ui": "3.2.16-beta.1",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
45
|
"commander": "^14.0.2",
|
|
46
46
|
"dotenv": "^17.2.3",
|
|
@@ -13,6 +13,7 @@ import { parseSpecFile } from '../../exporters/spec-parser';
|
|
|
13
13
|
import { loadTestData } from '../../exporters/test-data-resolver';
|
|
14
14
|
import { loadSelectorKeyMap } from '../../exporters/selector-key-resolver';
|
|
15
15
|
import { loadPlaywrightReport, resolveResultsPath as resolveResultsPathShared } from '../../exporters/playwright-report-parser';
|
|
16
|
+
import { discoverLocaleVariants } from '../../exporters/locale-variants';
|
|
16
17
|
import { mergeFeatureAndSpec } from '../../exporters/scenario-merger';
|
|
17
18
|
import {
|
|
18
19
|
buildTestCaseRows,
|
|
@@ -24,6 +25,16 @@ import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet
|
|
|
24
25
|
import { loadApiCatalog } from '../../exporters/api-catalog-loader';
|
|
25
26
|
import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
|
|
26
27
|
import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
|
|
28
|
+
import { mapFilePath } from '../../exporters/matrix/map-loader';
|
|
29
|
+
import {
|
|
30
|
+
MatrixTargetPaths,
|
|
31
|
+
loadMatrixModel,
|
|
32
|
+
approveMatrix,
|
|
33
|
+
writeMatrixDeliverables,
|
|
34
|
+
hasErrors,
|
|
35
|
+
hasReview,
|
|
36
|
+
} from '../../exporters/matrix/export';
|
|
37
|
+
import { MatrixFinding } from '../../exporters/matrix/types';
|
|
27
38
|
|
|
28
39
|
const COLOR = {
|
|
29
40
|
reset: '\x1b[0m',
|
|
@@ -167,70 +178,11 @@ function resolveResultsPath(cwd: string, target: DeliveryTarget): string | null
|
|
|
167
178
|
return resolveResultsPathShared(cwd, generatedDir(cwd, target), target.featureBaseName);
|
|
168
179
|
}
|
|
169
180
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
* `<name>-test-result.<locale>.json` files found in the generated directory.
|
|
176
|
-
*
|
|
177
|
-
* `displayCode` is what we show in sheet names — `BASE_LOCALE.toUpperCase()`
|
|
178
|
-
* for base, the locale code uppercased otherwise (e.g. `EN`, `JA`, `EN-US`).
|
|
179
|
-
*/
|
|
180
|
-
interface LocaleVariant {
|
|
181
|
-
locale: string; // '' for base, 'en' / 'ja' / 'staging-ja' for variants
|
|
182
|
-
displayCode: string; // 'VI' / 'EN' / 'JA' — used in sheet name
|
|
183
|
-
resultsPath: string | null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Base locale assumed when no `SUNGEN_ENV` and no explicit override.
|
|
188
|
-
* Configurable later via `qa/.sungen-config.yaml` or similar — currently
|
|
189
|
-
* hardcoded since every existing sungen project uses Vietnamese as the
|
|
190
|
-
* source-of-truth `test-data/<feature>.yaml`.
|
|
191
|
-
*/
|
|
192
|
-
const DEFAULT_BASE_LOCALE = 'vi';
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Discover every locale variant available for a feature by scanning the
|
|
196
|
-
* generated directory for `<featureBaseName>-test-result*.json` files.
|
|
197
|
-
*
|
|
198
|
-
* Result always includes the base (locale = '') as the first entry — even if
|
|
199
|
-
* `<name>-test-result.json` doesn't exist, callers still need somewhere to
|
|
200
|
-
* fall back to (Pending status). Variants follow alphabetically by locale
|
|
201
|
-
* code, so the sheet order is deterministic across runs.
|
|
202
|
-
*/
|
|
203
|
-
function discoverLocaleVariants(cwd: string, target: DeliveryTarget): LocaleVariant[] {
|
|
204
|
-
const genDir = generatedDir(cwd, target);
|
|
205
|
-
const prefix = `${target.featureBaseName}-test-result`;
|
|
206
|
-
const variants: LocaleVariant[] = [];
|
|
207
|
-
|
|
208
|
-
// Base variant: per-target result file if present, else fall back to the global
|
|
209
|
-
// test-results/results.json (what playwright.config writes by default) via resolveResultsPath.
|
|
210
|
-
variants.push({
|
|
211
|
-
locale: '',
|
|
212
|
-
displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
|
|
213
|
-
resultsPath: resolveResultsPath(cwd, target),
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
if (fs.existsSync(genDir)) {
|
|
217
|
-
const localeFiles = fs.readdirSync(genDir)
|
|
218
|
-
.filter((f) => f.startsWith(`${prefix}.`) && f.endsWith('.json') && f !== `${prefix}.json`)
|
|
219
|
-
.sort();
|
|
220
|
-
for (const f of localeFiles) {
|
|
221
|
-
// Strip prefix + '.' on the left, '.json' on the right → locale code.
|
|
222
|
-
const locale = f.slice(prefix.length + 1, -'.json'.length);
|
|
223
|
-
if (!locale) continue;
|
|
224
|
-
variants.push({
|
|
225
|
-
locale,
|
|
226
|
-
displayCode: locale.toUpperCase(),
|
|
227
|
-
resultsPath: path.join(genDir, f),
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return variants;
|
|
233
|
-
}
|
|
181
|
+
// Locale/env variant discovery is SHARED with the dashboard snapshot builder —
|
|
182
|
+
// see exporters/locale-variants.ts for the classification rules (declared
|
|
183
|
+
// locales via qa/capabilities.yaml, env pairing, mobile OS exemption). The two
|
|
184
|
+
// surfaces previously hand-mirrored this logic and drifted into the
|
|
185
|
+
// fake-locale bug (any env-suffixed result file became a "locale" sheet).
|
|
234
186
|
|
|
235
187
|
function resolveTestDataPathForTarget(cwd: string, target: DeliveryTarget): string {
|
|
236
188
|
return path.join(qaDir(cwd, target), 'test-data', `${target.featureBaseName}.yaml`);
|
|
@@ -522,7 +474,17 @@ async function exportTarget(
|
|
|
522
474
|
return buildSummary(label, rows, path.relative(cwd, csvPath));
|
|
523
475
|
}
|
|
524
476
|
|
|
525
|
-
const variants = discoverLocaleVariants(
|
|
477
|
+
const { variants, ignored } = discoverLocaleVariants(
|
|
478
|
+
cwd, generatedDir(cwd, target), target.featureBaseName,
|
|
479
|
+
() => resolveResultsPath(cwd, target),
|
|
480
|
+
);
|
|
481
|
+
if (ignored.length > 0) {
|
|
482
|
+
const env = process.env.SUNGEN_ENV;
|
|
483
|
+
log(` ${COLOR.gray}Ignored result file(s) from other environments: ${ignored.join(', ')}${COLOR.reset}`);
|
|
484
|
+
for (const suffix of ignored) {
|
|
485
|
+
log(` ${COLOR.cyan}→ sungen delivery ${target.screen} --env ${suffix}${COLOR.reset}${env ? '' : ` ${COLOR.gray}(this run uses the base results only)${COLOR.reset}`}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
526
488
|
|
|
527
489
|
// Mobile: emit ONE deliverable per OS (csv + xlsx), each from its own
|
|
528
490
|
// `<feature>-test-result.<os>.json`. Android/iOS are SEPARATE platform runs,
|
|
@@ -542,7 +504,11 @@ async function exportTarget(
|
|
|
542
504
|
featureName: feature.featureName,
|
|
543
505
|
featurePath: resolvePlatformAppId(featurePath, variant.locale), // OS-precise app id
|
|
544
506
|
merged,
|
|
545
|
-
|
|
507
|
+
// Overlay = the after-OS tail (what SUNGEN_ENV was during that run):
|
|
508
|
+
// `android.ja` merged <name>.ja.yaml at runtime — the old code passed the
|
|
509
|
+
// full `android.ja` suffix, looked up a nonexistent overlay, and silently
|
|
510
|
+
// showed base-language values in the Test Data column.
|
|
511
|
+
testData: loadTestData(testDataFile, variant.overlay ?? null),
|
|
546
512
|
results: variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null,
|
|
547
513
|
env,
|
|
548
514
|
selectorKeyMap,
|
|
@@ -579,9 +545,11 @@ async function exportTarget(
|
|
|
579
545
|
const multiLocale = variants.length > 1;
|
|
580
546
|
|
|
581
547
|
for (const variant of variants) {
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
|
|
548
|
+
// Overlay parity with the RUNTIME: merge exactly what TestDataLoader merged
|
|
549
|
+
// during that run (the run's SUNGEN_ENV) — the base variant under --env dev
|
|
550
|
+
// merges <feature>.dev.yaml, a ja variant merges <feature>.ja.yaml, plain
|
|
551
|
+
// base merges nothing.
|
|
552
|
+
const variantTestData = loadTestData(testDataFile, variant.overlay ?? null);
|
|
585
553
|
const variantResults = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
|
|
586
554
|
const variantRows = buildTestCaseRows({
|
|
587
555
|
screen: label,
|
|
@@ -639,6 +607,114 @@ async function exportTarget(
|
|
|
639
607
|
}
|
|
640
608
|
}
|
|
641
609
|
|
|
610
|
+
// ----------------------------------------------------------------------------
|
|
611
|
+
// Matrix export (Delivery v2 — the default; --legacy keeps the classic workbook)
|
|
612
|
+
// ----------------------------------------------------------------------------
|
|
613
|
+
|
|
614
|
+
function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths {
|
|
615
|
+
const base = qaDir(cwd, target);
|
|
616
|
+
const genBase = generatedDir(cwd, target);
|
|
617
|
+
return {
|
|
618
|
+
cwd,
|
|
619
|
+
unit: target.featureBaseName,
|
|
620
|
+
label: target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName,
|
|
621
|
+
featureFile: path.join(base, 'features', `${target.featureBaseName}.feature`),
|
|
622
|
+
testDataFile: resolveTestDataPathForTarget(cwd, target),
|
|
623
|
+
specFile: path.join(genBase, `${target.featureBaseName}.spec.ts`),
|
|
624
|
+
resultsPath: resolveResultsPath(cwd, target),
|
|
625
|
+
mapFile: mapFilePath(base, target.featureBaseName),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function printFindings(findings: MatrixFinding[]): void {
|
|
630
|
+
const icon = { error: `${COLOR.red}✗`, review: `${COLOR.yellow}⚠`, warning: `${COLOR.gray}·` } as const;
|
|
631
|
+
for (const f of findings) {
|
|
632
|
+
log(` ${icon[f.severity]} [Gate ${f.gate}]${COLOR.reset} ${f.ref ? `${f.ref}: ` : ''}${f.message}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
interface MatrixRunSummary {
|
|
637
|
+
label: string;
|
|
638
|
+
items: number;
|
|
639
|
+
variants: number;
|
|
640
|
+
passed: number;
|
|
641
|
+
failed: number;
|
|
642
|
+
file: string;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Matrix-mode export of one target. Returns null when blocked (missing map,
|
|
647
|
+
* gate errors, or unapproved groups without --preview) — the caller aggregates
|
|
648
|
+
* a non-zero exit.
|
|
649
|
+
*/
|
|
650
|
+
async function exportMatrixTarget(
|
|
651
|
+
cwd: string,
|
|
652
|
+
target: DeliveryTarget,
|
|
653
|
+
opts: { check?: boolean; preview?: boolean },
|
|
654
|
+
): Promise<MatrixRunSummary | null> {
|
|
655
|
+
const paths = matrixPathsFor(cwd, target);
|
|
656
|
+
const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
|
|
657
|
+
|
|
658
|
+
if (mapMissing) {
|
|
659
|
+
log(`${COLOR.yellow}${paths.label}: no delivery map${COLOR.reset} (${path.relative(cwd, paths.mapFile)})`);
|
|
660
|
+
log(` The matrix needs the reviewed grouping (target / intent / oracle per delivery item).`);
|
|
661
|
+
log(` ${COLOR.cyan}→ run /sungen:delivery ${target.screen} so the map is proposed for your review${COLOR.reset}`);
|
|
662
|
+
log(` ${COLOR.cyan}→ or export the classic per-scenario workbook: sungen delivery ${target.screen} --legacy${COLOR.reset}`);
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if (mapErrors.length > 0 || !model) {
|
|
666
|
+
log(`${COLOR.red}${paths.label}: delivery map is invalid${COLOR.reset}`);
|
|
667
|
+
for (const e of mapErrors) log(` - ${e}`);
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
log(`${COLOR.bold}${paths.label}${COLOR.reset} — ${model.manifest.itemCount} delivery item(s), ${model.manifest.variantCount} coverage variant(s), state: ${model.manifest.approvalState}`);
|
|
672
|
+
printFindings(model.findings);
|
|
673
|
+
|
|
674
|
+
if (opts.check) {
|
|
675
|
+
// Gates-only run: the summary IS the output; exit code reflects errors.
|
|
676
|
+
return hasErrors(model.findings)
|
|
677
|
+
? null
|
|
678
|
+
: { label: paths.label, items: model.manifest.itemCount, variants: model.manifest.variantCount, passed: 0, failed: 0, file: '(check only)' };
|
|
679
|
+
}
|
|
680
|
+
if (hasErrors(model.findings)) {
|
|
681
|
+
log(` ${COLOR.red}blocked:${COLOR.reset} fix the Gate errors above, then re-run.`);
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
if (hasReview(model.findings) && !opts.preview) {
|
|
685
|
+
log(` ${COLOR.yellow}not approved for official delivery.${COLOR.reset}`);
|
|
686
|
+
log(` ${COLOR.cyan}→ approve the reviewed grouping: sungen delivery ${target.screen} --approve${COLOR.reset}`);
|
|
687
|
+
log(` ${COLOR.cyan}→ or render a watermarked draft: sungen delivery ${target.screen} --preview${COLOR.reset}`);
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const { csvPath, xlsxPath } = await writeMatrixDeliverables(paths, model);
|
|
692
|
+
const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
|
|
693
|
+
const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
|
|
694
|
+
log(` ${COLOR.green}→ ${path.relative(cwd, xlsxPath)}${COLOR.reset} ${COLOR.gray}(+ ${path.relative(cwd, csvPath)})${COLOR.reset}`);
|
|
695
|
+
return {
|
|
696
|
+
label: paths.label,
|
|
697
|
+
items: model.manifest.itemCount,
|
|
698
|
+
variants: model.manifest.variantCount,
|
|
699
|
+
passed,
|
|
700
|
+
failed,
|
|
701
|
+
file: path.relative(cwd, xlsxPath),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function printMatrixSummaryTable(summaries: MatrixRunSummary[]): void {
|
|
706
|
+
log(`\n${COLOR.bold}Delivery matrix export complete${COLOR.reset}\n`);
|
|
707
|
+
const colWidth = Math.max(20, ...summaries.map((s) => s.label.length)) + 1;
|
|
708
|
+
log(` ${'Feature'.padEnd(colWidth)}Items Variants Passed Failed File`);
|
|
709
|
+
log(' ' + '-'.repeat(colWidth + 50));
|
|
710
|
+
for (const s of summaries) {
|
|
711
|
+
log(
|
|
712
|
+
` ${s.label.padEnd(colWidth)}${String(s.items).padStart(5)} ${String(s.variants).padStart(8)} ${String(s.passed).padStart(6)} ${String(s.failed).padStart(6)} ${s.file}`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
log(`\n${COLOR.gray}Items = review rows (one target + intent + oracle family). Variants = execution units — use variant counts for progress.${COLOR.reset}\n`);
|
|
716
|
+
}
|
|
717
|
+
|
|
642
718
|
// ----------------------------------------------------------------------------
|
|
643
719
|
// Reporter
|
|
644
720
|
// ----------------------------------------------------------------------------
|
|
@@ -709,12 +785,24 @@ function printSummaryTable(summaries: ScreenSummary[]): void {
|
|
|
709
785
|
export function registerDeliveryCommand(program: Command): void {
|
|
710
786
|
program
|
|
711
787
|
.command('delivery')
|
|
712
|
-
.description('Export Gherkin +
|
|
788
|
+
.description('Export the Test Case & Coverage Matrix (Gherkin + results). --legacy keeps the classic per-scenario workbook.')
|
|
713
789
|
.argument('[names...]', 'Specific screen or flow names. Omit to process all.')
|
|
714
790
|
.option('--skip-preflight', 'Skip pre-flight checks (not recommended)')
|
|
715
791
|
.option('--continue-on-missing', 'Skip targets with blocking misses instead of aborting')
|
|
716
|
-
.
|
|
792
|
+
.option('--env <env>', 'Environment/locale of the run to deliver (sets SUNGEN_ENV; e.g. dev, staging, ja, dev-ja)')
|
|
793
|
+
.option('--legacy', 'Classic per-scenario-steps workbook (BM-2-901-13 layout)')
|
|
794
|
+
.option('--full', 'Alias of --legacy')
|
|
795
|
+
.option('--check', 'Matrix gates only — validate the delivery map, write nothing')
|
|
796
|
+
.option('--approve [ids]', 'Approve proposed groups (all, or a comma-separated id list) + stamp fingerprints')
|
|
797
|
+
.option('--preview', 'Render the matrix despite review-required findings (DRAFT watermark)')
|
|
798
|
+
.action(async (names: string[], options: {
|
|
799
|
+
skipPreflight?: boolean; continueOnMissing?: boolean; env?: string;
|
|
800
|
+
legacy?: boolean; full?: boolean; check?: boolean; approve?: boolean | string; preview?: boolean;
|
|
801
|
+
}) => {
|
|
717
802
|
try {
|
|
803
|
+
// Same effect as running with SUNGEN_ENV=<env> — result-file resolution,
|
|
804
|
+
// locale pairing, and test-data overlays all key off this one string.
|
|
805
|
+
if (options.env) process.env.SUNGEN_ENV = options.env;
|
|
718
806
|
const cwd = process.cwd();
|
|
719
807
|
|
|
720
808
|
// 1. Scope detection — each positional name expands into one target
|
|
@@ -766,6 +854,46 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
766
854
|
}
|
|
767
855
|
|
|
768
856
|
// 3. Export
|
|
857
|
+
const legacy = options.legacy || options.full;
|
|
858
|
+
if (!legacy) {
|
|
859
|
+
// Matrix mode (the default). --approve is its own sub-action.
|
|
860
|
+
if (options.approve !== undefined) {
|
|
861
|
+
const ids = typeof options.approve === 'string'
|
|
862
|
+
? options.approve.split(',').map((s) => s.trim()).filter(Boolean)
|
|
863
|
+
: undefined;
|
|
864
|
+
let blocked = false;
|
|
865
|
+
for (const target of toExport) {
|
|
866
|
+
const paths = matrixPathsFor(cwd, target);
|
|
867
|
+
try {
|
|
868
|
+
const { findings, approved } = approveMatrix(paths, ids);
|
|
869
|
+
if (approved.length === 0) {
|
|
870
|
+
log(`${COLOR.red}${paths.label}: approval blocked by Gate errors${COLOR.reset}`);
|
|
871
|
+
printFindings(findings);
|
|
872
|
+
blocked = true;
|
|
873
|
+
} else {
|
|
874
|
+
log(`${COLOR.green}${paths.label}: approved ${approved.length} group(s)${COLOR.reset} — fingerprints stamped in ${path.relative(cwd, paths.mapFile)}`);
|
|
875
|
+
}
|
|
876
|
+
} catch (e) {
|
|
877
|
+
log(`${COLOR.red}${paths.label}:${COLOR.reset} ${e instanceof Error ? e.message : e}`);
|
|
878
|
+
blocked = true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (blocked) process.exit(1);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const matrixSummaries: MatrixRunSummary[] = [];
|
|
886
|
+
let failedTargets = 0;
|
|
887
|
+
for (const target of toExport) {
|
|
888
|
+
const s = await exportMatrixTarget(cwd, target, { check: options.check, preview: options.preview });
|
|
889
|
+
if (s) matrixSummaries.push(s);
|
|
890
|
+
else failedTargets++;
|
|
891
|
+
}
|
|
892
|
+
if (matrixSummaries.length > 0 && !options.check) printMatrixSummaryTable(matrixSummaries);
|
|
893
|
+
if (failedTargets > 0) process.exit(1);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
769
897
|
const env = getEnvironment(cwd);
|
|
770
898
|
const summaries: ScreenSummary[] = [];
|
|
771
899
|
for (const target of toExport) {
|