@sun-asterisk/sungen 3.2.18 → 3.2.19
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/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +4 -1
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +4 -2
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +10 -1
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +19 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +4 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/dashboard/html-renderer.d.ts.map +1 -1
- package/dist/dashboard/html-renderer.js +7 -2
- package/dist/dashboard/html-renderer.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +3 -0
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/types.d.ts +4 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/api-environment.d.ts.map +1 -1
- package/dist/exporters/api-environment.js +4 -24
- package/dist/exporters/api-environment.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +6 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +11 -4
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +40 -9
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/env-qa.d.ts +13 -0
- package/dist/exporters/env-qa.d.ts.map +1 -0
- package/dist/exporters/env-qa.js +82 -0
- package/dist/exporters/env-qa.js.map +1 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +16 -33
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +57 -20
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +6 -5
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/gates.d.ts.map +1 -1
- package/dist/exporters/matrix/gates.js +3 -2
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/step-buckets.d.ts +13 -0
- package/dist/exporters/matrix/step-buckets.d.ts.map +1 -0
- package/dist/exporters/matrix/step-buckets.js +7 -0
- package/dist/exporters/matrix/step-buckets.js.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts +43 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.js +143 -0
- package/dist/exporters/qa-env-secret-denylist.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts +20 -13
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +55 -30
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/step-formatter.d.ts +42 -33
- package/dist/exporters/step-formatter.d.ts.map +1 -1
- package/dist/exporters/step-formatter.js +68 -68
- package/dist/exporters/step-formatter.js.map +1 -1
- package/dist/exporters/types.d.ts +9 -8
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +70 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +7 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +6 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
- package/dist/generators/test-generator/recipe-expander.js +1 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +17 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +108 -17
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +7 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +8 -4
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
- 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 +8 -7
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +2 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +1 -55
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +14 -9
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +1 -3
- package/dist/harness/depth-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +9 -7
- 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 +4 -3
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/manifest.js +2 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts +12 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +28 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +52 -30
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +3 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +14 -10
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +15 -4
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/trace.js +1 -1
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/unit-paths.d.ts +16 -0
- package/dist/harness/unit-paths.d.ts.map +1 -1
- package/dist/harness/unit-paths.js +39 -2
- package/dist/harness/unit-paths.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +8 -2
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/dist/orchestrator/templates/playwright.config.ts +33 -2
- package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-mock.js +137 -0
- package/dist/orchestrator/templates/specs-mock.js.map +1 -0
- package/dist/orchestrator/templates/specs-mock.ts +179 -0
- package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
- package/dist/orchestrator/test-data-guide.js +4 -0
- package/dist/orchestrator/test-data-guide.js.map +1 -1
- package/package.json +3 -3
- package/src/capabilities/discover.ts +4 -1
- package/src/cli/commands/add.ts +4 -2
- package/src/cli/commands/dashboard.ts +10 -1
- package/src/cli/commands/delivery.ts +19 -1
- package/src/cli/commands/generate.ts +4 -1
- package/src/dashboard/html-renderer.ts +8 -2
- package/src/dashboard/snapshot-builder.ts +3 -0
- package/src/dashboard/types.ts +4 -0
- package/src/exporters/api-environment.ts +3 -20
- package/src/exporters/api-testcase-formatter.ts +8 -2
- package/src/exporters/csv-exporter.ts +47 -8
- package/src/exporters/env-qa.ts +41 -0
- package/src/exporters/feature-parser.ts +17 -38
- package/src/exporters/json-exporter.ts +71 -24
- package/src/exporters/matrix/build.ts +6 -5
- package/src/exporters/matrix/gates.ts +3 -2
- package/src/exporters/matrix/step-buckets.ts +15 -0
- package/src/exporters/qa-env-secret-denylist.ts +118 -0
- package/src/exporters/scenario-merger.ts +80 -42
- package/src/exporters/step-formatter.ts +67 -66
- package/src/exporters/types.ts +9 -8
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/src/generators/test-generator/code-generator.ts +82 -7
- package/src/generators/test-generator/diagnostics.ts +7 -1
- package/src/generators/test-generator/recipe-expander.ts +2 -2
- package/src/generators/test-generator/step-mapper.ts +106 -15
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/generators/test-generator/utils/data-resolver.ts +8 -5
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
- package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
- package/src/harness/audit.ts +10 -9
- package/src/harness/capability-plan.ts +2 -2
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/challenge.ts +2 -3
- package/src/harness/data-driven-lint.ts +13 -9
- package/src/harness/depth-lint.ts +2 -4
- package/src/harness/flow-check.ts +9 -7
- package/src/harness/flow-plan.ts +4 -3
- package/src/harness/manifest.ts +3 -3
- package/src/harness/parse.ts +28 -1
- package/src/harness/script-check.ts +52 -32
- package/src/harness/secret-scan.ts +11 -7
- package/src/harness/sensors.ts +15 -4
- package/src/harness/trace.ts +2 -2
- package/src/harness/unit-paths.ts +39 -2
- package/src/index.ts +1 -0
- package/src/orchestrator/screen-manager.ts +7 -2
- package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/src/orchestrator/templates/playwright.config.ts +33 -2
- package/src/orchestrator/templates/specs-mock.ts +179 -0
- package/src/orchestrator/test-data-guide.ts +4 -0
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
mapVpToCategory2,
|
|
16
16
|
splitVpAndName,
|
|
17
17
|
} from './feature-parser';
|
|
18
|
-
import {
|
|
18
|
+
import { formatPrecondition, groupExpectedByAnchor, keepActor, stripActor } from './step-formatter';
|
|
19
19
|
import { formatTestData, substituteTestDataVars } from './test-data-resolver';
|
|
20
20
|
import {
|
|
21
21
|
formatExecutedDate,
|
|
@@ -26,6 +26,7 @@ import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult, ScreenSummary, Test
|
|
|
26
26
|
import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
27
27
|
import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
|
|
28
28
|
import { formatVariantRow, resolveResultVariants } from './result-variants';
|
|
29
|
+
import { collectQaEnvSecretDenylist, warnIfRedactionIncomplete } from './qa-env-secret-denylist';
|
|
29
30
|
|
|
30
31
|
export interface BuildCsvInput {
|
|
31
32
|
screen: string;
|
|
@@ -65,6 +66,10 @@ export interface BuildCsvInput {
|
|
|
65
66
|
export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
66
67
|
const rows: TestCaseRow[] = [];
|
|
67
68
|
let fallbackIndex = 1;
|
|
69
|
+
// Runtime failure messages embed RESOLVED ${QA_*} credentials (Expected:/Received:
|
|
70
|
+
// carry the substituted value) and are lifted into the committed Note column below —
|
|
71
|
+
// build the value denylist once per unit so every Note gets scrubbed.
|
|
72
|
+
const qaEnvSecrets = collectQaEnvSecretDenylist(input.testData);
|
|
68
73
|
|
|
69
74
|
for (const m of input.merged) {
|
|
70
75
|
// Data-driven (@cases): expand to one CSV row per executed dataset input (Playwright titles
|
|
@@ -83,9 +88,9 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
83
88
|
const testcaseType = extractTestcaseType(m.feature.tags);
|
|
84
89
|
const authRole = extractAuthRole(m.feature.tags);
|
|
85
90
|
|
|
86
|
-
// Pre-condition
|
|
87
|
-
//
|
|
88
|
-
|
|
91
|
+
// Pre-condition = auth line + feature Background steps (kept out of Steps
|
|
92
|
+
// so the numbering matches the .feature scenario). Steps section = @extend
|
|
93
|
+
// base (entire) + own pre-Then. Expected = own's Then-onwards only.
|
|
89
94
|
const localize = (text: string): string => {
|
|
90
95
|
// 1. resolve [Reference] selector keys → their label/value; 2. resolve {{var}} test-data
|
|
91
96
|
// placeholders → concrete values, so Steps / Expected results show what QA actually verifies
|
|
@@ -98,13 +103,29 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
98
103
|
};
|
|
99
104
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
100
105
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
101
|
-
const
|
|
102
|
-
|
|
106
|
+
const precondition = formatPrecondition(
|
|
107
|
+
authRole,
|
|
108
|
+
(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
|
|
109
|
+
);
|
|
110
|
+
const stepLines = m.resolvedSteps.map(formatStep).filter(Boolean);
|
|
111
|
+
// Scenarios whose assertions verify different moments render Expected as
|
|
112
|
+
// "— after step N" groups (shared rule in groupExpectedByAnchor); the
|
|
113
|
+
// common all-after-last-step shape keeps the plain checklist.
|
|
114
|
+
const { lines: expectedLines, grouped } = groupExpectedByAnchor(
|
|
115
|
+
m.resolvedExpected
|
|
116
|
+
.map((s) => ({ text: formatStep(s), anchorStep: s.anchorStep }))
|
|
117
|
+
.filter((e) => e.text),
|
|
118
|
+
m.resolvedAnchorActionCount ?? 0
|
|
119
|
+
);
|
|
120
|
+
// When group headers reference step numbers, the Steps cell must show
|
|
121
|
+
// them — number even a single line.
|
|
103
122
|
let steps =
|
|
104
123
|
stepLines.length === 0 ? '' :
|
|
105
|
-
stepLines.length === 1 ? stepLines[0] :
|
|
124
|
+
stepLines.length === 1 && !grouped ? stepLines[0] :
|
|
106
125
|
stepLines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
126
|
+
// Grouped cells carry their own per-group numbering — join verbatim.
|
|
107
127
|
let expectedResults =
|
|
128
|
+
grouped ? expectedLines.join('\n') :
|
|
108
129
|
expectedLines.length === 0 ? '' :
|
|
109
130
|
expectedLines.length === 1 ? expectedLines[0] :
|
|
110
131
|
expectedLines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
@@ -138,6 +159,19 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
138
159
|
const variantRow = formatVariantRow(variant, '\n');
|
|
139
160
|
if (variantRow) testData = testData ? `${testData}\n${variantRow}` : variantRow;
|
|
140
161
|
|
|
162
|
+
// Runtime-secret pass over EVERY kind's content cells (screen/flow included, not
|
|
163
|
+
// only api): a sensitive ${QA_*} VALUE that reached cell content — a hardcoded
|
|
164
|
+
// copy in test-data, a @cases dataset row, a locale substitution — is erased.
|
|
165
|
+
// Value-based only: fake demo values ("password123"), error-message strings
|
|
166
|
+
// ("Password is required") and element names ([Password]) never match and stay
|
|
167
|
+
// readable. The key-name-based pass above stays API-only by design — applied to
|
|
168
|
+
// screen cells it would mangle exactly those strings.
|
|
169
|
+
if (qaEnvSecrets.values.length > 0) {
|
|
170
|
+
steps = redactSecrets(steps, qaEnvSecrets.values);
|
|
171
|
+
expectedResults = redactSecrets(expectedResults, qaEnvSecrets.values);
|
|
172
|
+
testData = redactSecrets(testData, qaEnvSecrets.values);
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
// Status for this (possibly per-input) row — resolved into `variant` above.
|
|
142
176
|
const result: PlaywrightResult | undefined = variant.result;
|
|
143
177
|
|
|
@@ -156,7 +190,7 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
156
190
|
// first, those already-passed/failed rows were mislabelled N/A ("not compiled").
|
|
157
191
|
testResult = statusToTestResult(result.status);
|
|
158
192
|
executedDate = formatExecutedDate(result.startTime);
|
|
159
|
-
note = formatNote(result, expectedResults);
|
|
193
|
+
note = redactSecrets(formatNote(result, expectedResults), qaEnvSecrets.values);
|
|
160
194
|
environment = formatEnv(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
|
|
161
195
|
executor = input.env.executor;
|
|
162
196
|
} else if (!m.spec) {
|
|
@@ -202,6 +236,11 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
238
|
|
|
239
|
+
// A sensitive ${QA_*} ref that resolved to nothing here can still appear RESOLVED in
|
|
240
|
+
// failure text (the run may have happened in another shell, e.g. CI artifacts exported
|
|
241
|
+
// locally) — never export silently unredacted.
|
|
242
|
+
warnIfRedactionIncomplete(qaEnvSecrets, rows.some((r) => r.testResult === 'Failed'), input.screen);
|
|
243
|
+
|
|
205
244
|
return rows;
|
|
206
245
|
}
|
|
207
246
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .env.qa readers shared by the CLI surfaces (delivery, dashboard, screen capture)
|
|
3
|
+
* — the same precedence the RUNTIME uses (specs-test-data / specs-api loadEnvQa):
|
|
4
|
+
*
|
|
5
|
+
* shell env > .env.qa.<SUNGEN_ENV> > .env.qa
|
|
6
|
+
*
|
|
7
|
+
* Built as a LOCAL map: unlike the runtime's loadEnvQa, the CLI must never
|
|
8
|
+
* mutate process.env (it keeps running after the lookup).
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
|
|
13
|
+
/** Read KEY=value lines from one dotenv-style file (quotes stripped, comments ignored). */
|
|
14
|
+
export function readEnvFile(file: string, into: Record<string, string>): void {
|
|
15
|
+
if (!fs.existsSync(file)) return;
|
|
16
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
17
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
18
|
+
if (m && into[m[1]] === undefined) into[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Merged env view for a project dir, with the runtime's precedence. */
|
|
23
|
+
export function buildEnvMap(cwd: string): Record<string, string> {
|
|
24
|
+
const map: Record<string, string> = {};
|
|
25
|
+
for (const name of [`.env.qa.${process.env.SUNGEN_ENV || ''}`, '.env.qa']) {
|
|
26
|
+
if (!name.endsWith('.')) readEnvFile(path.join(cwd, name), map);
|
|
27
|
+
}
|
|
28
|
+
for (const [k, v] of Object.entries(process.env)) if (v !== undefined) map[k] = v;
|
|
29
|
+
return map;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The web baseURL a run under the CURRENT SUNGEN_ENV actually uses.
|
|
34
|
+
* `SUNGEN_BASE_URL` (shell > .env.qa.<SUNGEN_ENV> > .env.qa) overrides the static
|
|
35
|
+
* value written in playwright.config.ts — mirroring the scaffolded config's own
|
|
36
|
+
* resolveBaseUrl(), so the Test Environment column and screenshot capture report
|
|
37
|
+
* the host the tests really hit.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveWebBaseUrl(cwd: string, staticBaseUrl: string): string {
|
|
40
|
+
return buildEnvMap(cwd).SUNGEN_BASE_URL || staticBaseUrl;
|
|
41
|
+
}
|
|
@@ -45,18 +45,11 @@ function extractReferencedVars(scenario: ParsedScenario, includeComments = false
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Classify each step into Given / When / Then bucket based on
|
|
49
|
-
* explicit keyword. "And"
|
|
48
|
+
* Classify each step into its Given / When / Then bucket based on the
|
|
49
|
+
* preceding explicit keyword. "And" / "But" inherit the previous bucket.
|
|
50
|
+
* Chronological order is preserved.
|
|
50
51
|
*/
|
|
51
|
-
function classifySteps(scenario: ParsedScenario): {
|
|
52
|
-
given: string[];
|
|
53
|
-
when: string[];
|
|
54
|
-
then: string[];
|
|
55
|
-
ordered: OrderedStep[];
|
|
56
|
-
} {
|
|
57
|
-
const given: string[] = [];
|
|
58
|
-
const when: string[] = [];
|
|
59
|
-
const then: string[] = [];
|
|
52
|
+
function classifySteps(scenario: ParsedScenario): OrderedStep[] {
|
|
60
53
|
const ordered: OrderedStep[] = [];
|
|
61
54
|
let currentBucket: 'given' | 'when' | 'then' = 'given';
|
|
62
55
|
|
|
@@ -67,14 +60,10 @@ function classifySteps(scenario: ParsedScenario): {
|
|
|
67
60
|
else if (keyword === 'Then') currentBucket = 'then';
|
|
68
61
|
// And / But → inherit currentBucket
|
|
69
62
|
|
|
70
|
-
if (currentBucket === 'given') given.push(step.text);
|
|
71
|
-
else if (currentBucket === 'when') when.push(step.text);
|
|
72
|
-
else then.push(step.text);
|
|
73
|
-
|
|
74
63
|
ordered.push({ text: step.text, bucket: currentBucket });
|
|
75
64
|
}
|
|
76
65
|
|
|
77
|
-
return
|
|
66
|
+
return ordered;
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
/**
|
|
@@ -86,35 +75,25 @@ export function parseFeatureMetadata(featureFilePath: string): FeatureMetadata {
|
|
|
86
75
|
|
|
87
76
|
// Background block — classify once and surface on FeatureMetadata so the
|
|
88
77
|
// merger can prepend it to each runnable scenario without re-parsing.
|
|
89
|
-
const
|
|
78
|
+
const backgroundOrderedSteps = parsed.background
|
|
90
79
|
? classifySteps(parsed.background)
|
|
91
|
-
:
|
|
92
|
-
|
|
93
|
-
const scenarios: ScenarioMetadata[] = parsed.scenarios.map((sc) => {
|
|
94
|
-
const { given, when, then, ordered } = classifySteps(sc);
|
|
80
|
+
: [];
|
|
95
81
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
orderedSteps: ordered,
|
|
106
|
-
comments: sc.comments,
|
|
107
|
-
};
|
|
108
|
-
});
|
|
82
|
+
const scenarios: ScenarioMetadata[] = parsed.scenarios.map((sc) => ({
|
|
83
|
+
name: sc.name,
|
|
84
|
+
tags: [...parsed.tags, ...sc.tags],
|
|
85
|
+
stepsName: sc.stepsName,
|
|
86
|
+
extendsName: sc.extendsName,
|
|
87
|
+
referencedVars: extractReferencedVars(sc, extractTestcaseType([...parsed.tags, ...sc.tags]) === 'Manual'),
|
|
88
|
+
orderedSteps: classifySteps(sc),
|
|
89
|
+
comments: sc.comments,
|
|
90
|
+
}));
|
|
109
91
|
|
|
110
92
|
return {
|
|
111
93
|
featureName: parsed.name,
|
|
112
94
|
featurePath: parsed.path,
|
|
113
95
|
featureTags: parsed.tags,
|
|
114
|
-
|
|
115
|
-
backgroundWhenSteps: bg.when,
|
|
116
|
-
backgroundThenSteps: bg.then,
|
|
117
|
-
backgroundOrderedSteps: bg.ordered,
|
|
96
|
+
backgroundOrderedSteps,
|
|
118
97
|
scenarios,
|
|
119
98
|
};
|
|
120
99
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
mapVpToCategory2,
|
|
15
15
|
splitVpAndName,
|
|
16
16
|
} from './feature-parser';
|
|
17
|
-
import { formatPrecondition, keepActor, stripActor } from './step-formatter';
|
|
17
|
+
import { formatPrecondition, groupExpectedByAnchor, keepActor, stripActor } from './step-formatter';
|
|
18
18
|
import { formatTestData, substituteTestDataVars } from './test-data-resolver';
|
|
19
19
|
import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
20
20
|
import {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult } from './types';
|
|
25
25
|
import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
|
|
26
26
|
import { formatVariantRow, resolveResultVariants } from './result-variants';
|
|
27
|
+
import { collectQaEnvSecretDenylist, warnIfRedactionIncomplete } from './qa-env-secret-denylist';
|
|
27
28
|
import {
|
|
28
29
|
ScenarioSnapshot,
|
|
29
30
|
ScreenSnapshot,
|
|
@@ -71,24 +72,56 @@ export interface BuildScreenSnapshotInput {
|
|
|
71
72
|
export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnapshot {
|
|
72
73
|
const scenarios: ScenarioSnapshot[] = [];
|
|
73
74
|
let fallbackIndex = 1;
|
|
75
|
+
// Runtime failure messages embed RESOLVED ${QA_*} credentials, and `errorMessage`
|
|
76
|
+
// feeds the dashboard snapshot + its committed history (never deleted by design) —
|
|
77
|
+
// build the value denylist once per unit so every errorMessage gets scrubbed.
|
|
78
|
+
const qaEnvSecrets = collectQaEnvSecretDenylist(input.testData);
|
|
74
79
|
|
|
75
80
|
for (const m of input.merged) {
|
|
76
81
|
const priority = extractPriority(m.feature.tags) as ScenarioSnapshot['priority'];
|
|
77
82
|
const baseType = extractTestcaseType(m.feature.tags);
|
|
78
83
|
const authRole = extractAuthRole(m.feature.tags);
|
|
79
84
|
|
|
80
|
-
// Pre-condition
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// Pre-condition = auth line + feature Background steps (kept out of Steps
|
|
86
|
+
// so numbering matches the .feature scenario — parity with csv-exporter).
|
|
87
|
+
// Steps section = @extend base (all of it, including its Then) + own
|
|
88
|
+
// pre-Then. Expected = own's Then-onwards only. Given → keepActor (state),
|
|
89
|
+
// When/Then → stripActor.
|
|
90
|
+
const localize = (text: string): string => {
|
|
91
|
+
// Same two-step resolution as csv-exporter's localize: 1. resolve [Reference]
|
|
92
|
+
// selector keys → their label/value; 2. resolve {{var}} test-data placeholders →
|
|
93
|
+
// concrete values, so the dashboard shows the identical Steps / Expected text as
|
|
94
|
+
// the delivery sheet. An unknown {{var}} is left literal.
|
|
95
|
+
const withKeys = input.selectorKeyMap
|
|
86
96
|
? substituteSelectorKeysInStep(text, input.selectorKeyMap, input.testData)
|
|
87
97
|
: text;
|
|
98
|
+
return substituteTestDataVars(withKeys, input.testData);
|
|
99
|
+
};
|
|
88
100
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
89
101
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
102
|
+
const precondition = formatPrecondition(
|
|
103
|
+
authRole,
|
|
104
|
+
(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
|
|
105
|
+
);
|
|
90
106
|
let steps = m.resolvedSteps.map(formatStep).filter(Boolean);
|
|
91
|
-
|
|
107
|
+
// Same anchor rule as csv-exporter (shared groupExpectedByAnchor) so the
|
|
108
|
+
// dashboard shows the identical Expected text as the delivery sheet.
|
|
109
|
+
const anchorResult = groupExpectedByAnchor(
|
|
110
|
+
m.resolvedExpected
|
|
111
|
+
.map((s) => ({ text: formatStep(s), anchorStep: s.anchorStep }))
|
|
112
|
+
.filter((e) => e.text),
|
|
113
|
+
m.resolvedAnchorActionCount ?? 0
|
|
114
|
+
);
|
|
115
|
+
// Grouped cells carry their own headers + per-group numbering: store as
|
|
116
|
+
// ONE entry so the dashboard XLSX adapter (formatList: 1 entry → raw)
|
|
117
|
+
// reproduces the delivery cell verbatim instead of re-numbering lines.
|
|
118
|
+
let expectedResults = anchorResult.grouped
|
|
119
|
+
? [anchorResult.lines.join('\n')]
|
|
120
|
+
: anchorResult.lines;
|
|
121
|
+
// Group headers reference Steps numbering; the adapter leaves a
|
|
122
|
+
// single-entry list unnumbered, so bake the "1. " in when a lone action
|
|
123
|
+
// is referenced by headers.
|
|
124
|
+
if (anchorResult.grouped && steps.length === 1) steps = [`1. ${steps[0]}`];
|
|
92
125
|
|
|
93
126
|
// API test cases: mirror csv-exporter's request/response rendering so the
|
|
94
127
|
// dashboard (UI + XLSX export) shows the same Steps / Expected results as
|
|
@@ -97,20 +130,15 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
97
130
|
// adapter's formatList() (1 entry → raw) reproduces the delivery cell
|
|
98
131
|
// byte-for-byte, and the UI renders the block with pre-wrap.
|
|
99
132
|
if (input.kind === 'api' && input.apiCatalog && hasApiCalls(m.feature.tags)) {
|
|
100
|
-
// Delivery cells substitute {{var}} test-data refs (csv-exporter's localize);
|
|
101
|
-
// scope that to the API branch so non-API snapshot behavior is untouched.
|
|
102
|
-
const localizeApi = (text: string): string => substituteTestDataVars(localize(text), input.testData);
|
|
103
|
-
const formatStepApi = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
104
|
-
localizeApi(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
105
133
|
const joinCell = (lines: string[]): string =>
|
|
106
134
|
lines.length === 0 ? '' :
|
|
107
135
|
lines.length === 1 ? lines[0] :
|
|
108
136
|
lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
109
|
-
let stepsCell = joinCell(m.resolvedSteps.map(
|
|
110
|
-
let expectedCell = joinCell(m.resolvedExpected.map(
|
|
111
|
-
const apiRequest = formatApiRequest(m.feature.tags, input.apiCatalog,
|
|
137
|
+
let stepsCell = joinCell(m.resolvedSteps.map(formatStep).filter(Boolean));
|
|
138
|
+
let expectedCell = joinCell(m.resolvedExpected.map(formatStep).filter(Boolean));
|
|
139
|
+
const apiRequest = formatApiRequest(m.feature.tags, input.apiCatalog, localize);
|
|
112
140
|
if (apiRequest) stepsCell = stepsCell ? `${stepsCell}\n\n${apiRequest}` : apiRequest;
|
|
113
|
-
const apiExpected = formatApiExpected(m.resolvedExpected, m.feature.tags,
|
|
141
|
+
const apiExpected = formatApiExpected(m.resolvedExpected, m.feature.tags, localize);
|
|
114
142
|
if (apiExpected) expectedCell = apiExpected;
|
|
115
143
|
// Same value-based redaction backstop as the CSV path.
|
|
116
144
|
const secrets = collectSecretValues(input.testData);
|
|
@@ -121,9 +149,19 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
121
149
|
steps = stepsCell ? [stepsCell] : [];
|
|
122
150
|
expectedResults = expectedCell ? [expectedCell] : [];
|
|
123
151
|
}
|
|
152
|
+
// Runtime-secret pass over EVERY kind's content cells (parity with csv-exporter):
|
|
153
|
+
// value-based only, so demo values / error-message strings / element names stay
|
|
154
|
+
// readable — only a resolved sensitive ${QA_*} value that reached cell content is erased.
|
|
155
|
+
if (qaEnvSecrets.values.length > 0) {
|
|
156
|
+
steps = steps.map((s) => redactSecrets(s, qaEnvSecrets.values));
|
|
157
|
+
expectedResults = expectedResults.map((s) => redactSecrets(s, qaEnvSecrets.values));
|
|
158
|
+
}
|
|
124
159
|
// Dashboard modal uses whitespace-pre-wrap — pass '\n' to render one
|
|
125
160
|
// key/value per line instead of the CSV-friendly "; " join.
|
|
126
|
-
const baseTestDataStr =
|
|
161
|
+
const baseTestDataStr = redactSecrets(
|
|
162
|
+
formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n'),
|
|
163
|
+
qaEnvSecrets.values,
|
|
164
|
+
);
|
|
127
165
|
|
|
128
166
|
// @cases scenarios expand to one snapshot entry per executed dataset row — and manual
|
|
129
167
|
// @cases split per DATASET row (shared with the CSV exporter via resolveResultVariants),
|
|
@@ -132,7 +170,8 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
132
170
|
for (const variant of variants) {
|
|
133
171
|
// Per-line Test Data: shared vars + THIS variant's dataset row (parity with csv-exporter).
|
|
134
172
|
let testDataStr = baseTestDataStr;
|
|
135
|
-
|
|
173
|
+
// A @cases dataset row can carry the same values — redact it like the shared part.
|
|
174
|
+
const variantRow = redactSecrets(formatVariantRow(variant, '\n'), qaEnvSecrets.values);
|
|
136
175
|
if (variantRow) testDataStr = testDataStr ? `${testDataStr}\n${variantRow}` : variantRow;
|
|
137
176
|
const displayName = `${m.feature.name}${variant.nameSuffix}`;
|
|
138
177
|
const { vpId, category1 } = splitVpAndName(displayName);
|
|
@@ -164,11 +203,14 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
164
203
|
// mid-selector. Drop the stack trace (frames start with "at "), collapse the remaining
|
|
165
204
|
// newlines (incl. the Expected/Received lines) to spaces, and strip a leading "Error:"
|
|
166
205
|
// so consumers (dashboard / CSV `Note`) can prepend their own label.
|
|
167
|
-
errorMessage =
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
206
|
+
errorMessage = redactSecrets(
|
|
207
|
+
String(result.error)
|
|
208
|
+
.split(/\n\s*at\s/)[0]
|
|
209
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
210
|
+
.trim()
|
|
211
|
+
.replace(/^Error:\s*/i, ''),
|
|
212
|
+
qaEnvSecrets.values,
|
|
213
|
+
);
|
|
172
214
|
}
|
|
173
215
|
if (result.tracePath) tracePath = result.tracePath;
|
|
174
216
|
testEnvironment = formatTestEnvironment(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
|
|
@@ -208,6 +250,11 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
208
250
|
}
|
|
209
251
|
}
|
|
210
252
|
|
|
253
|
+
// A sensitive ${QA_*} ref that resolved to nothing here can still appear RESOLVED in
|
|
254
|
+
// failure text (the run may have happened in another shell, e.g. CI artifacts exported
|
|
255
|
+
// locally) — never export silently unredacted.
|
|
256
|
+
warnIfRedactionIncomplete(qaEnvSecrets, scenarios.some((s) => s.status === 'Failed'), input.screen);
|
|
257
|
+
|
|
211
258
|
return {
|
|
212
259
|
name: input.screen,
|
|
213
260
|
isFlow: input.isFlow,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { FeatureMetadata, PlaywrightResult } from '../types';
|
|
9
|
+
import { bucketTexts } from './step-buckets';
|
|
9
10
|
import { MergedScenario } from '../scenario-merger';
|
|
10
11
|
import {
|
|
11
12
|
extractAuthRole,
|
|
@@ -126,9 +127,9 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
126
127
|
: null;
|
|
127
128
|
|
|
128
129
|
// Raw (pre-wording) step texts — these feed shapes + Gate C, never the cells.
|
|
129
|
-
const rawTrigger = manual ? manual.actions : m.feature.
|
|
130
|
+
const rawTrigger = manual ? manual.actions : bucketTexts(m.feature.orderedSteps, 'when');
|
|
130
131
|
const rawOracle = manual
|
|
131
|
-
? (manual.expected.length > 0 ? manual.expected : m.feature.
|
|
132
|
+
? (manual.expected.length > 0 ? manual.expected : bucketTexts(m.feature.orderedSteps, 'then'))
|
|
132
133
|
: m.resolvedExpected.filter((s) => s.bucket === 'then').map((s) => s.text);
|
|
133
134
|
|
|
134
135
|
// Values the scenario itself produces during the run — captured from the page
|
|
@@ -142,7 +143,7 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
142
143
|
const preconditionProfile = [
|
|
143
144
|
authRole ?? '-',
|
|
144
145
|
m.feature.extendsName ?? '-',
|
|
145
|
-
...m.feature.
|
|
146
|
+
...bucketTexts(m.feature.orderedSteps, 'given').map(normalizeShape),
|
|
146
147
|
...(manual?.preconditions ?? []).map(normalizeShape),
|
|
147
148
|
].join(' | ');
|
|
148
149
|
// Display precondition = auth state + Background Given (shared start state) +
|
|
@@ -150,8 +151,8 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
150
151
|
// scenario often repeats the Background navigation as its own Given).
|
|
151
152
|
const precondition = Array.from(new Set([
|
|
152
153
|
...(authRole ? [authRole === 'no-auth' ? 'The user is signed out.' : `The user is signed in as ${authRole}.`] : []),
|
|
153
|
-
...inputs.feature.
|
|
154
|
-
...m.feature.
|
|
154
|
+
...bucketTexts(inputs.feature.backgroundOrderedSteps, 'given').map(renderPrecondition),
|
|
155
|
+
...bucketTexts(m.feature.orderedSteps, 'given').map(renderPrecondition),
|
|
155
156
|
...(manual?.preconditions ?? []).map((t) => renderSetupInstruction(t)),
|
|
156
157
|
]));
|
|
157
158
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { splitVpAndName, extractTestcaseType } from '../feature-parser';
|
|
15
15
|
import { mapContentFingerprint } from './fingerprint';
|
|
16
|
+
import { bucketTexts } from './step-buckets';
|
|
16
17
|
import { CoverageVariant, MatrixFinding, MAX_VARIANTS_PER_ITEM } from './types';
|
|
17
18
|
import { expandMapRef, BuildInputs } from './build';
|
|
18
19
|
|
|
@@ -73,7 +74,7 @@ function gateKCategory(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
73
74
|
function gateASource(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
74
75
|
const { feature, merged } = ctx.inputs;
|
|
75
76
|
|
|
76
|
-
if (feature.
|
|
77
|
+
if (bucketTexts(feature.backgroundOrderedSteps, 'when').length > 0 || bucketTexts(feature.backgroundOrderedSteps, 'then').length > 0) {
|
|
77
78
|
findings.push({
|
|
78
79
|
gate: 'A', severity: 'error',
|
|
79
80
|
message: 'Background contains When/Then steps — setup only; move the action/oracle into scenarios',
|
|
@@ -247,7 +248,7 @@ function gateDExecutability(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
247
248
|
for (const g of map.groups) {
|
|
248
249
|
const vs = g.variants.flatMap((ref) => expandMapRef(ref, variantsByVp));
|
|
249
250
|
for (const v of vs) {
|
|
250
|
-
const hasPrecondition = v.precondition.length > 0 || feature.
|
|
251
|
+
const hasPrecondition = v.precondition.length > 0 || bucketTexts(feature.backgroundOrderedSteps, 'given').length > 0;
|
|
251
252
|
if (!hasPrecondition) {
|
|
252
253
|
findings.push({
|
|
253
254
|
gate: 'D', severity: 'error', ref: v.ref,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-bucket raw step texts, derived from `orderedSteps`.
|
|
3
|
+
*
|
|
4
|
+
* The parser's `rawGiven/When/ThenSteps` and `backgroundGiven/When/ThenSteps`
|
|
5
|
+
* arrays were removed as dead code (0c3f4ca7 — they duplicated what
|
|
6
|
+
* `orderedSteps` already carries) while the matrix module, merged in parallel,
|
|
7
|
+
* still consumed them: main stopped compiling. classifySteps used to build
|
|
8
|
+
* those arrays as exactly this filter+map over the same buckets, so this view
|
|
9
|
+
* is 1:1 with the removed fields.
|
|
10
|
+
*/
|
|
11
|
+
import { OrderedStep } from '../types';
|
|
12
|
+
|
|
13
|
+
export function bucketTexts(steps: OrderedStep[], bucket: OrderedStep['bucket']): string[] {
|
|
14
|
+
return steps.filter((s) => s.bucket === bucket).map((s) => s.text);
|
|
15
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Denylist of RESOLVED `${QA_*}` secret values, for redacting runtime failure text
|
|
3
|
+
* (the CSV/XLSX `Note` column and the dashboard snapshot's `errorMessage`).
|
|
4
|
+
*
|
|
5
|
+
* A failed assertion's message carries the credential the runtime already substituted
|
|
6
|
+
* (`Expected: "<real password>"`), and delivery lifts that message into files that get
|
|
7
|
+
* committed permanently (qa/deliverables/*.csv, qa/dashboard/history/*.json). Key-based
|
|
8
|
+
* masking can't help there — the secret appears as a free-standing VALUE — so we build a
|
|
9
|
+
* value denylist and erase every occurrence.
|
|
10
|
+
*
|
|
11
|
+
* Selection = env-var names referenced as `${QA_*}` in the unit's test data, INTERSECTED
|
|
12
|
+
* with a sensitive-name signal on the ENV VAR NAME: QA_ADMIN_PASSWORD / QA_ACCESS_TOKEN
|
|
13
|
+
* are denylisted, QA_BASE_URL / QA_ADMIN_EMAIL are not — non-secret values must stay
|
|
14
|
+
* readable so the deliverable keeps its reproduction value.
|
|
15
|
+
*
|
|
16
|
+
* Values resolve like the runtime does (specs-test-data loadEnvQa precedence):
|
|
17
|
+
* shell/CI env highest > .env.qa.<SUNGEN_ENV> > .env.qa. Reading process.env is
|
|
18
|
+
* deliberate — on CI (where deliverables are actually committed) the .env.qa file is
|
|
19
|
+
* gitignored and usually absent; secrets arrive via env, and a parse-file-only denylist
|
|
20
|
+
* would be empty exactly where it matters. `.env.qa.example` (committed placeholders) is
|
|
21
|
+
* never read. This module only BUILDS the denylist — values are erased from output,
|
|
22
|
+
* never rendered into it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as fs from 'fs';
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
// Sensitive-name signal shared with the API exporter's key/body-field classifier —
|
|
28
|
+
// here it is applied to the ENV VAR NAME (QA_ACCESS_TOKEN), not the yaml key.
|
|
29
|
+
import { SENSITIVE_NAME } from './api-testcase-formatter';
|
|
30
|
+
|
|
31
|
+
/** `${QA_*}` env reference — same shape the runtime resolver allowlists (uppercase, QA_ prefix). */
|
|
32
|
+
const QA_ENV_REF = /\$\{(QA_[A-Z0-9_]+)\}/g;
|
|
33
|
+
|
|
34
|
+
export interface QaEnvSecretDenylist {
|
|
35
|
+
/** Resolved secret values to erase, deduped, longest-first (so a longer secret wins over its substring). */
|
|
36
|
+
values: string[];
|
|
37
|
+
/** Sensitive `${QA_*}` names referenced by the unit that resolved to no usable value. */
|
|
38
|
+
unresolved: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the denylist for one unit from its LOADED test-data map (values still carry the
|
|
43
|
+
* literal `${QA_*}` refs — delivery never resolves them into cells). `cwd` is the project
|
|
44
|
+
* root the delivery CLI runs from, where .env.qa files live.
|
|
45
|
+
*/
|
|
46
|
+
export function collectQaEnvSecretDenylist(
|
|
47
|
+
testData: Record<string, string>,
|
|
48
|
+
cwd: string = process.cwd(),
|
|
49
|
+
): QaEnvSecretDenylist {
|
|
50
|
+
const referenced = new Set<string>();
|
|
51
|
+
for (const v of Object.values(testData)) {
|
|
52
|
+
if (typeof v !== 'string') continue;
|
|
53
|
+
for (const m of v.matchAll(QA_ENV_REF)) referenced.add(m[1]);
|
|
54
|
+
}
|
|
55
|
+
const sensitive = [...referenced].filter((n) => SENSITIVE_NAME.test(n));
|
|
56
|
+
if (sensitive.length === 0) return { values: [], unresolved: [] };
|
|
57
|
+
|
|
58
|
+
const fileEnv = readEnvQaFiles(cwd);
|
|
59
|
+
const values: string[] = [];
|
|
60
|
+
const unresolved: string[] = [];
|
|
61
|
+
for (const name of sensitive) {
|
|
62
|
+
const v = process.env[name] !== undefined ? process.env[name] : fileEnv[name];
|
|
63
|
+
// Values shorter than 3 chars are unusable as a denylist entry (they'd mask
|
|
64
|
+
// coincidental text); treat them as unresolved so coverage gaps stay visible.
|
|
65
|
+
if (typeof v === 'string' && v.length >= 3) values.push(v);
|
|
66
|
+
else unresolved.push(name);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
values: [...new Set(values)].sort((a, b) => b.length - a.length),
|
|
70
|
+
unresolved,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read .env.qa files into a local map (never mutating process.env), with the runtime's
|
|
76
|
+
* precedence: first file that sets a key wins, and `.env.qa.<SUNGEN_ENV>` is read before
|
|
77
|
+
* the base `.env.qa`. Callers give process.env priority over everything here.
|
|
78
|
+
*/
|
|
79
|
+
function readEnvQaFiles(cwd: string): Record<string, string> {
|
|
80
|
+
const out: Record<string, string> = {};
|
|
81
|
+
const env = process.env.SUNGEN_ENV;
|
|
82
|
+
for (const name of [env ? `.env.qa.${env}` : '', '.env.qa']) {
|
|
83
|
+
// .env.qa.example is a committed placeholder file, never a source of real values.
|
|
84
|
+
if (!name || name === '.env.qa.example') continue;
|
|
85
|
+
const p = path.join(cwd, name);
|
|
86
|
+
if (!fs.existsSync(p)) continue;
|
|
87
|
+
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
88
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
89
|
+
if (m && out[m[1]] === undefined) out[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Keys already warned about, so per-variant exporter loops don't repeat themselves. */
|
|
96
|
+
const warnedUnits = new Set<string>();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fail-loud seam: a unit references sensitive `${QA_*}` vars whose values can't be
|
|
100
|
+
* resolved here, AND the run has failures whose messages may embed those very values
|
|
101
|
+
* (resolved by the runtime in a different shell, e.g. CI artifacts exported locally).
|
|
102
|
+
* Silence would mean exporting unredacted secrets without a trace — warn instead.
|
|
103
|
+
*/
|
|
104
|
+
export function warnIfRedactionIncomplete(
|
|
105
|
+
denylist: QaEnvSecretDenylist,
|
|
106
|
+
hasFailures: boolean,
|
|
107
|
+
unitLabel: string,
|
|
108
|
+
): void {
|
|
109
|
+
if (!hasFailures || denylist.unresolved.length === 0) return;
|
|
110
|
+
const key = `${unitLabel}:${denylist.unresolved.join(',')}`;
|
|
111
|
+
if (warnedUnits.has(key)) return;
|
|
112
|
+
warnedUnits.add(key);
|
|
113
|
+
console.warn(
|
|
114
|
+
`⚠ [${unitLabel}] redaction coverage incomplete: ${denylist.unresolved.join(', ')} ` +
|
|
115
|
+
'referenced in test-data but not resolvable here — failure notes in the deliverable ' +
|
|
116
|
+
'may contain unredacted secret values. Set the variable(s) in the shell/CI env or .env.qa before exporting.',
|
|
117
|
+
);
|
|
118
|
+
}
|