@sun-asterisk/sungen 3.2.34 → 3.2.35
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 +9 -4
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +5 -1
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/defect.d.ts +7 -0
- package/dist/cli/commands/defect.d.ts.map +1 -0
- package/dist/cli/commands/defect.js +68 -0
- package/dist/cli/commands/defect.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +35 -7
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/triage.d.ts +7 -0
- package/dist/cli/commands/triage.d.ts.map +1 -0
- package/dist/cli/commands/triage.js +98 -0
- package/dist/cli/commands/triage.js.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +4 -0
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +1 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +8 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +12 -0
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +49 -10
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.d.ts +19 -0
- package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.js +76 -6
- package/dist/exporters/matrix/carry-manual-results.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +5 -2
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +14 -3
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/render-csv.js +1 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +3 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +5 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +1 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +2 -0
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +12 -2
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +5 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +1 -0
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
- package/dist/generators/test-generator/code-generator.js +4 -5
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +9 -3
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +9 -3
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +8 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +52 -8
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +16 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +59 -0
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +10 -2
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +18 -5
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/counted-assertions.d.ts +2 -0
- package/dist/harness/counted-assertions.d.ts.map +1 -0
- package/dist/harness/counted-assertions.js +38 -0
- package/dist/harness/counted-assertions.js.map +1 -0
- package/dist/harness/defect-draft.d.ts +14 -0
- package/dist/harness/defect-draft.d.ts.map +1 -0
- package/dist/harness/defect-draft.js +143 -0
- package/dist/harness/defect-draft.js.map +1 -0
- package/dist/harness/defects.d.ts +24 -0
- package/dist/harness/defects.d.ts.map +1 -0
- package/dist/harness/defects.js +118 -0
- package/dist/harness/defects.js.map +1 -0
- package/dist/harness/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +2 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/next-step.d.ts +0 -4
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +34 -4
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts +18 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +20 -11
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +7 -1
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +23 -6
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/triage-signals.d.ts +20 -0
- package/dist/harness/triage-signals.d.ts.map +1 -0
- package/dist/harness/triage-signals.js +30 -0
- package/dist/harness/triage-signals.js.map +1 -0
- package/dist/harness/triage-unit.d.ts +23 -0
- package/dist/harness/triage-unit.d.ts.map +1 -0
- package/dist/harness/triage-unit.js +142 -0
- package/dist/harness/triage-unit.js.map +1 -0
- package/dist/harness/triage.d.ts +28 -0
- package/dist/harness/triage.d.ts.map +1 -0
- package/dist/harness/triage.js +99 -0
- package/dist/harness/triage.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/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/ai-skills/commands.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/commands.js +5 -3
- package/dist/orchestrator/ai-skills/commands.js.map +1 -1
- package/dist/orchestrator/ai-skills/golden-skills.test.js +29 -0
- package/dist/orchestrator/ai-skills/golden-skills.test.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +4 -2
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +8 -2
- package/dist/orchestrator/templates/ai-src/config/claude.md +20 -7
- package/dist/orchestrator/templates/ai-src/config/copilot.md +20 -7
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +7 -4
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +25 -9
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +2 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +4 -3
- package/dist/orchestrator/templates/specs-test-data.ts +22 -4
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/capability.ts +3 -1
- package/src/cli/commands/defect.ts +49 -0
- package/src/cli/commands/delivery.ts +33 -10
- package/src/cli/commands/triage.ts +57 -0
- package/src/cli/index.ts +4 -0
- package/src/exporters/csv-exporter.ts +5 -1
- package/src/exporters/feature-parser.ts +12 -0
- package/src/exporters/matrix/build.ts +51 -9
- package/src/exporters/matrix/carry-manual-results.ts +70 -7
- package/src/exporters/matrix/export.ts +12 -2
- package/src/exporters/matrix/render-csv.ts +1 -1
- package/src/exporters/matrix/render-xlsx.ts +3 -0
- package/src/exporters/matrix/types.ts +5 -0
- package/src/exporters/playwright-report-parser.ts +1 -0
- package/src/exporters/scenario-merger.ts +2 -1
- package/src/exporters/test-data-resolver.ts +10 -2
- package/src/exporters/types.ts +5 -0
- package/src/generators/gherkin-parser/index.ts +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
- package/src/generators/test-generator/code-generator.ts +4 -4
- package/src/generators/test-generator/diagnostics.ts +9 -3
- package/src/generators/test-generator/patterns/types.ts +2 -0
- package/src/generators/test-generator/step-mapper.ts +56 -9
- package/src/generators/test-generator/utils/data-resolver.ts +53 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +10 -2
- package/src/harness/audit.ts +19 -6
- package/src/harness/counted-assertions.ts +33 -0
- package/src/harness/defect-draft.ts +115 -0
- package/src/harness/defects.ts +94 -0
- package/src/harness/manifest.ts +2 -2
- package/src/harness/next-step.ts +29 -4
- package/src/harness/parse.ts +20 -11
- package/src/harness/script-check.ts +7 -1
- package/src/harness/sensors.ts +26 -7
- package/src/harness/triage-signals.ts +46 -0
- package/src/harness/triage-unit.ts +113 -0
- package/src/harness/triage.ts +127 -0
- package/src/harness/viewpoint-ledger.ts +2 -2
- package/src/index.ts +2 -1
- package/src/orchestrator/ai-skills/commands.ts +5 -3
- package/src/orchestrator/ai-skills/golden-skills.test.ts +23 -0
- package/src/orchestrator/screen-manager.ts +3 -1
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
- package/src/orchestrator/templates/ai-src/commands/run-test.md +8 -2
- package/src/orchestrator/templates/ai-src/config/claude.md +20 -7
- package/src/orchestrator/templates/ai-src/config/copilot.md +20 -7
- package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +7 -4
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +25 -9
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +2 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +4 -3
- package/src/orchestrator/templates/specs-test-data.ts +22 -4
|
@@ -180,7 +180,7 @@ export function registerAuditCommand(program: Command): void {
|
|
|
180
180
|
const dir = findUnitDir(name);
|
|
181
181
|
if (!dir) throw new Error(`Not found: qa/screens/${name}, qa/flows/${name}, or qa/api/${name}`);
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
let report = runAudit(dir, name);
|
|
184
184
|
|
|
185
185
|
// The baseline is only ever written HERE, never by the measurement itself:
|
|
186
186
|
// a first sighting has nothing to compare against, and any later change is
|
|
@@ -201,9 +201,14 @@ export function registerAuditCommand(program: Command): void {
|
|
|
201
201
|
if (vb.reparsed) acceptViewpointBaseline(process.cwd(), unitId, { hash: vb.hash, ids: vb.ids, fileHash: vb.fileHash });
|
|
202
202
|
if ((vb.status === 'new' && !postHoc) || (options.acceptViewpoint && (vb.status === 'changed' || postHoc))) {
|
|
203
203
|
acceptViewpointBaseline(process.cwd(), unitId, { hash: vb.hash, ids: vb.ids, fileHash: vb.fileHash });
|
|
204
|
-
if (options.acceptViewpoint
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
if (options.acceptViewpoint) {
|
|
205
|
+
// Measure again against what was just accepted: the report printed and saved below (the
|
|
206
|
+
// one `sungen next` reads) must not be the pre-accept one, still holding the axes (#867).
|
|
207
|
+
report = runAudit(dir, name);
|
|
208
|
+
if (!options.json) {
|
|
209
|
+
console.log(`\n ✓ test-viewpoint.md accepted as the baseline for ${name} (${vb.ids.length} viewpoint id(s)).`);
|
|
210
|
+
console.log(' The report below is measured against it.\n');
|
|
211
|
+
}
|
|
207
212
|
}
|
|
208
213
|
} else if (options.acceptViewpoint && !options.json) {
|
|
209
214
|
// `absent` covers a file with no readable viewpoint id too — say which (#832).
|
|
@@ -265,7 +265,9 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
265
265
|
else console.log(' – skipped appium-environment/ (already exists)');
|
|
266
266
|
|
|
267
267
|
// Best-effort driver install — non-fatal so capability add still succeeds offline.
|
|
268
|
-
|
|
268
|
+
// --skip-install promises no npm install; the driver + wdio/appium runtime is ~290 MB.
|
|
269
|
+
if (o.skipInstall) console.log(' – skipped mobile driver install (--skip-install) — later: sungen capability add mobile');
|
|
270
|
+
else installMobileDriver(cwd);
|
|
269
271
|
|
|
270
272
|
// --skip-install means "record only" — then provisioning the machine is not wanted either.
|
|
271
273
|
runAppiumEnvironmentSetup(cwd, { skip: !!(o.skipEnvSetup || o.skipInstall) });
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolveUnit } from '../resolve-unit';
|
|
3
|
+
import { draftDefects } from '../../harness/defect-draft';
|
|
4
|
+
import { readDefects } from '../../harness/defects';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `sungen defect draft|list` — defect records under qa/defects/ (see harness/defects.ts).
|
|
8
|
+
* `draft` writes one record per product-candidate row of the unit's last `sungen triage`.
|
|
9
|
+
*/
|
|
10
|
+
export function registerDefectCommand(program: Command): void {
|
|
11
|
+
const defect = program.command('defect').description('Draft and list defect records (qa/defects/DEF-NNN-*.md) from red scenarios');
|
|
12
|
+
|
|
13
|
+
defect
|
|
14
|
+
.command('draft')
|
|
15
|
+
.description('Draft a defect record for each product-candidate row of the last `sungen triage`')
|
|
16
|
+
.option('-s, --screen <name>', 'Screen or flow name')
|
|
17
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
18
|
+
.option('--scenario <title|id>', 'Only the scenario whose title contains this text, or that carries this VP id')
|
|
19
|
+
.option('--json', 'Output raw JSON')
|
|
20
|
+
.action((o: { screen?: string; api?: string; scenario?: string; json?: boolean }) => {
|
|
21
|
+
try {
|
|
22
|
+
const name = o.screen || o.api;
|
|
23
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
24
|
+
const unit = resolveUnit(name, process.cwd());
|
|
25
|
+
if (!unit) throw new Error(`Not found: qa/screens/${name}, qa/flows/${name}, or qa/api/${name}`);
|
|
26
|
+
const r = draftDefects(process.cwd(), unit, { scenario: o.scenario });
|
|
27
|
+
if (o.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
28
|
+
if (r.missingTriage) { console.log(`\n ${name}: no triage yet — run \`sungen triage --screen ${name}\` after the tests.\n`); return; }
|
|
29
|
+
for (const f of r.written) console.log(` ✎ ${f}`);
|
|
30
|
+
for (const s of r.skipped) console.log(` ○ already recorded as ${s.id}: ${s.scenario}`);
|
|
31
|
+
if (!r.written.length && !r.skipped.length) console.log(` ✓ ${name}: no product-candidate rows to draft.`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
defect
|
|
39
|
+
.command('list')
|
|
40
|
+
.description('List defect records (id, severity, status, units, scenario count)')
|
|
41
|
+
.option('--status <status>', 'Only records with this status (open, fixed, verified, closed, wont-fix)')
|
|
42
|
+
.option('--json', 'Output raw JSON')
|
|
43
|
+
.action((o: { status?: string; json?: boolean }) => {
|
|
44
|
+
const rows = readDefects(process.cwd()).filter((r) => !o.status || r.status === o.status);
|
|
45
|
+
if (o.json) { console.log(JSON.stringify(rows, null, 2)); return; }
|
|
46
|
+
if (!rows.length) { console.log(' (no defect records)'); return; }
|
|
47
|
+
for (const r of rows) console.log(` ${r.id} ${r.severity.padEnd(8)} ${r.status.padEnd(9)} ${r.units.join(',')} ${r.scenarios.length} scenario(s) ${r.title}`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -7,7 +7,7 @@ import { Command } from 'commander';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import { parse as parseYaml } from 'yaml';
|
|
10
|
-
import { parseFeatureMetadata } from '../../exporters/feature-parser';
|
|
10
|
+
import { isHookScenario, parseFeatureMetadata } from '../../exporters/feature-parser';
|
|
11
11
|
import { parseSpecFile } from '../../exporters/spec-parser';
|
|
12
12
|
import { loadTestData } from '../../exporters/test-data-resolver';
|
|
13
13
|
import { loadSelectorKeyMap } from '../../exporters/selector-key-resolver';
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
renderCsv,
|
|
21
21
|
writeCsv,
|
|
22
22
|
} from '../../exporters/csv-exporter';
|
|
23
|
-
import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
|
|
23
|
+
import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet, deliverableXlsxPath } from '../../exporters/xlsx-exporter';
|
|
24
24
|
import { loadApiCatalog } from '../../exporters/api-catalog-loader';
|
|
25
25
|
import { resolveApiEnvironment } from '../../exporters/api-environment';
|
|
26
26
|
import { getEnvironment, platformOfResultFile, RunEnvironment } from '../../exporters/run-environment';
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
hasReview,
|
|
39
39
|
} from '../../exporters/matrix/export';
|
|
40
40
|
import { MatrixFinding } from '../../exporters/matrix/types';
|
|
41
|
-
import { ManualCarryReport } from '../../exporters/matrix/carry-manual-results';
|
|
41
|
+
import { applyManualCarry, ManualCarryReport, PriorManualResults, readPriorManualResults } from '../../exporters/matrix/carry-manual-results';
|
|
42
42
|
import { formatTestOutcome, resultTotals } from '../../exporters/matrix/result-totals';
|
|
43
43
|
import { readCapabilities } from '../../harness/capability';
|
|
44
44
|
|
|
@@ -85,6 +85,12 @@ export interface DeliveryTarget {
|
|
|
85
85
|
isFlow: boolean;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** The command that regenerates a target's unit — flag by kind, the unit (not feature) name. */
|
|
89
|
+
function generateCommandFor(target: DeliveryTarget): string {
|
|
90
|
+
const flag = target.kind === 'flow' ? '--flow' : target.kind === 'api' ? '--api' : '--screen';
|
|
91
|
+
return `sungen generate ${flag} ${target.screen}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
function makeTarget(screen: string, featureBaseName: string, kind: UnitKind): DeliveryTarget {
|
|
89
95
|
return { screen, featureBaseName, name: featureBaseName, kind, isFlow: kind === 'flow' };
|
|
90
96
|
}
|
|
@@ -464,6 +470,7 @@ async function exportTarget(
|
|
|
464
470
|
if (isMobileProject(cwd)) removeBaseDeliverables(cwd, target.featureBaseName);
|
|
465
471
|
const rows = buildTestCaseRows({
|
|
466
472
|
screen: label,
|
|
473
|
+
generateCommand: generateCommandFor(target),
|
|
467
474
|
featureName: feature.featureName,
|
|
468
475
|
// OS-precise env column: a mobile dual-id Path resolves to just the
|
|
469
476
|
// android package / ios bundle id for SUNGEN_ENV=android|ios.
|
|
@@ -522,6 +529,7 @@ async function exportTarget(
|
|
|
522
529
|
for (const variant of osVariants) {
|
|
523
530
|
const variantRows = buildTestCaseRows({
|
|
524
531
|
screen: label,
|
|
532
|
+
generateCommand: generateCommandFor(target),
|
|
525
533
|
featureName: feature.featureName,
|
|
526
534
|
featurePath: resolvePlatformAppId(featurePath, variant.locale), // OS-precise app id
|
|
527
535
|
merged,
|
|
@@ -574,6 +582,7 @@ async function exportTarget(
|
|
|
574
582
|
const variantResults = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
|
|
575
583
|
const variantRows = buildTestCaseRows({
|
|
576
584
|
screen: label,
|
|
585
|
+
generateCommand: generateCommandFor(target),
|
|
577
586
|
featureName: feature.featureName,
|
|
578
587
|
// No-op for web (URL Path); for a mobile fall-through (a no-suffix result
|
|
579
588
|
// json) this collapses the dual-id to the android package so the combined
|
|
@@ -685,9 +694,11 @@ async function exportMatrixTarget(
|
|
|
685
694
|
// probes that are, by contract, outside the official suite.
|
|
686
695
|
{
|
|
687
696
|
const featureMeta = parseFeatureMetadata(paths.featureFile);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
697
|
+
// Hooks are setup/teardown, never test cases — they neither need nor make a deliverable (#880).
|
|
698
|
+
const scenarios = featureMeta.scenarios.filter((s) => !isHookScenario(s));
|
|
699
|
+
const deliverable = scenarios.filter((s) => !s.tags.some((t) => /^@exploration\b/i.test(t)));
|
|
700
|
+
if (scenarios.length > 0 && deliverable.length === 0) {
|
|
701
|
+
log(`${COLOR.gray}${paths.label}: nothing to deliver — all ${scenarios.length} scenario(s) are @exploration probes (outside the official suite).${COLOR.reset}`);
|
|
691
702
|
return 'skipped';
|
|
692
703
|
}
|
|
693
704
|
}
|
|
@@ -707,6 +718,17 @@ async function exportMatrixTarget(
|
|
|
707
718
|
return null;
|
|
708
719
|
}
|
|
709
720
|
|
|
721
|
+
// The tester's recorded Manual results are part of the outcome: read them BEFORE anything is
|
|
722
|
+
// counted, so this line, the workbook's totals and the summary table all agree (#891). A file
|
|
723
|
+
// that cannot be read is reported where it matters — when it would be overwritten.
|
|
724
|
+
const format = opts.format ?? 'xlsx';
|
|
725
|
+
let prior: PriorManualResults | null = null;
|
|
726
|
+
let priorError: Error | null = null;
|
|
727
|
+
try { prior = await readPriorManualResults(deliverableXlsxPath(cwd, paths.unit)); }
|
|
728
|
+
// A CSV-only export overwrites no workbook, so an unreadable one only means "nothing to carry".
|
|
729
|
+
catch (e) { if (format !== 'csv') priorError = e as Error; }
|
|
730
|
+
applyManualCarry(model, prior);
|
|
731
|
+
|
|
710
732
|
log(`${COLOR.bold}${paths.label}${COLOR.reset} — ${model.manifest.itemCount} delivery item(s), ${model.manifest.variantCount} coverage variant(s), review: ${model.manifest.approvalState} (design), test outcome: ${formatTestOutcome(resultTotals(model.items))}`);
|
|
711
733
|
{
|
|
712
734
|
const mocked = model.items.flatMap((it) => it.variants.filter((v) => v.mocked));
|
|
@@ -741,7 +763,8 @@ async function exportMatrixTarget(
|
|
|
741
763
|
|
|
742
764
|
let deliverables: Awaited<ReturnType<typeof writeMatrixDeliverables>>;
|
|
743
765
|
try {
|
|
744
|
-
|
|
766
|
+
if (priorError) throw priorError;
|
|
767
|
+
deliverables = await writeMatrixDeliverables(paths, model, format, prior);
|
|
745
768
|
} catch (e) {
|
|
746
769
|
// The existing workbook could not be read, so writing would destroy its Manual results.
|
|
747
770
|
log(` ${COLOR.red}not written:${COLOR.reset} ${(e as Error).message}`);
|
|
@@ -981,9 +1004,9 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
981
1004
|
// Same contract as the export path: an all-@exploration feature has no
|
|
982
1005
|
// deliverable variants, so there is no map to approve — skip, not an error.
|
|
983
1006
|
{
|
|
984
|
-
const
|
|
985
|
-
if (
|
|
986
|
-
&&
|
|
1007
|
+
const scenarios = parseFeatureMetadata(paths.featureFile).scenarios.filter((s) => !isHookScenario(s));
|
|
1008
|
+
if (scenarios.length > 0
|
|
1009
|
+
&& scenarios.every((s) => s.tags.some((t) => /^@exploration\b/i.test(t)))) {
|
|
987
1010
|
log(`${COLOR.gray}${paths.label}: nothing to approve — all scenario(s) are @exploration probes.${COLOR.reset}`);
|
|
988
1011
|
continue;
|
|
989
1012
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { resolveUnit } from '../resolve-unit';
|
|
4
|
+
import { triageUnit, writeTriageReport } from '../../harness/triage-unit';
|
|
5
|
+
import type { TriageClass } from '../../harness/triage';
|
|
6
|
+
|
|
7
|
+
/** Display order: what needs a decision first, green last. */
|
|
8
|
+
const ORDER: TriageClass[] = ['product-candidate', 'unclassified', 'test-defect', 'environment', 'blocked-capability', 'known-defect', 'flaky'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `sungen triage` — classify every red or flaky scenario of the latest run (see harness/triage.ts).
|
|
12
|
+
* Reports, never gates: always exits 0 once the unit is found.
|
|
13
|
+
*/
|
|
14
|
+
export function registerTriageCommand(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command('triage')
|
|
17
|
+
.description('Classify each red or flaky scenario of the latest run: known-defect, blocked, environment, test-defect, product-candidate, unclassified')
|
|
18
|
+
.option('-s, --screen <name>', 'Screen or flow name')
|
|
19
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
20
|
+
.option('--area <name>', 'Alias of --api')
|
|
21
|
+
.option('--json', 'Output the raw JSON report')
|
|
22
|
+
.action((o: { screen?: string; api?: string; area?: string; json?: boolean }) => {
|
|
23
|
+
try {
|
|
24
|
+
const name = o.screen || o.api || o.area;
|
|
25
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const unit = resolveUnit(name, cwd);
|
|
28
|
+
if (!unit) throw new Error(`Not found: qa/screens/${name}, qa/flows/${name}, or qa/api/${name}`);
|
|
29
|
+
|
|
30
|
+
const report = triageUnit(cwd, unit);
|
|
31
|
+
if (report.resultFiles.length === 0) {
|
|
32
|
+
if (o.json) console.log(JSON.stringify(report, null, 2));
|
|
33
|
+
else console.log(`\n ${name}: no results — run the tests first.\n`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const saved = writeTriageReport(cwd, report);
|
|
37
|
+
if (o.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
38
|
+
|
|
39
|
+
const L = console.log;
|
|
40
|
+
L(`\n━━━ Triage: ${name} ━━━`);
|
|
41
|
+
if (report.rows.length === 0) L(' ✓ every scenario passed on its first attempt');
|
|
42
|
+
for (const cls of ORDER) {
|
|
43
|
+
const rows = report.rows.filter((r) => r.class === cls);
|
|
44
|
+
if (rows.length === 0) continue;
|
|
45
|
+
L(`\n ${cls} (${rows.length})`);
|
|
46
|
+
for (const r of rows) {
|
|
47
|
+
L(` • ${r.scenario}${r.variant ? ` [${r.variant}]` : ''}${r.defectId ? ` → ${r.defectId}` : ''}`);
|
|
48
|
+
L(` ${r.ruleId}: ${r.evidence}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
L(`\n Report: ${path.relative(cwd, saved)}\n`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -37,6 +37,8 @@ import { registerCapabilityCommand } from './commands/capability';
|
|
|
37
37
|
import { registerFlowCheckCommand } from './commands/flow-check';
|
|
38
38
|
import { registerContextCommand } from './commands/context';
|
|
39
39
|
import { registerRepairCommand } from './commands/repair';
|
|
40
|
+
import { registerTriageCommand } from './commands/triage';
|
|
41
|
+
import { registerDefectCommand } from './commands/defect';
|
|
40
42
|
import { registerResultsCommand } from './commands/results-merge';
|
|
41
43
|
import { registerLoginCommand } from './commands/login';
|
|
42
44
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
@@ -89,6 +91,8 @@ registerSetupMobileEnvCommand(program);
|
|
|
89
91
|
registerFlowCheckCommand(program);
|
|
90
92
|
registerContextCommand(program);
|
|
91
93
|
registerRepairCommand(program);
|
|
94
|
+
registerTriageCommand(program);
|
|
95
|
+
registerDefectCommand(program);
|
|
92
96
|
registerResultsCommand(program);
|
|
93
97
|
registerIngestCommand(program);
|
|
94
98
|
registerEvalCommand(program);
|
|
@@ -32,6 +32,10 @@ import { formatEnv } from './run-environment';
|
|
|
32
32
|
export interface BuildCsvInput {
|
|
33
33
|
screen: string;
|
|
34
34
|
featureName: string;
|
|
35
|
+
/** The exact command that regenerates this unit (`sungen generate --flow checkout`). `screen`
|
|
36
|
+
* is the deliverable label — the FEATURE basename, `flow/<x>` for a flow — so a note built from
|
|
37
|
+
* it named a command that fails (#880). */
|
|
38
|
+
generateCommand?: string;
|
|
35
39
|
/** URL path for the screen/flow entry point — concatenated onto baseURL
|
|
36
40
|
* when building the Test Environment column so QA can see the exact URL
|
|
37
41
|
* the test targets. */
|
|
@@ -207,7 +211,7 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
207
211
|
// No execution record AND no compiled test → Manual if @manual, else genuinely not compiled.
|
|
208
212
|
testResult = testcaseType === 'Manual' ? 'Pending' : 'N/A';
|
|
209
213
|
if (testcaseType !== 'Manual') {
|
|
210
|
-
note =
|
|
214
|
+
note = `Scenario not compiled — re-run \`${input.generateCommand ?? `sungen generate --screen ${input.screen}`}\``;
|
|
211
215
|
} else {
|
|
212
216
|
// Why the case is manual — the `# MANUAL: …` comment — so the tester reading the
|
|
213
217
|
// deliverable knows what blocks automation (#810). Empty when no reason is written.
|
|
@@ -117,6 +117,7 @@ export function parseFeatureMetadata(featureFilePath: string): FeatureMetadata {
|
|
|
117
117
|
referencedVars: extractReferencedVars(sc, extractTestcaseType([...parsed.tags, ...sc.tags]) === 'Manual'),
|
|
118
118
|
orderedSteps: classifySteps(sc),
|
|
119
119
|
comments: sc.comments,
|
|
120
|
+
hookType: sc.hookType,
|
|
120
121
|
}));
|
|
121
122
|
|
|
122
123
|
return {
|
|
@@ -136,6 +137,17 @@ export function isStepsBaseScenario(sc: ScenarioMetadata): boolean {
|
|
|
136
137
|
return !!sc.stepsName;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* A hook (`@beforeAll` / `@afterEach` / `@afterAll`) — setup or teardown around the tests, compiled
|
|
142
|
+
* to `test.beforeAll()` etc. Read from the scenario's OWN tag (the parser's `hookType`), never from
|
|
143
|
+
* the feature+scenario tag union. It is not a test case: counted as one, it blocked the matrix at
|
|
144
|
+
* Gate A (a hook carries no VP id, as the gherkin skill tells authors) and showed in the legacy
|
|
145
|
+
* workbook and the dashboard as a "Not compiled" row (#880).
|
|
146
|
+
*/
|
|
147
|
+
export function isHookScenario(sc: ScenarioMetadata): boolean {
|
|
148
|
+
return !!sc.hookType;
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
/**
|
|
140
152
|
* Return true if this is the default scaffold sample scenario (not real).
|
|
141
153
|
*/
|
|
@@ -96,6 +96,7 @@ function resolveDataPairs(
|
|
|
96
96
|
vars: string[],
|
|
97
97
|
testData: Record<string, string> | null,
|
|
98
98
|
row?: Record<string, unknown>,
|
|
99
|
+
runtimeVars: Set<string> = new Set(),
|
|
99
100
|
): string[] {
|
|
100
101
|
const pairs: string[] = [];
|
|
101
102
|
if (row) {
|
|
@@ -107,7 +108,10 @@ function resolveDataPairs(
|
|
|
107
108
|
for (const v of vars) {
|
|
108
109
|
const val = row && v in row ? undefined : testData?.[v]; // row columns already listed
|
|
109
110
|
// Credential-named refs (`accounts.0.password`) render `***` — this column is read outside the run.
|
|
110
|
-
|
|
111
|
+
// A value may embed a var the scenario captures at run time (`Total ({{stat}})`) or a
|
|
112
|
+
// dataset column — expanded with the same vars as the cells, so both columns agree (#879).
|
|
113
|
+
const scope = row ? { ...(testData ?? {}), ...rowAsStrings(row) } : (testData ?? {});
|
|
114
|
+
if (val !== undefined) pairs.push(`${v}: ${displayValue(maskCredentialValue(v, expandValue(String(val), scope, runtimeVars, new Set([v]))))}`);
|
|
111
115
|
}
|
|
112
116
|
return pairs;
|
|
113
117
|
}
|
|
@@ -136,6 +140,10 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
136
140
|
// Test-data values may cross-reference other keys (email_padded: " {{valid_email}} ") —
|
|
137
141
|
// resolve one level so display cells never leak a template token (review B-04).
|
|
138
142
|
const testData = resolveCrossRefs(inputs.testData);
|
|
143
|
+
// Display cells expand from the RAW values, per scenario — with its row and its runtime vars —
|
|
144
|
+
// in the runtime's order (captured var > row column > test-data key). The pre-resolved copy above
|
|
145
|
+
// only serves row discovery and result matching, which render nothing (#879).
|
|
146
|
+
const rawTestData = inputs.testData ?? {};
|
|
139
147
|
const apiCatalog = inputs.apiCatalog ?? {};
|
|
140
148
|
const variants: CoverageVariant[] = [];
|
|
141
149
|
|
|
@@ -305,14 +313,14 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
305
313
|
const resultByLabel = new Map(resultVariants.map((rv) => [rv.nameSuffix.replace(/^ — /, ''), rv.result]));
|
|
306
314
|
rows.forEach((row, i) => {
|
|
307
315
|
const label = String(row.case ?? row.name ?? row.label ?? `row ${i + 1}`);
|
|
308
|
-
const cells = renderCells({ ...
|
|
316
|
+
const cells = renderCells({ ...rawTestData, ...rowAsStrings(row) });
|
|
309
317
|
variants.push({
|
|
310
318
|
...base,
|
|
311
319
|
ref: `${vpId}#${label}`,
|
|
312
320
|
caseLabel: label,
|
|
313
321
|
title: `${category1} — ${label}`,
|
|
314
322
|
condition: label,
|
|
315
|
-
data: resolveDataPairs(m.feature.referencedVars,
|
|
323
|
+
data: resolveDataPairs(m.feature.referencedVars, rawTestData, row, runtimeVars),
|
|
316
324
|
trigger: cells.trigger,
|
|
317
325
|
oracle: cells.oracle,
|
|
318
326
|
precondition: cells.precondition,
|
|
@@ -322,13 +330,13 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
322
330
|
});
|
|
323
331
|
});
|
|
324
332
|
} else {
|
|
325
|
-
const cells = renderCells(
|
|
333
|
+
const cells = renderCells(rawTestData);
|
|
326
334
|
variants.push({
|
|
327
335
|
...base,
|
|
328
336
|
ref: vpId,
|
|
329
337
|
title: category1,
|
|
330
338
|
condition: category1,
|
|
331
|
-
data: resolveDataPairs(m.feature.referencedVars,
|
|
339
|
+
data: resolveDataPairs(m.feature.referencedVars, rawTestData, undefined, runtimeVars),
|
|
332
340
|
trigger: cells.trigger,
|
|
333
341
|
oracle: cells.oracle,
|
|
334
342
|
precondition: cells.precondition,
|
|
@@ -360,9 +368,9 @@ function substituteDisplayVars(
|
|
|
360
368
|
if (isCapture && /\bas\s+$/i.test(text.slice(0, offset))) return m;
|
|
361
369
|
// A value the test produces at run time has no static form — name it so the
|
|
362
370
|
// tester knows to compare against what the earlier step captured.
|
|
363
|
-
if (
|
|
371
|
+
if (isRuntimeVar(key, runtimeVars)) return `the captured ${key}`;
|
|
364
372
|
if (!(key in vars)) return m; // unknown stays literal → Gate D flags it
|
|
365
|
-
return displayValue(vars[key]);
|
|
373
|
+
return displayValue(expandValue(vars[key], vars, runtimeVars, new Set([key])));
|
|
366
374
|
});
|
|
367
375
|
// A dynamic token written straight into a step (`{{$uuid}}`) never reaches
|
|
368
376
|
// displayValue — name it here so the same wording covers both routes.
|
|
@@ -432,12 +440,45 @@ export function computeRequirements(
|
|
|
432
440
|
});
|
|
433
441
|
}
|
|
434
442
|
|
|
435
|
-
|
|
443
|
+
const isRuntimeVar = (key: string, runtimeVars: Set<string>): boolean =>
|
|
444
|
+
runtimeVars.has(key) || runtimeVars.has(key.split(/[.[]/)[0]);
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Expand the `{{refs}}` inside a test-data VALUE the way the runtime's `interpolate()` does
|
|
448
|
+
* (specs/generated/test-data.ts): recursively, depth-capped against reference cycles, and a var
|
|
449
|
+
* the scenario captures at run time is named, not looked up. An unknown ref (or a cycle) stays
|
|
450
|
+
* literal, so Gate D still refuses it; a nested value renders through displayValue, as it did
|
|
451
|
+
* when this resolved one level (an empty one reads `(empty)`).
|
|
452
|
+
*
|
|
453
|
+
* Delivery used to resolve ONE level, before any scenario's runtime vars were known: both
|
|
454
|
+
* `heading_expected: "Total ({{stat}})"` with `remember … as {{stat}}`, and a plain chain
|
|
455
|
+
* (`a: "{{b}}"`, `b: "x {{c}}"`), left a token that blocked the export of a suite the runtime
|
|
456
|
+
* runs fine (#879).
|
|
457
|
+
*/
|
|
458
|
+
function expandValue(
|
|
459
|
+
value: string, vars: Record<string, string>, runtimeVars: Set<string>,
|
|
460
|
+
expanding: ReadonlySet<string> = new Set(), depth = 0,
|
|
461
|
+
): string {
|
|
462
|
+
if (depth > 10 || !value.includes('{{')) return value;
|
|
463
|
+
return value.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (m, key: string) => {
|
|
464
|
+
if (isRuntimeVar(key, runtimeVars)) return `the captured ${key}`;
|
|
465
|
+
const v = vars[key];
|
|
466
|
+
// A key already being expanded is a cycle: keep the token (Gate D names it) instead of
|
|
467
|
+
// re-entering — `a: "{{a}}{{a}}"` doubled per level and exhausted the heap.
|
|
468
|
+
if (v === undefined || v === null || expanding.has(key)) return m;
|
|
469
|
+
return displayValue(expandValue(String(v), vars, runtimeVars, new Set([...expanding, key]), depth + 1));
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Pre-resolve test-data cross-references before any scenario is known — every static level.
|
|
475
|
+
* A runtime var is not known yet and stays a token here; each scenario's cells expand it.
|
|
476
|
+
*/
|
|
436
477
|
function resolveCrossRefs(testData: Record<string, string> | null): Record<string, string> | null {
|
|
437
478
|
if (!testData) return null;
|
|
438
479
|
const out: Record<string, string> = {};
|
|
439
480
|
for (const [k, v] of Object.entries(testData)) {
|
|
440
|
-
out[k] = typeof v === 'string' && v.includes('{{') ?
|
|
481
|
+
out[k] = typeof v === 'string' && v.includes('{{') ? renderDynamicTokens(expandValue(v, testData, new Set(), new Set([k]))) : v;
|
|
441
482
|
}
|
|
442
483
|
return out;
|
|
443
484
|
}
|
|
@@ -463,6 +504,7 @@ function rowAsStrings(row: Record<string, unknown>): Record<string, string> {
|
|
|
463
504
|
* as blocked), which silently skewed the derived parent result.
|
|
464
505
|
*/
|
|
465
506
|
export function variantState(v: CoverageVariant): VariantState {
|
|
507
|
+
if (v.manualState) return v.manualState; // a carried Manual result (#891)
|
|
466
508
|
const r = v.result;
|
|
467
509
|
if (!r) return 'Pending';
|
|
468
510
|
switch (r.status) {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import * as fs from 'fs';
|
|
21
21
|
import * as ExcelJS from 'exceljs';
|
|
22
|
-
import { MatrixModel } from './types';
|
|
22
|
+
import { MatrixModel, VariantState } from './types';
|
|
23
|
+
import { rollUp } from './build';
|
|
23
24
|
import { HEADER_ROW, RESULT_COL } from './render-testcase-sheet';
|
|
24
25
|
|
|
25
26
|
const MANUAL_SHEET = 'Manual';
|
|
@@ -169,6 +170,70 @@ function resetNote(prior: CarriedCell[]): ExcelJS.CellValue {
|
|
|
169
170
|
return own ? `${header}\n${own}` : header;
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
/**
|
|
174
|
+
* What happens to each Manual variant's prior row: `carry` its cells, or `reset` it to Pending
|
|
175
|
+
* because the procedure changed since the result was recorded. A row with nothing recorded has
|
|
176
|
+
* no entry. The ONE decision both the model (totals, #891) and the workbook cells read.
|
|
177
|
+
*/
|
|
178
|
+
export function planManualCarry(model: MatrixModel, prior: PriorManualResults | null): Map<string, 'carry' | 'reset'> {
|
|
179
|
+
const plan = new Map<string, 'carry' | 'reset'>();
|
|
180
|
+
if (!prior) return plan;
|
|
181
|
+
for (const v of model.items.flatMap((i) => i.variants)) {
|
|
182
|
+
if (v.mode !== 'manual') continue;
|
|
183
|
+
const before = prior.rows.get(v.ref);
|
|
184
|
+
if (!before || isUntouched(before.cells)) continue;
|
|
185
|
+
const stamped = prior.stamps?.get(v.ref);
|
|
186
|
+
// Only a recorded result is a claim about the procedure. A row with no result (a
|
|
187
|
+
// blocker note, or one already reset) is carried as-is, so a second procedure
|
|
188
|
+
// change does not stack a second marker on it.
|
|
189
|
+
const hasResult = !['', 'Pending'].includes(cellText(before.cells[0].value));
|
|
190
|
+
plan.set(v.ref, stamped !== undefined && stamped !== v.fingerprint && hasResult ? 'reset' : 'carry');
|
|
191
|
+
}
|
|
192
|
+
return plan;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A typed Result → its state word, case-insensitively (Excel's COUNTIF ignores case too). */
|
|
196
|
+
const STATE_BY_LOWER = new Map<string, VariantState>(
|
|
197
|
+
(['Passed', 'Failed', 'Blocked', 'Pending', 'N/A'] as VariantState[]).map((s) => [s.toLowerCase(), s]),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Put the carried Manual results into the MODEL, before anything is rendered or counted.
|
|
202
|
+
*
|
|
203
|
+
* The workbook's totals, Overview and group roll-ups are formulas with a cached result computed
|
|
204
|
+
* from the model, and the CSV and the CLI summary read the model too. Carrying only the cells
|
|
205
|
+
* after rendering left all of them counting the recorded Manual results as Pending: a preview
|
|
206
|
+
* of the file, and the CLI, reported "5 Pending" for cases the tester had run (#891).
|
|
207
|
+
* A reset row counts as Pending, as its cell reads. A Result that is not a state word (a tester's
|
|
208
|
+
* "OK") counts as Pending too; the cell keeps the text, and its date still reaches the CSV.
|
|
209
|
+
* Idempotent: every Manual variant's carried fields are cleared first, so the result depends only
|
|
210
|
+
* on `prior`.
|
|
211
|
+
*/
|
|
212
|
+
export function applyManualCarry(model: MatrixModel, prior: PriorManualResults | null): void {
|
|
213
|
+
const plan = planManualCarry(model, prior);
|
|
214
|
+
for (const item of model.items) {
|
|
215
|
+
let touched = false;
|
|
216
|
+
for (const v of item.variants) {
|
|
217
|
+
if (v.mode !== 'manual') continue;
|
|
218
|
+
if (v.manualState !== undefined || v.manualDate !== undefined) touched = true;
|
|
219
|
+
delete v.manualState;
|
|
220
|
+
delete v.manualDate;
|
|
221
|
+
const decision = plan.get(v.ref);
|
|
222
|
+
if (!decision) continue;
|
|
223
|
+
touched = true;
|
|
224
|
+
if (decision === 'reset') { v.manualState = 'Pending'; continue; }
|
|
225
|
+
const [result, date] = prior!.rows.get(v.ref)!.cells.map((c) => cellText(c.value));
|
|
226
|
+
if (date) v.manualDate = date;
|
|
227
|
+
v.manualState = STATE_BY_LOWER.get(result.toLowerCase()) ?? 'Pending';
|
|
228
|
+
}
|
|
229
|
+
if (touched) {
|
|
230
|
+
const { result, counts } = rollUp(item.variants);
|
|
231
|
+
item.result = result;
|
|
232
|
+
item.resultCounts = counts;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
172
237
|
/**
|
|
173
238
|
* Carry `prior` into the freshly rendered `wb`, then stamp every manual variant's fingerprint.
|
|
174
239
|
* Only the Manual sheet's variant sub-rows are touched.
|
|
@@ -178,6 +243,7 @@ export function carryManualResults(
|
|
|
178
243
|
model: MatrixModel,
|
|
179
244
|
prior: PriorManualResults | null,
|
|
180
245
|
): ManualCarryReport {
|
|
246
|
+
const plan = planManualCarry(model, prior);
|
|
181
247
|
const report: ManualCarryReport = { carried: 0, unchecked: 0, reset: [], notPlaced: [], duplicates: prior?.duplicates ?? [] };
|
|
182
248
|
const fingerprints = new Map(
|
|
183
249
|
model.items.flatMap((i) => i.variants).filter((v) => v.mode === 'manual').map((v) => [v.ref, v.fingerprint]),
|
|
@@ -190,14 +256,11 @@ export function carryManualResults(
|
|
|
190
256
|
const before = prior.rows.get(ref);
|
|
191
257
|
if (!before) continue;
|
|
192
258
|
placed.add(ref);
|
|
193
|
-
|
|
259
|
+
const decision = plan.get(ref);
|
|
260
|
+
if (!decision) continue;
|
|
194
261
|
|
|
195
262
|
const stamped = prior.stamps?.get(ref);
|
|
196
|
-
|
|
197
|
-
// blocker note, or one already reset) is carried as-is, so a second procedure
|
|
198
|
-
// change does not stack a second marker on it.
|
|
199
|
-
const hasResult = !['', 'Pending'].includes(cellText(before.cells[0].value));
|
|
200
|
-
if (stamped !== undefined && stamped !== fingerprints.get(ref) && hasResult) {
|
|
263
|
+
if (decision === 'reset') {
|
|
201
264
|
row.getCell(RESULT_COL).value = 'Pending';
|
|
202
265
|
row.getCell(CARRIED_COLS[NOTE_INDEX]).value = resetNote(before.cells);
|
|
203
266
|
report.reset.push(ref);
|
|
@@ -17,7 +17,8 @@ import { mergeFeatureAndSpec } from '../scenario-merger';
|
|
|
17
17
|
import { getPackageVersion } from '../package-info';
|
|
18
18
|
import { writeCsv } from '../csv-exporter';
|
|
19
19
|
import { deliverableXlsxPath, writeXlsx } from '../xlsx-exporter';
|
|
20
|
-
import { carryManualResults, ManualCarryReport, readPriorManualResults } from './carry-manual-results';
|
|
20
|
+
import { applyManualCarry, carryManualResults, ManualCarryReport, PriorManualResults, readPriorManualResults } from './carry-manual-results';
|
|
21
|
+
export { readPriorManualResults } from './carry-manual-results';
|
|
21
22
|
import { loadPerfConfig, perfVerdict } from '../../harness/perf';
|
|
22
23
|
import { loadDeliveryMap, writeDeliveryMap } from './map-loader';
|
|
23
24
|
import { mapContentFingerprint } from './fingerprint';
|
|
@@ -168,12 +169,21 @@ export async function writeMatrixDeliverables(
|
|
|
168
169
|
paths: MatrixTargetPaths,
|
|
169
170
|
model: MatrixModel,
|
|
170
171
|
format: MatrixFormat = 'xlsx',
|
|
172
|
+
/** The prior workbook's Manual results, when the caller already read them; read here if absent. */
|
|
173
|
+
priorRead?: PriorManualResults | null,
|
|
171
174
|
): Promise<{ csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport }> {
|
|
172
175
|
const out: { csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport } = {};
|
|
173
176
|
const xlsx = format === 'xlsx' || format === 'both';
|
|
174
177
|
// Read the workbook about to be overwritten BEFORE writing anything, so the tester's Manual
|
|
175
178
|
// results survive (#756) and a refusal leaves no half-written target behind.
|
|
176
|
-
|
|
179
|
+
// A CSV-only export overwrites no workbook, so an unreadable one is no reason to refuse it — but a
|
|
180
|
+
// readable one still holds the Manual results the CSV should show.
|
|
181
|
+
const prior = priorRead !== undefined ? priorRead
|
|
182
|
+
: xlsx ? await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit))
|
|
183
|
+
: await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit)).catch(() => null);
|
|
184
|
+
// Into the model first: the CSV, every cached total and roll-up, and the caller's summary
|
|
185
|
+
// all read it (#891).
|
|
186
|
+
applyManualCarry(model, prior);
|
|
177
187
|
if (format === 'csv' || format === 'both') {
|
|
178
188
|
out.csvPath = writeCsv(paths.cwd, paths.unit, renderMatrixCsv(model));
|
|
179
189
|
}
|
|
@@ -78,7 +78,7 @@ export function renderMatrixCsv(model: MatrixModel): string {
|
|
|
78
78
|
v.mode === 'manual' ? 'Manual' : 'Auto',
|
|
79
79
|
v.traces.join(' '),
|
|
80
80
|
variantState(v),
|
|
81
|
-
isoDate(v.result?.startTime),
|
|
81
|
+
v.manualDate ?? isoDate(v.result?.startTime),
|
|
82
82
|
// Redact BEFORE clipping — a clip boundary must never split a secret in
|
|
83
83
|
// a way that lets its prefix through. Same denylist as the Note column.
|
|
84
84
|
v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
|
|
@@ -74,6 +74,9 @@ export function projectItems(model: MatrixModel, mode: 'auto' | 'manual'): Deliv
|
|
|
74
74
|
|
|
75
75
|
export function renderMatrixXlsx(model: MatrixModel, sungenVersion?: string): ExcelJS.Workbook {
|
|
76
76
|
const wb = new ExcelJS.Workbook();
|
|
77
|
+
// Every total and roll-up is a formula with a cached result. The cache is what a preview
|
|
78
|
+
// (GitHub, Drive, QuickLook) shows; a spreadsheet app recomputes on open as well (#891).
|
|
79
|
+
wb.calcProperties.fullCalcOnLoad = true;
|
|
77
80
|
const sheets: SheetProjection[] = [
|
|
78
81
|
{ sheetName: 'Auto', items: projectItems(model, 'auto') },
|
|
79
82
|
{ sheetName: 'Manual', items: projectItems(model, 'manual') },
|
|
@@ -147,6 +147,11 @@ export interface CoverageVariant {
|
|
|
147
147
|
fingerprint: string;
|
|
148
148
|
/** Execution result, when the run report has one for this variant. */
|
|
149
149
|
result?: PlaywrightResult;
|
|
150
|
+
/** A Manual variant's result as the tester recorded it in the previous workbook, carried
|
|
151
|
+
* BEFORE rendering so every total, roll-up, the CSV and the CLI count it (#891). */
|
|
152
|
+
manualState?: VariantState;
|
|
153
|
+
/** The Executed Date the tester recorded with it (yyyy-mm-dd, or as typed). */
|
|
154
|
+
manualDate?: string;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run' | 'na';
|