@sun-asterisk/sungen 3.2.17 → 3.2.18
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/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +23 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +209 -16
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +8 -1
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/exporters/api-environment.d.ts +11 -0
- package/dist/exporters/api-environment.d.ts.map +1 -0
- package/dist/exporters/api-environment.js +129 -0
- package/dist/exporters/api-environment.js.map +1 -0
- package/dist/exporters/feature-parser.d.ts +16 -1
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +21 -2
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +70 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -0
- package/dist/exporters/matrix/build.js +516 -0
- package/dist/exporters/matrix/build.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +50 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -0
- package/dist/exporters/matrix/export.js +152 -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 +367 -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 +193 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -0
- package/dist/exporters/matrix/render-csv.d.ts +9 -0
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
- package/dist/exporters/matrix/render-csv.js +92 -0
- package/dist/exporters/matrix/render-csv.js.map +1 -0
- package/dist/exporters/matrix/render-xlsx.d.ts +41 -0
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
- package/dist/exporters/matrix/render-xlsx.js +430 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -0
- package/dist/exporters/matrix/types.d.ts +199 -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/matrix/wording.d.ts +61 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -0
- package/dist/exporters/matrix/wording.js +221 -0
- package/dist/exporters/matrix/wording.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +2 -1
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +2 -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/harness/audit.d.ts +10 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +98 -16
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/blindspot.d.ts.map +1 -1
- package/dist/harness/blindspot.js +2 -1
- package/dist/harness/blindspot.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +17 -4
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/feedback.d.ts.map +1 -1
- package/dist/harness/feedback.js +3 -2
- package/dist/harness/feedback.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +2 -1
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +3 -2
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/intent.d.ts.map +1 -1
- package/dist/harness/intent.js +2 -1
- package/dist/harness/intent.js.map +1 -1
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +3 -2
- package/dist/harness/journey.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 +4 -3
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -3
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +2 -1
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/read-text.d.ts +22 -0
- package/dist/harness/read-text.d.ts.map +1 -0
- package/dist/harness/read-text.js +64 -0
- package/dist/harness/read-text.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +3 -2
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts +13 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +63 -20
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +5 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +17 -7
- package/dist/harness/spec-coverage.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/viewpoint-baseline.d.ts +49 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -0
- package/dist/harness/viewpoint-baseline.js +141 -0
- package/dist/harness/viewpoint-baseline.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/orchestrator/assets-drift.d.ts +24 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -0
- package/dist/orchestrator/assets-drift.js +80 -0
- package/dist/orchestrator/assets-drift.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +24 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +203 -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 +102 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +23 -1
- package/src/cli/commands/delivery.ts +240 -18
- package/src/cli/index.ts +6 -0
- package/src/dashboard/snapshot-builder.ts +8 -1
- package/src/dashboard/templates/index.html +1 -1
- package/src/exporters/api-environment.ts +97 -0
- package/src/exporters/feature-parser.ts +21 -2
- package/src/exporters/matrix/build.ts +551 -0
- package/src/exporters/matrix/export.ts +149 -0
- package/src/exporters/matrix/fingerprint.ts +49 -0
- package/src/exporters/matrix/gates.ts +400 -0
- package/src/exporters/matrix/map-loader.ts +163 -0
- package/src/exporters/matrix/render-csv.ts +94 -0
- package/src/exporters/matrix/render-xlsx.ts +420 -0
- package/src/exporters/matrix/types.ts +226 -0
- package/src/exporters/matrix/wording.ts +221 -0
- package/src/exporters/scenario-merger.ts +2 -1
- package/src/exporters/spec-parser.ts +2 -1
- package/src/exporters/xlsx-report-builder.ts +3 -2
- package/src/harness/audit.ts +104 -18
- package/src/harness/blindspot.ts +2 -1
- package/src/harness/capability-plan.ts +17 -4
- package/src/harness/feedback.ts +3 -2
- package/src/harness/flow-check.ts +2 -1
- package/src/harness/flow-plan.ts +3 -2
- package/src/harness/intent.ts +2 -1
- package/src/harness/journey.ts +3 -2
- package/src/harness/ledger.ts +3 -2
- package/src/harness/manifest.ts +4 -3
- package/src/harness/parse.ts +17 -3
- package/src/harness/quality-gates.ts +2 -1
- package/src/harness/read-text.ts +28 -0
- package/src/harness/script-check.ts +3 -2
- package/src/harness/sensors.ts +55 -8
- package/src/harness/spec-coverage.ts +22 -7
- package/src/harness/trace.ts +4 -3
- package/src/harness/viewpoint-baseline.ts +128 -0
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/orchestrator/assets-drift.ts +64 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +24 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +203 -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 +102 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
|
@@ -295,6 +295,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
295
295
|
|
|
296
296
|
| Tag | Effect |
|
|
297
297
|
|---|---|
|
|
298
|
+
| `@spec:<id>` | **Traceability**: this scenario proves requirement `<id>` from `requirements/spec.md` (`@spec:FR-003`). Repeatable — a scenario may prove several. Read by `sungen audit` (requirement coverage) and by delivery (the Coverage sheet's requirement table). Without it the link is only inferred from wording and cannot survive an edit. |
|
|
298
299
|
| `@manual` | Skip in generation |
|
|
299
300
|
| `@auth:role` | Use auth storage state for role |
|
|
300
301
|
| `@no-auth` | Disable inherited auth |
|
|
@@ -4,6 +4,28 @@ description: 'Use when create-test needs to translate spec.md/Figma/UI into .fea
|
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
## Traceability — tag the requirement each scenario proves
|
|
9
|
+
|
|
10
|
+
Every requirement id in `requirements/spec.md` (`FR-`/`TR-`/`NFR-`) must be reachable from a
|
|
11
|
+
scenario, and the link must be **written down, not inferred**:
|
|
12
|
+
|
|
13
|
+
```gherkin
|
|
14
|
+
@high @spec:FR-003
|
|
15
|
+
Scenario: VP-VAL-002 An email in an invalid format shows the format error
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Put `@spec:<id>` on the scenario that actually proves the requirement; repeat the tag when one
|
|
19
|
+
scenario proves several (`@spec:FR-003 @spec:FR-004`).
|
|
20
|
+
- One requirement may be proved by several scenarios — tag each of them.
|
|
21
|
+
- A requirement you deliberately do not cover here (performance budget, i18n suite, server-side
|
|
22
|
+
concern) needs no tag; record it later in the delivery map's `requirements:` section with a
|
|
23
|
+
status and reason.
|
|
24
|
+
- Why it matters: `sungen audit` also counts a requirement as covered when its keywords merely
|
|
25
|
+
resemble a scenario title, and reports `SPEC-TRACE-IMPLICIT` for those. Delivery's requirement
|
|
26
|
+
table follows **only** the explicit tag — an untagged requirement reads as an uncovered gap in
|
|
27
|
+
the customer-facing coverage sheet even when a scenario does prove it.
|
|
28
|
+
|
|
7
29
|
## ⚠️ Gotchas — read before generating
|
|
8
30
|
|
|
9
31
|
- **Field-level test-data follows the Data Factory standard — don't hand-invent values.** When the unit has input fields, the field values (valid / boundary / invalid + error codes) come from the **`sungen-data-factory`** catalog via a field-map + `sungen data gen` (create-test step 5.3), not free-form guessing. Weave the standardized boundary/invalid sets into `@cases` with `CHK-*` trace. This skill still owns scenario structure, viewpoints, and non-field oracles.
|
|
@@ -318,6 +340,7 @@ Security: [S1 – admin only]
|
|
|
318
340
|
`sungen audit` enforces these. Generate compliant output up front:
|
|
319
341
|
|
|
320
342
|
1. **Taxonomy-match** (`VP-TAXONOMY-MISMATCH`, gate-FAIL) — when `test-viewpoint.md` declares its own viewpoint IDs (e.g. `VP0`, `VP1`, … `VP12`, `MS-HP-001`, `MS-EH-001`), **reuse those IDs verbatim as the scenario codes**. Do NOT invent a generic `VP-UI / VP-LOGIC / VP-VAL` scheme — that breaks the coverage matrix. Only fall back to `VP-<CATEGORY>-<NNN>` when the viewpoint file declares no IDs.
|
|
343
|
+
- **Match the scenarios to the file — never the file to the scenarios.** A filled `test-viewpoint.md` is an input; do not rewrite its declarations to fit what you generated. That is not compliance, it is moving the yardstick: `traceability` + `atomicLedger` then read 100% by construction and a dropped viewpoint stops being missing from anything. Disagree with the taxonomy → propose the diff and ask. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and excludes both axes until a human accepts the change (`sungen audit --screen <name> --accept-viewpoint`).
|
|
321
344
|
2. **Spec-coverage triggers** (`TRIGGER-UNCOVERED`, gate-FAIL) — the Validation-Rules table lists a **trigger** per constraint (e.g. `blur, submit`). Generate one scenario **per (constraint × trigger)** — a `format` rule validating *on blur AND on submit* needs BOTH a blur scenario (`press Tab`) and a submit scenario (`click [Submit]` / `press Enter`). Never collapse the trigger × input matrix to one representative case.
|
|
322
345
|
3. **Claim-Proof** (`CLAIM-UNPROVEN`) — a title claiming `all`/`only`/`every`/`single`/`correct`/`same`/`changes`/`hidden`/`cleared`/`restored`/`independent`/`sanitized`/`announces` MUST have the matching assertion (`see all …`, count, `remember`+compare, `is hidden`, return-and-assert-empty, etc.). If the title promises it, the steps must prove it.
|
|
323
346
|
- **Negative / absence claims** (`does not` / `no` / `never` / `prevents` / `không` / `chưa` — any language; `no-side-effect/no-duplicate`, `negative-claim/absence`): the `Then` must **differ** between the claim holding and not holding. A terminal `see [X] page` that looks identical whether or not the bad thing happened proves nothing. For a side-effect that should NOT repeat (re-submit on back, re-charge, duplicate order, resend OTP), assert the **count is unchanged** (`User see [Records] table with {{one}}` / `row with {{count}}`); if it's not UI-observable, mark `@manual` with a request-count oracle (shape below). This is general — it covers any side-effect, not a fixed verb list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.18",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -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.18",
|
|
43
|
+
"@sungen/driver-ui": "3.2.18",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
45
|
"commander": "^14.0.2",
|
|
46
46
|
"dotenv": "^17.2.3",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
-
import { runAudit, AuditReport } from '../../harness/audit';
|
|
4
|
+
import { runAudit, AuditReport, catalogIdFromScreenDir } from '../../harness/audit';
|
|
5
5
|
import { reportSlug } from '../../harness/unit-paths';
|
|
6
|
+
import { acceptViewpointBaseline } from '../../harness/viewpoint-baseline';
|
|
6
7
|
|
|
7
8
|
function findScreenDir(name: string): string | null {
|
|
8
9
|
// `name` may be a bare screen/flow/area, or an api unit id (`api/<area>`, `api/flows/<flow>`).
|
|
@@ -78,7 +79,12 @@ function render(r: AuditReport): void {
|
|
|
78
79
|
if (r.calibration) {
|
|
79
80
|
const ax = Object.entries(r.calibration.axes).map(([k, v]) => `${k}=${(v * 100).toFixed(0)}%`).join(' · ');
|
|
80
81
|
L(` ⑨ Calibration — ${ax}`);
|
|
82
|
+
const na = r.calibration.notApplicable ?? [];
|
|
83
|
+
if (na.length) L(` not scored (no evidence in this unit): ${na.join(', ')}`);
|
|
81
84
|
L(` weakest: ${r.calibration.weakest.axis} ${(r.calibration.weakest.value * 100).toFixed(0)}%${r.calibration.inflated ? ' ⚠ SCORE-INFLATED-BY-BREADTH' : ''}`);
|
|
85
|
+
if (r.calibration.cappedAt !== undefined) {
|
|
86
|
+
L(` ⚠ score held at ${r.calibration.cappedAt.toFixed(1)} by the weakest axis — fix it to lift the number`);
|
|
87
|
+
}
|
|
82
88
|
L('');
|
|
83
89
|
}
|
|
84
90
|
L(' ── Findings (Repair targets) ──');
|
|
@@ -95,6 +101,7 @@ export function registerAuditCommand(program: Command): void {
|
|
|
95
101
|
.option('--api <name>', 'API-first area or api flow to audit (e.g. orders, flows/signup)')
|
|
96
102
|
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
97
103
|
.option('--json', 'Output the raw JSON report')
|
|
104
|
+
.option('--accept-viewpoint', 'Accept the current test-viewpoint.md as the baseline the suite is measured against')
|
|
98
105
|
.action((options) => {
|
|
99
106
|
try {
|
|
100
107
|
const name = options.screen || options.api || options.area;
|
|
@@ -104,6 +111,21 @@ export function registerAuditCommand(program: Command): void {
|
|
|
104
111
|
|
|
105
112
|
const report = runAudit(dir, name);
|
|
106
113
|
|
|
114
|
+
// The baseline is only ever written HERE, never by the measurement itself:
|
|
115
|
+
// a first sighting has nothing to compare against, and any later change is
|
|
116
|
+
// a human decision (`--accept-viewpoint`) — see harness/viewpoint-baseline.ts.
|
|
117
|
+
const unitId = catalogIdFromScreenDir(dir);
|
|
118
|
+
const vb = report.viewpointBaseline;
|
|
119
|
+
if (vb.status === 'new' || (options.acceptViewpoint && vb.status === 'changed')) {
|
|
120
|
+
acceptViewpointBaseline(process.cwd(), unitId, { hash: vb.hash, ids: vb.ids });
|
|
121
|
+
if (options.acceptViewpoint && !options.json) {
|
|
122
|
+
console.log(`\n ✓ test-viewpoint.md accepted as the baseline for ${name} (${vb.ids.length} viewpoint id(s)).`);
|
|
123
|
+
console.log(' Re-run the audit to score atomicLedger + traceability against it.\n');
|
|
124
|
+
}
|
|
125
|
+
} else if (options.acceptViewpoint && !options.json) {
|
|
126
|
+
console.log(`\n ○ nothing to accept for ${name} — the viewpoint baseline is ${vb.status}.\n`);
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
// Persist report under .sungen/reports/ (flat slug: api flow `flows/x` → `flows-x-audit.json`).
|
|
108
130
|
const outDir = path.join(process.cwd(), '.sungen', 'reports');
|
|
109
131
|
fs.mkdirSync(outDir, { recursive: true });
|
|
@@ -23,8 +23,20 @@ import {
|
|
|
23
23
|
} from '../../exporters/csv-exporter';
|
|
24
24
|
import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
|
|
25
25
|
import { loadApiCatalog } from '../../exporters/api-catalog-loader';
|
|
26
|
+
import { resolveApiEnvironment } from '../../exporters/api-environment';
|
|
26
27
|
import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
|
|
27
28
|
import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
|
|
29
|
+
import { mapFilePath } from '../../exporters/matrix/map-loader';
|
|
30
|
+
import {
|
|
31
|
+
MatrixTargetPaths,
|
|
32
|
+
loadMatrixModel,
|
|
33
|
+
approveMatrix,
|
|
34
|
+
writeMatrixDeliverables,
|
|
35
|
+
MatrixFormat,
|
|
36
|
+
hasErrors,
|
|
37
|
+
hasReview,
|
|
38
|
+
} from '../../exporters/matrix/export';
|
|
39
|
+
import { MatrixFinding } from '../../exporters/matrix/types';
|
|
28
40
|
|
|
29
41
|
const COLOR = {
|
|
30
42
|
reset: '\x1b[0m',
|
|
@@ -421,6 +433,15 @@ async function exportTarget(
|
|
|
421
433
|
const apiCatalog = loadApiCatalog(cwd, target.kind, target.screen);
|
|
422
434
|
const hasApiCatalog = target.kind === 'api' && Object.keys(apiCatalog).length > 0;
|
|
423
435
|
|
|
436
|
+
// API units call the datasource base_url (datasources.yaml + .env.qa), NOT the web
|
|
437
|
+
// baseURL from playwright.config.ts — the two hosts routinely differ, and an
|
|
438
|
+
// API-only project has no web baseURL at all. Resolve the real environment for the
|
|
439
|
+
// Test Environment cell; on any gap (no datasources.yaml / no base_url) keep the
|
|
440
|
+
// generic env unchanged. Shared with the dashboard so both surfaces agree.
|
|
441
|
+
const unitEnv = target.kind === 'api'
|
|
442
|
+
? { ...env, ...(resolveApiEnvironment(cwd, apiCatalog) ?? {}) }
|
|
443
|
+
: env;
|
|
444
|
+
|
|
424
445
|
// Decide between single-locale and aggregated multi-locale flows.
|
|
425
446
|
// • SUNGEN_ENV set → single locale (existing behaviour, no change)
|
|
426
447
|
// • SUNGEN_ENV unset → discover every *-test-result*.json variant.
|
|
@@ -440,7 +461,7 @@ async function exportTarget(
|
|
|
440
461
|
merged,
|
|
441
462
|
testData: loadTestData(testDataFile),
|
|
442
463
|
results: resolveResultsPath(cwd, target) ? loadPlaywrightReport(resolveResultsPath(cwd, target)!) : null,
|
|
443
|
-
env,
|
|
464
|
+
env: unitEnv,
|
|
444
465
|
selectorKeyMap,
|
|
445
466
|
kind: target.kind,
|
|
446
467
|
apiCatalog,
|
|
@@ -500,7 +521,7 @@ async function exportTarget(
|
|
|
500
521
|
// showed base-language values in the Test Data column.
|
|
501
522
|
testData: loadTestData(testDataFile, variant.overlay ?? null),
|
|
502
523
|
results: variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null,
|
|
503
|
-
env,
|
|
524
|
+
env: unitEnv,
|
|
504
525
|
selectorKeyMap,
|
|
505
526
|
kind: target.kind,
|
|
506
527
|
apiCatalog,
|
|
@@ -551,7 +572,7 @@ async function exportTarget(
|
|
|
551
572
|
merged,
|
|
552
573
|
testData: variantTestData,
|
|
553
574
|
results: variantResults,
|
|
554
|
-
env,
|
|
575
|
+
env: unitEnv,
|
|
555
576
|
selectorKeyMap,
|
|
556
577
|
kind: target.kind,
|
|
557
578
|
apiCatalog,
|
|
@@ -597,6 +618,129 @@ async function exportTarget(
|
|
|
597
618
|
}
|
|
598
619
|
}
|
|
599
620
|
|
|
621
|
+
// ----------------------------------------------------------------------------
|
|
622
|
+
// Matrix export (Delivery v2 — the default; --legacy keeps the classic workbook)
|
|
623
|
+
// ----------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths {
|
|
626
|
+
const base = qaDir(cwd, target);
|
|
627
|
+
const genBase = generatedDir(cwd, target);
|
|
628
|
+
return {
|
|
629
|
+
cwd,
|
|
630
|
+
unit: target.featureBaseName,
|
|
631
|
+
label: target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName,
|
|
632
|
+
featureFile: path.join(base, 'features', `${target.featureBaseName}.feature`),
|
|
633
|
+
testDataFile: resolveTestDataPathForTarget(cwd, target),
|
|
634
|
+
specFile: path.join(genBase, `${target.featureBaseName}.spec.ts`),
|
|
635
|
+
specMdFile: path.join(base, 'requirements', 'spec.md'),
|
|
636
|
+
resultsPath: resolveResultsPath(cwd, target),
|
|
637
|
+
mapFile: mapFilePath(base, target.featureBaseName),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function printFindings(findings: MatrixFinding[]): void {
|
|
642
|
+
const icon = { error: `${COLOR.red}✗`, review: `${COLOR.yellow}⚠`, warning: `${COLOR.gray}·` } as const;
|
|
643
|
+
for (const f of findings) {
|
|
644
|
+
log(` ${icon[f.severity]} [Gate ${f.gate}]${COLOR.reset} ${f.ref ? `${f.ref}: ` : ''}${f.message}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
interface MatrixRunSummary {
|
|
649
|
+
label: string;
|
|
650
|
+
items: number;
|
|
651
|
+
variants: number;
|
|
652
|
+
passed: number;
|
|
653
|
+
failed: number;
|
|
654
|
+
file: string;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Matrix-mode export of one target. Returns null when blocked (missing map,
|
|
659
|
+
* gate errors, or unapproved groups without --preview) — the caller aggregates
|
|
660
|
+
* a non-zero exit.
|
|
661
|
+
*/
|
|
662
|
+
async function exportMatrixTarget(
|
|
663
|
+
cwd: string,
|
|
664
|
+
target: DeliveryTarget,
|
|
665
|
+
opts: { check?: boolean; preview?: boolean; format?: MatrixFormat },
|
|
666
|
+
): Promise<MatrixRunSummary | null> {
|
|
667
|
+
const paths = matrixPathsFor(cwd, target);
|
|
668
|
+
const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
|
|
669
|
+
|
|
670
|
+
if (mapMissing) {
|
|
671
|
+
log(`${COLOR.yellow}${paths.label}: no delivery map${COLOR.reset} (${path.relative(cwd, paths.mapFile)})`);
|
|
672
|
+
log(` The matrix needs the reviewed grouping (target / intent / oracle per delivery item).`);
|
|
673
|
+
log(` ${COLOR.cyan}→ run /sungen:delivery ${target.screen} so the map is proposed for your review${COLOR.reset}`);
|
|
674
|
+
log(` ${COLOR.cyan}→ or export the classic per-scenario workbook: sungen delivery ${target.screen} --legacy${COLOR.reset}`);
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
if (mapErrors.length > 0 || !model) {
|
|
678
|
+
log(`${COLOR.red}${paths.label}: delivery map is invalid${COLOR.reset}`);
|
|
679
|
+
for (const e of mapErrors) log(` - ${e}`);
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
log(`${COLOR.bold}${paths.label}${COLOR.reset} — ${model.manifest.itemCount} delivery item(s), ${model.manifest.variantCount} coverage variant(s), state: ${model.manifest.approvalState}`);
|
|
684
|
+
printFindings(model.findings);
|
|
685
|
+
|
|
686
|
+
if (opts.check) {
|
|
687
|
+
// Gates-only run: the summary IS the output; exit code reflects errors.
|
|
688
|
+
return hasErrors(model.findings)
|
|
689
|
+
? null
|
|
690
|
+
: { label: paths.label, items: model.manifest.itemCount, variants: model.manifest.variantCount, passed: 0, failed: 0, file: '(check only)' };
|
|
691
|
+
}
|
|
692
|
+
if (hasErrors(model.findings)) {
|
|
693
|
+
log(` ${COLOR.red}blocked:${COLOR.reset} fix the Gate errors above, then re-run.`);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
if (hasReview(model.findings) && !opts.preview) {
|
|
697
|
+
log(` ${COLOR.yellow}not approved for official delivery.${COLOR.reset}`);
|
|
698
|
+
log(` ${COLOR.cyan}→ approve the reviewed grouping: sungen delivery ${target.screen} --approve${COLOR.reset}`);
|
|
699
|
+
log(` ${COLOR.cyan}→ or render a watermarked draft: sungen delivery ${target.screen} --preview${COLOR.reset}`);
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const { csvPath, xlsxPath } = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
|
|
704
|
+
const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
|
|
705
|
+
const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
|
|
706
|
+
const written = [xlsxPath, csvPath].filter((p): p is string => !!p).map((p) => path.relative(cwd, p));
|
|
707
|
+
for (const rel of written) log(` ${COLOR.green}→ ${rel}${COLOR.reset}`);
|
|
708
|
+
return {
|
|
709
|
+
label: paths.label,
|
|
710
|
+
items: model.manifest.itemCount,
|
|
711
|
+
variants: model.manifest.variantCount,
|
|
712
|
+
passed,
|
|
713
|
+
failed,
|
|
714
|
+
file: written.join(' + '),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* One place that accounts for every target the run did NOT write, so a sweep is
|
|
720
|
+
* never silently partial: scaffolds skipped by preflight and units whose gates /
|
|
721
|
+
* missing map stopped the export.
|
|
722
|
+
*/
|
|
723
|
+
function printSkipSummary(skipped: string[], notExported: string[]): void {
|
|
724
|
+
if (skipped.length === 0 && notExported.length === 0) return;
|
|
725
|
+
log(`${COLOR.gray}Not exported (${skipped.length + notExported.length}):${COLOR.reset}`);
|
|
726
|
+
for (const s of skipped) log(` ${COLOR.gray}· ${s}${COLOR.reset}`);
|
|
727
|
+
for (const n of notExported) log(` ${COLOR.gray}· ${n} (see the message above)${COLOR.reset}`);
|
|
728
|
+
log('');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function printMatrixSummaryTable(summaries: MatrixRunSummary[]): void {
|
|
732
|
+
log(`\n${COLOR.bold}Delivery matrix export complete${COLOR.reset}\n`);
|
|
733
|
+
const colWidth = Math.max(20, ...summaries.map((s) => s.label.length)) + 1;
|
|
734
|
+
log(` ${'Feature'.padEnd(colWidth)}Items Variants Passed Failed File(s)`);
|
|
735
|
+
log(' ' + '-'.repeat(colWidth + 50));
|
|
736
|
+
for (const s of summaries) {
|
|
737
|
+
log(
|
|
738
|
+
` ${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}`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
log(`\n${COLOR.gray}Items = review rows (one target + intent + oracle family). Variants = execution units — use variant counts for progress.${COLOR.reset}\n`);
|
|
742
|
+
}
|
|
743
|
+
|
|
600
744
|
// ----------------------------------------------------------------------------
|
|
601
745
|
// Reporter
|
|
602
746
|
// ----------------------------------------------------------------------------
|
|
@@ -667,23 +811,41 @@ function printSummaryTable(summaries: ScreenSummary[]): void {
|
|
|
667
811
|
export function registerDeliveryCommand(program: Command): void {
|
|
668
812
|
program
|
|
669
813
|
.command('delivery')
|
|
670
|
-
.description('Export Gherkin +
|
|
814
|
+
.description('Export the Test Case & Coverage Matrix (Gherkin + results). --legacy keeps the classic per-scenario workbook.')
|
|
671
815
|
.argument('[names...]', 'Specific screen or flow names. Omit to process all.')
|
|
672
816
|
.option('--skip-preflight', 'Skip pre-flight checks (not recommended)')
|
|
673
817
|
.option('--continue-on-missing', 'Skip targets with blocking misses instead of aborting')
|
|
674
818
|
.option('--env <env>', 'Environment/locale of the run to deliver (sets SUNGEN_ENV; e.g. dev, staging, ja, dev-ja)')
|
|
675
|
-
.
|
|
819
|
+
.option('--legacy', 'Classic per-scenario-steps workbook (BM-2-901-13 layout)')
|
|
820
|
+
.option('--full', 'Alias of --legacy')
|
|
821
|
+
.option('--check', 'Matrix gates only — validate the delivery map, write nothing')
|
|
822
|
+
.option('--approve [ids]', 'Approve proposed groups (all, or a comma-separated id list) + stamp fingerprints')
|
|
823
|
+
.option('--preview', 'Render the matrix despite review-required findings (DRAFT watermark)')
|
|
824
|
+
.option('--format <format>', 'Matrix output format: xlsx (default) | csv | both', 'xlsx')
|
|
825
|
+
.action(async (names: string[], options: {
|
|
826
|
+
skipPreflight?: boolean; continueOnMissing?: boolean; env?: string;
|
|
827
|
+
legacy?: boolean; full?: boolean; check?: boolean; approve?: boolean | string; preview?: boolean;
|
|
828
|
+
format?: string;
|
|
829
|
+
}) => {
|
|
676
830
|
try {
|
|
677
831
|
// Same effect as running with SUNGEN_ENV=<env> — result-file resolution,
|
|
678
832
|
// locale pairing, and test-data overlays all key off this one string.
|
|
679
833
|
if (options.env) process.env.SUNGEN_ENV = options.env;
|
|
680
834
|
const cwd = process.cwd();
|
|
681
835
|
|
|
836
|
+
// Matrix output format — one artifact by default (xlsx); csv/both on request.
|
|
837
|
+
const format = (options.format ?? 'xlsx').toLowerCase();
|
|
838
|
+
if (!['xlsx', 'csv', 'both'].includes(format)) {
|
|
839
|
+
console.error(`${COLOR.red}Invalid --format "${options.format}"${COLOR.reset} — use xlsx | csv | both`);
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
|
|
682
843
|
// 1. Scope detection — each positional name expands into one target
|
|
683
844
|
// per `.feature` file inside that screen/flow. Passing a specific
|
|
684
845
|
// feature basename (e.g. `home-modal`) narrows to that one file.
|
|
685
846
|
let targets: DeliveryTarget[];
|
|
686
|
-
|
|
847
|
+
const explicitScope = !!(names && names.length > 0);
|
|
848
|
+
if (explicitScope) {
|
|
687
849
|
targets = names.flatMap((n) => resolveTargetsFromArg(cwd, n));
|
|
688
850
|
} else {
|
|
689
851
|
targets = listAllTargets(cwd);
|
|
@@ -696,8 +858,13 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
696
858
|
const labels = targets.map((t) => t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName);
|
|
697
859
|
log(`${COLOR.bold}sungen delivery${COLOR.reset} — exporting ${targets.length} feature(s): ${labels.join(', ')}\n`);
|
|
698
860
|
|
|
699
|
-
// 2. Pre-flight
|
|
861
|
+
// 2. Pre-flight — strictness follows the SCOPE. Naming a unit means "I want
|
|
862
|
+
// this one" (fail loudly); a no-argument sweep means "export everything
|
|
863
|
+
// that is ready", so scaffolded units (added but not yet authored) are
|
|
864
|
+
// skipped with a note instead of aborting the whole run — otherwise one
|
|
865
|
+
// untouched scaffold in a work-in-progress project produces zero files.
|
|
700
866
|
let toExport: DeliveryTarget[];
|
|
867
|
+
const skipped: string[] = [];
|
|
701
868
|
if (options.skipPreflight) {
|
|
702
869
|
toExport = targets;
|
|
703
870
|
} else {
|
|
@@ -706,28 +873,79 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
706
873
|
|
|
707
874
|
const blockers = checks.filter(hasBlockingMissing);
|
|
708
875
|
if (blockers.length > 0) {
|
|
709
|
-
if (options.continueOnMissing) {
|
|
710
|
-
const passedScreens = new Set(
|
|
711
|
-
checks.filter((c) => !hasBlockingMissing(c)).map((c) => c.screen)
|
|
712
|
-
);
|
|
713
|
-
toExport = targets.filter((t) => {
|
|
714
|
-
const label = t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName;
|
|
715
|
-
return passedScreens.has(label);
|
|
716
|
-
});
|
|
717
|
-
log(`${COLOR.yellow}Continuing with ${toExport.length} ready target(s).${COLOR.reset}\n`);
|
|
718
|
-
} else {
|
|
876
|
+
if (explicitScope && !options.continueOnMissing) {
|
|
719
877
|
console.error(
|
|
720
|
-
`${COLOR.red}Aborted:${COLOR.reset} ${blockers.length} target(s) have blocking issues.\n` +
|
|
878
|
+
`${COLOR.red}Aborted:${COLOR.reset} ${blockers.length} named target(s) have blocking issues.\n` +
|
|
721
879
|
`Run the suggested commands above, or use ${COLOR.cyan}--continue-on-missing${COLOR.reset} to skip them.`
|
|
722
880
|
);
|
|
723
881
|
process.exit(1);
|
|
724
882
|
}
|
|
883
|
+
const readyLabels = new Set(
|
|
884
|
+
checks.filter((c) => !hasBlockingMissing(c)).map((c) => c.screen)
|
|
885
|
+
);
|
|
886
|
+
toExport = targets.filter((t) => {
|
|
887
|
+
const label = t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName;
|
|
888
|
+
return readyLabels.has(label);
|
|
889
|
+
});
|
|
890
|
+
for (const b of blockers) skipped.push(`${b.screen} (not authored yet — run /sungen:create-test)`);
|
|
891
|
+
log(`${COLOR.yellow}Skipping ${blockers.length} target(s) that have no test cases yet; continuing with ${toExport.length}.${COLOR.reset}\n`);
|
|
725
892
|
} else {
|
|
726
893
|
toExport = targets;
|
|
727
894
|
}
|
|
728
895
|
}
|
|
729
896
|
|
|
730
897
|
// 3. Export
|
|
898
|
+
const legacy = options.legacy || options.full;
|
|
899
|
+
if (!legacy) {
|
|
900
|
+
// Matrix mode (the default). --approve is its own sub-action.
|
|
901
|
+
if (options.approve !== undefined) {
|
|
902
|
+
const ids = typeof options.approve === 'string'
|
|
903
|
+
? options.approve.split(',').map((s) => s.trim()).filter(Boolean)
|
|
904
|
+
: undefined;
|
|
905
|
+
let blocked = false;
|
|
906
|
+
for (const target of toExport) {
|
|
907
|
+
const paths = matrixPathsFor(cwd, target);
|
|
908
|
+
try {
|
|
909
|
+
const { findings, approved } = approveMatrix(paths, ids);
|
|
910
|
+
if (approved.length === 0) {
|
|
911
|
+
log(`${COLOR.red}${paths.label}: approval blocked by Gate errors${COLOR.reset}`);
|
|
912
|
+
printFindings(findings);
|
|
913
|
+
blocked = true;
|
|
914
|
+
} else {
|
|
915
|
+
log(`${COLOR.green}${paths.label}: approved ${approved.length} group(s)${COLOR.reset} — fingerprints stamped in ${path.relative(cwd, paths.mapFile)}`);
|
|
916
|
+
}
|
|
917
|
+
} catch (e) {
|
|
918
|
+
log(`${COLOR.red}${paths.label}:${COLOR.reset} ${e instanceof Error ? e.message : e}`);
|
|
919
|
+
blocked = true;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (blocked) process.exit(1);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const matrixSummaries: MatrixRunSummary[] = [];
|
|
927
|
+
const notExported: string[] = [];
|
|
928
|
+
for (const target of toExport) {
|
|
929
|
+
const s = await exportMatrixTarget(cwd, target, {
|
|
930
|
+
check: options.check, preview: options.preview, format: format as MatrixFormat,
|
|
931
|
+
});
|
|
932
|
+
if (s) matrixSummaries.push(s);
|
|
933
|
+
else notExported.push(target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName);
|
|
934
|
+
}
|
|
935
|
+
if (matrixSummaries.length > 0 && !options.check) printMatrixSummaryTable(matrixSummaries);
|
|
936
|
+
printSkipSummary(skipped, notExported);
|
|
937
|
+
// A named target that produced nothing is a failure. A sweep is a failure
|
|
938
|
+
// only when it produced nothing at all — units still waiting for a map or
|
|
939
|
+
// an approval are the normal state of a live project. Preflight skips are
|
|
940
|
+
// not counted here: reaching this point in an explicit scope means
|
|
941
|
+
// --continue-on-missing was passed, which IS the opt-in to tolerate them.
|
|
942
|
+
const failed = explicitScope
|
|
943
|
+
? notExported.length > 0
|
|
944
|
+
: matrixSummaries.length === 0;
|
|
945
|
+
if (failed) process.exit(1);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
731
949
|
const env = getEnvironment(cwd);
|
|
732
950
|
const summaries: ScreenSummary[] = [];
|
|
733
951
|
for (const target of toExport) {
|
|
@@ -737,6 +955,10 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
737
955
|
|
|
738
956
|
// 4. Summary
|
|
739
957
|
printSummaryTable(summaries);
|
|
958
|
+
printSkipSummary(skipped, []);
|
|
959
|
+
// Same rule as the matrix path: every attempted target in an explicit scope
|
|
960
|
+
// must have produced a file; a sweep only fails when nothing was written.
|
|
961
|
+
if (explicitScope ? summaries.length < toExport.length : summaries.length === 0) process.exit(1);
|
|
740
962
|
} catch (err) {
|
|
741
963
|
console.error(`${COLOR.red}Fatal:${COLOR.reset} ${err instanceof Error ? err.message : err}`);
|
|
742
964
|
process.exit(1);
|
package/src/cli/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { registerRepairCommand } from './commands/repair';
|
|
|
35
35
|
import { registerLoginCommand } from './commands/login';
|
|
36
36
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
37
37
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
38
|
+
import { warnAssetsDrift } from '../orchestrator/assets-drift';
|
|
38
39
|
|
|
39
40
|
// Read version from package.json so `--version` never drifts from the released version.
|
|
40
41
|
const { version } = require('../../package.json') as { version: string };
|
|
@@ -88,6 +89,11 @@ async function main() {
|
|
|
88
89
|
for (const registerCommand of cap.cliCommands ?? []) registerCommand(program);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// Every command warns once when the project's AI assets came from a different
|
|
93
|
+
// build — `update` is the only thing that refreshes them, and nothing used to
|
|
94
|
+
// say so (stderr, so `--json` output stays parseable).
|
|
95
|
+
program.hook('preAction', () => { warnAssetsDrift(process.cwd()); });
|
|
96
|
+
|
|
91
97
|
await program.parseAsync(process.argv);
|
|
92
98
|
}
|
|
93
99
|
|
|
@@ -23,6 +23,7 @@ import { loadPlaywrightReport, resolveResultsPath } from '../exporters/playwrigh
|
|
|
23
23
|
import { mergeFeatureAndSpec } from '../exporters/scenario-merger';
|
|
24
24
|
import { buildScreenSnapshot } from '../exporters/json-exporter';
|
|
25
25
|
import { loadApiCatalog } from '../exporters/api-catalog-loader';
|
|
26
|
+
import { resolveApiEnvironment } from '../exporters/api-environment';
|
|
26
27
|
import { buildApiDetailRows } from '../exporters/xlsx-exporter';
|
|
27
28
|
import { resolvePlatformAppId } from '../exporters/mobile-app-id';
|
|
28
29
|
import { discoverLocaleVariants, LocaleVariant } from '../exporters/locale-variants';
|
|
@@ -132,9 +133,15 @@ function buildOneScreen(
|
|
|
132
133
|
// shared+unit merge delivery.ts uses — so Steps/Expected cells and the API
|
|
133
134
|
// detail sheet match the delivery CSV/XLSX exactly.
|
|
134
135
|
const apiCatalog = loadApiCatalog(cwd, target.kind, target.name);
|
|
136
|
+
// API units call the datasource base_url, not the web baseURL from
|
|
137
|
+
// playwright.config.ts — same Test-Environment override as delivery.ts, so the
|
|
138
|
+
// dashboard's Suites/Export cells never disagree with the CSV/XLSX deliverable.
|
|
139
|
+
const unitEnv = target.kind === 'api'
|
|
140
|
+
? { ...env, ...(resolveApiEnvironment(cwd, apiCatalog) ?? {}) }
|
|
141
|
+
: env;
|
|
135
142
|
const features: FeatureSnapshot[] = [];
|
|
136
143
|
for (const featureBaseName of featureBaseNames) {
|
|
137
|
-
features.push(...buildOneFeature(cwd, target, featureBaseName,
|
|
144
|
+
features.push(...buildOneFeature(cwd, target, featureBaseName, unitEnv, specLink, mobile, apiCatalog));
|
|
138
145
|
}
|
|
139
146
|
if (features.length === 0) return null;
|
|
140
147
|
|