@sun-asterisk/sungen 3.0.1 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/challenge.d.ts.map +1 -1
- package/dist/cli/commands/challenge.js +9 -2
- package/dist/cli/commands/challenge.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +3 -2
- 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 +8 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +92 -76
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +3 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
- package/dist/generators/test-generator/code-generator.d.ts +12 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +137 -4
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +6 -0
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/database-patterns.js +95 -0
- package/dist/generators/test-generator/patterns/database-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +3 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js +54 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +8 -1
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +6 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +8 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +4 -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/runtime-data-transformer.d.ts +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +5 -5
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/harness/audit.js +1 -1
- package/dist/harness/capability-plan.js +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +1 -1
- package/dist/harness/challenge.d.ts +1 -0
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +49 -2
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts +7 -0
- package/dist/harness/data-driven-lint.d.ts.map +1 -0
- package/dist/harness/data-driven-lint.js +153 -0
- package/dist/harness/data-driven-lint.js.map +1 -0
- package/dist/harness/flow-plan.js +1 -1
- package/dist/harness/parse.d.ts +2 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -0
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/query-catalog.d.ts +48 -0
- package/dist/harness/query-catalog.d.ts.map +1 -0
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +11 -5
- package/dist/harness/script-check.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +40 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +40 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/specs-db.d.ts +26 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-db.js +193 -0
- package/dist/orchestrator/templates/specs-db.js.map +1 -0
- package/dist/orchestrator/templates/specs-db.ts +169 -0
- package/dist/orchestrator/templates/specs-test-data.ts +76 -15
- package/docs/orchestration-spec.md +3 -3
- package/package.json +2 -2
- package/src/cli/commands/challenge.ts +6 -2
- package/src/cli/commands/delivery.ts +3 -2
- package/src/cli/commands/generate.ts +8 -0
- package/src/exporters/csv-exporter.ts +22 -6
- package/src/exporters/spec-parser.ts +3 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +2 -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/scenario.hbs +19 -1
- package/src/generators/test-generator/code-generator.ts +133 -4
- package/src/generators/test-generator/patterns/database-patterns.ts +96 -0
- package/src/generators/test-generator/patterns/expect-patterns.ts +49 -0
- package/src/generators/test-generator/patterns/index.ts +5 -0
- package/src/generators/test-generator/step-mapper.ts +9 -0
- package/src/generators/test-generator/template-engine.ts +5 -2
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +5 -5
- package/src/harness/audit.ts +1 -1
- package/src/harness/capability-plan.ts +1 -1
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/catalog/universal-viewpoints.yaml +1 -1
- package/src/harness/challenge.ts +47 -2
- package/src/harness/data-driven-lint.ts +119 -0
- package/src/harness/flow-plan.ts +1 -1
- package/src/harness/parse.ts +12 -0
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/script-check.ts +12 -6
- package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +40 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +40 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/specs-db.ts +169 -0
- package/src/orchestrator/templates/specs-test-data.ts +76 -15
|
@@ -54,6 +54,25 @@ user-invocable: false
|
|
|
54
54
|
OR condition: generate 1 scenario per branch where that branch alone triggers the outcome.
|
|
55
55
|
→ Happy-path only = missing the most common multi-condition implementation bug.
|
|
56
56
|
|
|
57
|
+
- **Many inputs, same steps → ONE data-driven scenario (`@cases`), not N copies:**
|
|
58
|
+
When a rule needs lots of inputs with the *same* step shape (email/format validation,
|
|
59
|
+
BVA boundary triples, EP classes, decision-table rows), tag one scenario `@cases:<dataset>`,
|
|
60
|
+
reference each row's columns as `{{col}}`, and put the rows as a LIST in test-data:
|
|
61
|
+
```gherkin
|
|
62
|
+
@high @cases:email_validation
|
|
63
|
+
Scenario: VP-VAL-001 The email field rejects invalid formats
|
|
64
|
+
When User fill [Email] field with {{email}}
|
|
65
|
+
Then User see [Error] message with {{expected_error}}
|
|
66
|
+
```
|
|
67
|
+
```yaml
|
|
68
|
+
email_validation:
|
|
69
|
+
- { case: "no @", email: "plainaddress", expected_error: "Invalid email" }
|
|
70
|
+
- { case: "valid", email: "ok@x.com", expected_error: "" }
|
|
71
|
+
```
|
|
72
|
+
→ one `test()` per row, each labelled by `case`. Adding inputs = editing test-data (no recompile),
|
|
73
|
+
and env overlays apply. Prefer this over duplicating a scenario per value. (Gherkin
|
|
74
|
+
`Scenario Outline`/`Examples` is NOT supported — use `@cases`.)
|
|
75
|
+
|
|
57
76
|
---
|
|
58
77
|
|
|
59
78
|
## Tier System
|
|
@@ -120,6 +120,7 @@ Build a mapping table: for each applicable group, does the feature have a matchi
|
|
|
120
120
|
- **EP**: keep only **one representative** per invalid class; same-class duplicates → flag as redundant.
|
|
121
121
|
- **BVA**: spec defines min/max → cover `min-1`, `min`, `max`, `max+1` (Maxlength, counts…).
|
|
122
122
|
- Error messages must match the spec **word-for-word**, not generic.
|
|
123
|
+
- **Data-driven (`@cases`)**: a `@cases:<dataset>` scenario legitimately covers many inputs in ONE scenario (one row per EP class / boundary / rule). Do **not** flag it as "too few negative cases" or as duplication — instead review the **dataset rows**: are all EP classes / boundary triples present, each labelled, expected values exact? N near-identical scenarios that differ only by input value → flag and recommend collapsing to `@cases`.
|
|
123
124
|
|
|
124
125
|
---
|
|
125
126
|
|
package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md
CHANGED
|
@@ -17,6 +17,12 @@ Apply selectively — not every screen needs all four techniques. Use the techni
|
|
|
17
17
|
|
|
18
18
|
**Rule:** These techniques determine **how many** and **which** scenarios to generate. `sungen-viewpoint` determines **which viewpoints** to cover.
|
|
19
19
|
|
|
20
|
+
**Implementing the data table → `@cases` (data-driven):** when EP classes / BVA boundary triples /
|
|
21
|
+
decision-table rows share the *same step shape* and differ only by input/expected values, encode
|
|
22
|
+
them as ONE `@cases:<dataset>` scenario (each class/boundary/rule = one row in the test-data list,
|
|
23
|
+
labelled by a `case` column) instead of N near-duplicate scenarios. The technique still decides the
|
|
24
|
+
rows; `@cases` is how you write them compactly. See `sungen-gherkin-syntax` → Data-driven.
|
|
25
|
+
|
|
20
26
|
---
|
|
21
27
|
|
|
22
28
|
## 1. Equivalence Partitioning (EP)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare class DataSource {
|
|
2
|
+
private configs;
|
|
3
|
+
private engines;
|
|
4
|
+
private cfg;
|
|
5
|
+
private engine;
|
|
6
|
+
private build;
|
|
7
|
+
/** A row matching `filter` must exist; if `expected` given, assert those columns on the first match. */
|
|
8
|
+
assertRow(table: string, filter: Record<string, any>, expected?: Record<string, any>, datasource?: string): Promise<void>;
|
|
9
|
+
/** No row matching `filter` may exist. */
|
|
10
|
+
assertNoRow(table: string, filter: Record<string, any>, datasource?: string): Promise<void>;
|
|
11
|
+
/** Exactly `count` rows must match `filter`. */
|
|
12
|
+
assertCount(table: string, filter: Record<string, any>, count: number, datasource?: string): Promise<void>;
|
|
13
|
+
/** Rewrites $1/$2 placeholders to `?` for SQLite. */
|
|
14
|
+
private sqlFor;
|
|
15
|
+
/** Read-only guard (second layer): a named query must be a single SELECT/WITH statement. */
|
|
16
|
+
private assertSelectOnly;
|
|
17
|
+
/**
|
|
18
|
+
* Run a catalog query (read-only) and return its rows. The result is bound to a `{{name}}`
|
|
19
|
+
* variable via `testData.bind(...)`, so the scenario asserts on it with `expect …` steps and
|
|
20
|
+
* path access (`{{name.count}}`, `{{name.first.col}}`, `{{name[2].col}}`).
|
|
21
|
+
*/
|
|
22
|
+
fetchQuery(label: string, sql: string, params: any[], datasource?: string): Promise<any[]>;
|
|
23
|
+
}
|
|
24
|
+
export declare const db: DataSource;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=specs-db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specs-db.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-db.ts"],"names":[],"mappings":"AA+DA,cAAM,UAAU;IACd,OAAO,CAAC,OAAO,CAAiD;IAChE,OAAO,CAAC,OAAO,CAA6B;IAE5C,OAAO,CAAC,GAAG;YAQG,MAAM;IAoBpB,OAAO,CAAC,KAAK;IAOb,wGAAwG;IAClG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/H,0CAA0C;IACpC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjG,gDAAgD;IAC1C,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhH,qDAAqD;IACrD,OAAO,CAAC,MAAM;IAKd,4FAA4F;IAC5F,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAKjG;AAMD,eAAO,MAAM,EAAE,YAAmB,CAAC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.db = void 0;
|
|
37
|
+
/* eslint-disable */
|
|
38
|
+
/**
|
|
39
|
+
* Sungen Data Driver — runtime DB-verification helper (auto-generated into specs/db.ts).
|
|
40
|
+
*
|
|
41
|
+
* Read-only by construction: it only ever issues a single parameterized SELECT, and
|
|
42
|
+
* table/column identifiers are validated against a strict allowlist pattern (identifiers
|
|
43
|
+
* can't be bound as parameters). Secrets come from .env.qa / process.env, never inline.
|
|
44
|
+
*
|
|
45
|
+
* Engines: PostgreSQL (`pg`) and SQLite (`better-sqlite3`), lazy-loaded on first use.
|
|
46
|
+
* Config: a `datasources.yaml` at the project root (or qa/), with ${VAR} resolved from env.
|
|
47
|
+
*
|
|
48
|
+
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
49
|
+
*/
|
|
50
|
+
const test_1 = require("@playwright/test");
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
54
|
+
const ident = (s) => {
|
|
55
|
+
if (!IDENT.test(s))
|
|
56
|
+
throw new Error(`Unsafe identifier: ${JSON.stringify(s)} (allowed: [A-Za-z_][A-Za-z0-9_]*)`);
|
|
57
|
+
return s;
|
|
58
|
+
};
|
|
59
|
+
function loadEnvQa() {
|
|
60
|
+
for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
|
|
61
|
+
const p = path.join(process.cwd(), name);
|
|
62
|
+
if (!name.endsWith('.') && fs.existsSync(p)) {
|
|
63
|
+
for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
|
|
64
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
65
|
+
if (m && process.env[m[1]] === undefined) {
|
|
66
|
+
process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function loadConfig() {
|
|
73
|
+
loadEnvQa();
|
|
74
|
+
const file = [
|
|
75
|
+
path.join(process.cwd(), 'datasources.yaml'),
|
|
76
|
+
path.join(process.cwd(), 'qa', 'datasources.yaml'),
|
|
77
|
+
].find((f) => fs.existsSync(f));
|
|
78
|
+
if (!file)
|
|
79
|
+
throw new Error('Data Driver: no datasources.yaml found (project root or qa/).');
|
|
80
|
+
let raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
|
|
81
|
+
const { parse } = require('yaml');
|
|
82
|
+
const doc = parse(raw) || {};
|
|
83
|
+
if (!doc.datasources || typeof doc.datasources !== 'object') {
|
|
84
|
+
throw new Error('Data Driver: datasources.yaml must define a top-level `datasources:` map.');
|
|
85
|
+
}
|
|
86
|
+
return doc.datasources;
|
|
87
|
+
}
|
|
88
|
+
class DataSource {
|
|
89
|
+
constructor() {
|
|
90
|
+
this.configs = null;
|
|
91
|
+
this.engines = new Map();
|
|
92
|
+
}
|
|
93
|
+
cfg(name) {
|
|
94
|
+
if (!this.configs)
|
|
95
|
+
this.configs = loadConfig();
|
|
96
|
+
const key = name || Object.keys(this.configs)[0];
|
|
97
|
+
const conf = this.configs[key];
|
|
98
|
+
if (!conf)
|
|
99
|
+
throw new Error(`Data Driver: datasource "${key}" not found in datasources.yaml`);
|
|
100
|
+
return { key, conf };
|
|
101
|
+
}
|
|
102
|
+
async engine(name) {
|
|
103
|
+
const { key, conf } = this.cfg(name);
|
|
104
|
+
if (this.engines.has(key))
|
|
105
|
+
return { engine: this.engines.get(key), conf };
|
|
106
|
+
if (!conf.url)
|
|
107
|
+
throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
|
|
108
|
+
let engine;
|
|
109
|
+
if (conf.engine === 'postgres') {
|
|
110
|
+
const { Pool } = require('pg');
|
|
111
|
+
const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
|
|
112
|
+
engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
|
|
113
|
+
}
|
|
114
|
+
else if (conf.engine === 'sqlite') {
|
|
115
|
+
const Database = require('better-sqlite3');
|
|
116
|
+
const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
|
|
117
|
+
engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
throw new Error(`Data Driver: engine "${conf.engine}" not supported yet (postgres | sqlite).`);
|
|
121
|
+
}
|
|
122
|
+
this.engines.set(key, engine);
|
|
123
|
+
return { engine, conf };
|
|
124
|
+
}
|
|
125
|
+
build(table, filter) {
|
|
126
|
+
const cols = Object.keys(filter);
|
|
127
|
+
const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
|
|
128
|
+
const sql = `SELECT * FROM ${ident(table)}${where ? ' WHERE ' + where : ''} LIMIT 50`;
|
|
129
|
+
return { sql, params: cols.map((c) => filter[c]) };
|
|
130
|
+
}
|
|
131
|
+
/** A row matching `filter` must exist; if `expected` given, assert those columns on the first match. */
|
|
132
|
+
async assertRow(table, filter, expected, datasource) {
|
|
133
|
+
const { engine, conf } = await this.engine(datasource);
|
|
134
|
+
const { sql, params } = this.build(table, filter);
|
|
135
|
+
const rows = await engine.query(this.sqlFor(conf, sql), params);
|
|
136
|
+
(0, test_1.expect)(rows.length, `Expected a row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBeGreaterThanOrEqual(1);
|
|
137
|
+
if (expected) {
|
|
138
|
+
const row = rows[0];
|
|
139
|
+
for (const [col, val] of Object.entries(expected)) {
|
|
140
|
+
ident(col);
|
|
141
|
+
(0, test_1.expect)(String(row[col]), `Column "${col}" of "${table}" where ${desc(filter)}`).toBe(String(val));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** No row matching `filter` may exist. */
|
|
146
|
+
async assertNoRow(table, filter, datasource) {
|
|
147
|
+
const { engine, conf } = await this.engine(datasource);
|
|
148
|
+
const { sql, params } = this.build(table, filter);
|
|
149
|
+
const rows = await engine.query(this.sqlFor(conf, sql), params);
|
|
150
|
+
(0, test_1.expect)(rows.length, `Expected NO row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBe(0);
|
|
151
|
+
}
|
|
152
|
+
/** Exactly `count` rows must match `filter`. */
|
|
153
|
+
async assertCount(table, filter, count, datasource) {
|
|
154
|
+
const { engine, conf } = await this.engine(datasource);
|
|
155
|
+
const cols = Object.keys(filter);
|
|
156
|
+
const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
|
|
157
|
+
const sql = `SELECT count(*) AS n FROM ${ident(table)}${where ? ' WHERE ' + where : ''}`;
|
|
158
|
+
const rows = await engine.query(this.sqlFor(conf, sql), cols.map((c) => filter[c]));
|
|
159
|
+
const n = Number(rows[0]?.n ?? rows[0]?.['count(*)'] ?? 0);
|
|
160
|
+
(0, test_1.expect)(n, `Expected ${count} row(s) in "${table}"${cols.length ? ' where ' + desc(filter) : ''} — found ${n}`).toBe(Number(count));
|
|
161
|
+
}
|
|
162
|
+
/** Rewrites $1/$2 placeholders to `?` for SQLite. */
|
|
163
|
+
sqlFor(conf, sql) {
|
|
164
|
+
return conf.engine === 'sqlite' ? sql.replace(/\$\d+/g, '?') : sql;
|
|
165
|
+
}
|
|
166
|
+
// --- Named queries (catalog-backed; SQL is resolved + embedded at compile time) -----------
|
|
167
|
+
/** Read-only guard (second layer): a named query must be a single SELECT/WITH statement. */
|
|
168
|
+
assertSelectOnly(label, sql) {
|
|
169
|
+
const s = sql.trim().replace(/;\s*$/, '');
|
|
170
|
+
if (!/^(SELECT|WITH)\b/i.test(s))
|
|
171
|
+
throw new Error(`Data Driver: ${label} is not a read-only SELECT — refused.`);
|
|
172
|
+
if (s.includes(';'))
|
|
173
|
+
throw new Error(`Data Driver: ${label} contains multiple statements — refused.`);
|
|
174
|
+
if (/\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|MERGE|REPLACE|CALL|EXEC|EXECUTE|ATTACH|PRAGMA|VACUUM)\b/i.test(s)) {
|
|
175
|
+
throw new Error(`Data Driver: ${label} contains a write/DDL keyword — refused.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Run a catalog query (read-only) and return its rows. The result is bound to a `{{name}}`
|
|
180
|
+
* variable via `testData.bind(...)`, so the scenario asserts on it with `expect …` steps and
|
|
181
|
+
* path access (`{{name.count}}`, `{{name.first.col}}`, `{{name[2].col}}`).
|
|
182
|
+
*/
|
|
183
|
+
async fetchQuery(label, sql, params, datasource) {
|
|
184
|
+
this.assertSelectOnly(label, sql);
|
|
185
|
+
const { engine, conf } = await this.engine(datasource);
|
|
186
|
+
return engine.query(this.sqlFor(conf, sql), params);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function desc(filter) {
|
|
190
|
+
return Object.entries(filter).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
|
|
191
|
+
}
|
|
192
|
+
exports.db = new DataSource();
|
|
193
|
+
//# sourceMappingURL=specs-db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specs-db.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-db.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oBAAoB;AACpB;;;;;;;;;;;GAWG;AACH,2CAA0C;AAC1C,uCAAyB;AACzB,2CAA6B;AAE7B,MAAM,KAAK,GAAG,0BAA0B,CAAC;AACzC,MAAM,KAAK,GAAG,CAAC,CAAS,EAAU,EAAE;IAClC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC;IACjH,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAUF,SAAS,SAAS;IAChB,KAAK,MAAM,IAAI,IAAI,CAAC,SAAS,EAAE,WAAW,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;QAC1E,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;gBACrE,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;oBACzC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;gBACvD,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,UAAU;IACjB,SAAS,EAAE,CAAC;IACZ,MAAM,IAAI,GAAG;QACX,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,kBAAkB,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,kBAAkB,CAAC;KACnD,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IAC5F,IAAI,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,iCAAiC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnH,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,GAAG,CAAC,WAAW,CAAC;AACzB,CAAC;AAID,MAAM,UAAU;IAAhB;QACU,YAAO,GAA4C,IAAI,CAAC;QACxD,YAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAiG9C,CAAC;IA/FS,GAAG,CAAC,IAAa;QACvB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,OAAO,GAAG,UAAU,EAAE,CAAC;QAC/C,MAAM,GAAG,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,GAAG,iCAAiC,CAAC,CAAC;QAC7F,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACvB,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,IAAa;QAChC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAE,EAAE,IAAI,EAAE,CAAC;QAC3E,IAAI,CAAC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,GAAG,mCAAmC,CAAC,CAAC;QACnG,IAAI,MAAc,CAAC;QACnB,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,iBAAiB,EAAE,IAAI,CAAC,oBAAoB,IAAI,IAAI,EAAE,CAAC,CAAC;YACpH,MAAM,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAClF,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;YAC3C,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC,CAAC;YACjG,MAAM,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,CAAC,MAAM,0CAA0C,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAEO,KAAK,CAAC,KAAa,EAAE,MAA2B;QACtD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,iBAAiB,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC;QACtF,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,CAAC;IAED,wGAAwG;IACxG,KAAK,CAAC,SAAS,CAAC,KAAa,EAAE,MAA2B,EAAE,QAA8B,EAAE,UAAmB;QAC7G,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAChE,IAAA,aAAM,EAAC,IAAI,CAAC,MAAM,EAAE,sBAAsB,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QAC3H,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClD,KAAK,CAAC,GAAG,CAAC,CAAC;gBACX,IAAA,aAAM,EAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,SAAS,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACpG,CAAC;QACH,CAAC;IACH,CAAC;IAED,0CAA0C;IAC1C,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,MAA2B,EAAE,UAAmB;QAC/E,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAChE,IAAA,aAAM,EAAC,IAAI,CAAC,MAAM,EAAE,uBAAuB,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5G,CAAC;IAED,gDAAgD;IAChD,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,MAA2B,EAAE,KAAa,EAAE,UAAmB;QAC9F,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,6BAA6B,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACzF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,IAAA,aAAM,EAAC,CAAC,EAAE,YAAY,KAAK,eAAe,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACrI,CAAC;IAED,qDAAqD;IAC7C,MAAM,CAAC,IAAsB,EAAE,GAAW;QAChD,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACrE,CAAC;IAED,6FAA6F;IAC7F,4FAA4F;IACpF,gBAAgB,CAAC,KAAa,EAAE,GAAW;QACjD,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,uCAAuC,CAAC,CAAC;QAChH,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,0CAA0C,CAAC,CAAC;QACtG,IAAI,0HAA0H,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACvI,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,0CAA0C,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,UAAU,CAAC,KAAa,EAAE,GAAW,EAAE,MAAa,EAAE,UAAmB;QAC7E,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAClC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvD,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;CACF;AAED,SAAS,IAAI,CAAC,MAA2B;IACvC,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxF,CAAC;AAEY,QAAA,EAAE,GAAG,IAAI,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Sungen Data Driver — runtime DB-verification helper (auto-generated into specs/db.ts).
|
|
4
|
+
*
|
|
5
|
+
* Read-only by construction: it only ever issues a single parameterized SELECT, and
|
|
6
|
+
* table/column identifiers are validated against a strict allowlist pattern (identifiers
|
|
7
|
+
* can't be bound as parameters). Secrets come from .env.qa / process.env, never inline.
|
|
8
|
+
*
|
|
9
|
+
* Engines: PostgreSQL (`pg`) and SQLite (`better-sqlite3`), lazy-loaded on first use.
|
|
10
|
+
* Config: a `datasources.yaml` at the project root (or qa/), with ${VAR} resolved from env.
|
|
11
|
+
*
|
|
12
|
+
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
13
|
+
*/
|
|
14
|
+
import { expect } from '@playwright/test';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
|
|
18
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
19
|
+
const ident = (s: string): string => {
|
|
20
|
+
if (!IDENT.test(s)) throw new Error(`Unsafe identifier: ${JSON.stringify(s)} (allowed: [A-Za-z_][A-Za-z0-9_]*)`);
|
|
21
|
+
return s;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface DataSourceConfig {
|
|
25
|
+
engine: 'postgres' | 'mysql' | 'sqlite';
|
|
26
|
+
url: string;
|
|
27
|
+
readonly?: boolean;
|
|
28
|
+
statement_timeout_ms?: number;
|
|
29
|
+
max_rows?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadEnvQa(): void {
|
|
33
|
+
for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
|
|
34
|
+
const p = path.join(process.cwd(), name);
|
|
35
|
+
if (!name.endsWith('.') && fs.existsSync(p)) {
|
|
36
|
+
for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
|
|
37
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
38
|
+
if (m && process.env[m[1]] === undefined) {
|
|
39
|
+
process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadConfig(): Record<string, DataSourceConfig> {
|
|
47
|
+
loadEnvQa();
|
|
48
|
+
const file = [
|
|
49
|
+
path.join(process.cwd(), 'datasources.yaml'),
|
|
50
|
+
path.join(process.cwd(), 'qa', 'datasources.yaml'),
|
|
51
|
+
].find((f) => fs.existsSync(f));
|
|
52
|
+
if (!file) throw new Error('Data Driver: no datasources.yaml found (project root or qa/).');
|
|
53
|
+
let raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
|
|
54
|
+
const { parse } = require('yaml');
|
|
55
|
+
const doc = parse(raw) || {};
|
|
56
|
+
if (!doc.datasources || typeof doc.datasources !== 'object') {
|
|
57
|
+
throw new Error('Data Driver: datasources.yaml must define a top-level `datasources:` map.');
|
|
58
|
+
}
|
|
59
|
+
return doc.datasources;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Engine = { query(sql: string, params: any[]): Promise<any[]>; };
|
|
63
|
+
|
|
64
|
+
class DataSource {
|
|
65
|
+
private configs: Record<string, DataSourceConfig> | null = null;
|
|
66
|
+
private engines = new Map<string, Engine>();
|
|
67
|
+
|
|
68
|
+
private cfg(name?: string): { key: string; conf: DataSourceConfig } {
|
|
69
|
+
if (!this.configs) this.configs = loadConfig();
|
|
70
|
+
const key = name || Object.keys(this.configs)[0];
|
|
71
|
+
const conf = this.configs[key];
|
|
72
|
+
if (!conf) throw new Error(`Data Driver: datasource "${key}" not found in datasources.yaml`);
|
|
73
|
+
return { key, conf };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async engine(name?: string): Promise<{ engine: Engine; conf: DataSourceConfig }> {
|
|
77
|
+
const { key, conf } = this.cfg(name);
|
|
78
|
+
if (this.engines.has(key)) return { engine: this.engines.get(key)!, conf };
|
|
79
|
+
if (!conf.url) throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
|
|
80
|
+
let engine: Engine;
|
|
81
|
+
if (conf.engine === 'postgres') {
|
|
82
|
+
const { Pool } = require('pg');
|
|
83
|
+
const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
|
|
84
|
+
engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
|
|
85
|
+
} else if (conf.engine === 'sqlite') {
|
|
86
|
+
const Database = require('better-sqlite3');
|
|
87
|
+
const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
|
|
88
|
+
engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(`Data Driver: engine "${conf.engine}" not supported yet (postgres | sqlite).`);
|
|
91
|
+
}
|
|
92
|
+
this.engines.set(key, engine);
|
|
93
|
+
return { engine, conf };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private build(table: string, filter: Record<string, any>): { sql: string; params: any[] } {
|
|
97
|
+
const cols = Object.keys(filter);
|
|
98
|
+
const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
|
|
99
|
+
const sql = `SELECT * FROM ${ident(table)}${where ? ' WHERE ' + where : ''} LIMIT 50`;
|
|
100
|
+
return { sql, params: cols.map((c) => filter[c]) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A row matching `filter` must exist; if `expected` given, assert those columns on the first match. */
|
|
104
|
+
async assertRow(table: string, filter: Record<string, any>, expected?: Record<string, any>, datasource?: string): Promise<void> {
|
|
105
|
+
const { engine, conf } = await this.engine(datasource);
|
|
106
|
+
const { sql, params } = this.build(table, filter);
|
|
107
|
+
const rows = await engine.query(this.sqlFor(conf, sql), params);
|
|
108
|
+
expect(rows.length, `Expected a row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBeGreaterThanOrEqual(1);
|
|
109
|
+
if (expected) {
|
|
110
|
+
const row = rows[0];
|
|
111
|
+
for (const [col, val] of Object.entries(expected)) {
|
|
112
|
+
ident(col);
|
|
113
|
+
expect(String(row[col]), `Column "${col}" of "${table}" where ${desc(filter)}`).toBe(String(val));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** No row matching `filter` may exist. */
|
|
119
|
+
async assertNoRow(table: string, filter: Record<string, any>, datasource?: string): Promise<void> {
|
|
120
|
+
const { engine, conf } = await this.engine(datasource);
|
|
121
|
+
const { sql, params } = this.build(table, filter);
|
|
122
|
+
const rows = await engine.query(this.sqlFor(conf, sql), params);
|
|
123
|
+
expect(rows.length, `Expected NO row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBe(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Exactly `count` rows must match `filter`. */
|
|
127
|
+
async assertCount(table: string, filter: Record<string, any>, count: number, datasource?: string): Promise<void> {
|
|
128
|
+
const { engine, conf } = await this.engine(datasource);
|
|
129
|
+
const cols = Object.keys(filter);
|
|
130
|
+
const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
|
|
131
|
+
const sql = `SELECT count(*) AS n FROM ${ident(table)}${where ? ' WHERE ' + where : ''}`;
|
|
132
|
+
const rows = await engine.query(this.sqlFor(conf, sql), cols.map((c) => filter[c]));
|
|
133
|
+
const n = Number(rows[0]?.n ?? rows[0]?.['count(*)'] ?? 0);
|
|
134
|
+
expect(n, `Expected ${count} row(s) in "${table}"${cols.length ? ' where ' + desc(filter) : ''} — found ${n}`).toBe(Number(count));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Rewrites $1/$2 placeholders to `?` for SQLite. */
|
|
138
|
+
private sqlFor(conf: DataSourceConfig, sql: string): string {
|
|
139
|
+
return conf.engine === 'sqlite' ? sql.replace(/\$\d+/g, '?') : sql;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Named queries (catalog-backed; SQL is resolved + embedded at compile time) -----------
|
|
143
|
+
/** Read-only guard (second layer): a named query must be a single SELECT/WITH statement. */
|
|
144
|
+
private assertSelectOnly(label: string, sql: string): void {
|
|
145
|
+
const s = sql.trim().replace(/;\s*$/, '');
|
|
146
|
+
if (!/^(SELECT|WITH)\b/i.test(s)) throw new Error(`Data Driver: ${label} is not a read-only SELECT — refused.`);
|
|
147
|
+
if (s.includes(';')) throw new Error(`Data Driver: ${label} contains multiple statements — refused.`);
|
|
148
|
+
if (/\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|MERGE|REPLACE|CALL|EXEC|EXECUTE|ATTACH|PRAGMA|VACUUM)\b/i.test(s)) {
|
|
149
|
+
throw new Error(`Data Driver: ${label} contains a write/DDL keyword — refused.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run a catalog query (read-only) and return its rows. The result is bound to a `{{name}}`
|
|
155
|
+
* variable via `testData.bind(...)`, so the scenario asserts on it with `expect …` steps and
|
|
156
|
+
* path access (`{{name.count}}`, `{{name.first.col}}`, `{{name[2].col}}`).
|
|
157
|
+
*/
|
|
158
|
+
async fetchQuery(label: string, sql: string, params: any[], datasource?: string): Promise<any[]> {
|
|
159
|
+
this.assertSelectOnly(label, sql);
|
|
160
|
+
const { engine, conf } = await this.engine(datasource);
|
|
161
|
+
return engine.query(this.sqlFor(conf, sql), params);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function desc(filter: Record<string, any>): string {
|
|
166
|
+
return Object.entries(filter).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const db = new DataSource();
|
|
@@ -5,6 +5,8 @@ import yaml from 'yaml';
|
|
|
5
5
|
|
|
6
6
|
export class TestDataLoader {
|
|
7
7
|
private data: Record<string, any>;
|
|
8
|
+
// Data-driven (@cases): when set (via withRow), get() prefers this row's columns.
|
|
9
|
+
private row?: Record<string, any>;
|
|
8
10
|
|
|
9
11
|
private constructor(data: Record<string, any>) {
|
|
10
12
|
this.data = data;
|
|
@@ -41,23 +43,56 @@ export class TestDataLoader {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
get(key: string): string {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let current: any = this.data[key];
|
|
47
|
-
if (current === undefined || current === null) {
|
|
48
|
-
// Fall back to nested navigation for YAML-structured keys (e.g. "cart.qty_two").
|
|
49
|
-
current = this.data;
|
|
50
|
-
for (const part of key.split('.')) {
|
|
51
|
-
if (current == null || typeof current !== 'object') {
|
|
52
|
-
throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
|
|
53
|
-
}
|
|
54
|
-
current = current[part];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (current === undefined || current === null) {
|
|
46
|
+
const value = this.resolve(key);
|
|
47
|
+
if (value === undefined || value === null) {
|
|
58
48
|
throw new Error(`Test data key not found: ${key}`);
|
|
59
49
|
}
|
|
60
|
-
return String(
|
|
50
|
+
return String(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a `{{...}}` reference to its raw value. Supports:
|
|
55
|
+
* - flat keys (incl. captured runtime vars stored under a literal dotted key);
|
|
56
|
+
* - `@cases` row columns (the current row wins);
|
|
57
|
+
* - structured paths over nested data AND `@query`-bound result arrays:
|
|
58
|
+
* `q.count` / `q.length` → number of rows
|
|
59
|
+
* `q.first.col` / `q.last.col` / `q[2].col` → a specific row's column
|
|
60
|
+
* `q.col` → shorthand for the first row's column
|
|
61
|
+
*/
|
|
62
|
+
private resolve(key: string): any {
|
|
63
|
+
// 1. Exact flat key — captured vars (set()) live under a literal, possibly dotted, key.
|
|
64
|
+
if (this.row && key in this.row && this.row[key] !== undefined && this.row[key] !== null) {
|
|
65
|
+
return this.row[key];
|
|
66
|
+
}
|
|
67
|
+
if (this.data[key] !== undefined && this.data[key] !== null) {
|
|
68
|
+
return this.data[key];
|
|
69
|
+
}
|
|
70
|
+
// 2. Structured path: head from the row (cases) or shared data, then walk segments.
|
|
71
|
+
const tokens = String(key).replace(/\[(\d+)\]/g, '.$1').split('.');
|
|
72
|
+
let cur: any = (this.row && tokens[0] in this.row) ? this.row[tokens[0]] : this.data[tokens[0]];
|
|
73
|
+
for (let i = 1; i < tokens.length && cur != null; i++) cur = TestDataLoader.step(cur, tokens[i]);
|
|
74
|
+
return cur;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One navigation step over an array (with count/first/last/index/field-shorthand) or object. */
|
|
78
|
+
private static step(cur: any, token: string): any {
|
|
79
|
+
if (Array.isArray(cur)) {
|
|
80
|
+
if (token === 'count' || token === 'length') return cur.length;
|
|
81
|
+
if (token === 'first') return cur[0];
|
|
82
|
+
if (token === 'last') return cur[cur.length - 1];
|
|
83
|
+
if (/^\d+$/.test(token)) return cur[Number(token)];
|
|
84
|
+
return cur[0] == null ? undefined : cur[0][token]; // shorthand: first row's field
|
|
85
|
+
}
|
|
86
|
+
if (cur && typeof cur === 'object') return cur[token];
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Bind a raw value (e.g. an `@query` result array) under `key` so `{{key.…}}` paths resolve.
|
|
92
|
+
* Unlike set(), the value is stored as-is (array/object), not coerced to a string.
|
|
93
|
+
*/
|
|
94
|
+
bind(key: string, value: any): void {
|
|
95
|
+
this.data[key] = value;
|
|
61
96
|
}
|
|
62
97
|
|
|
63
98
|
/**
|
|
@@ -68,6 +103,32 @@ export class TestDataLoader {
|
|
|
68
103
|
set(key: string, value: string): void {
|
|
69
104
|
this.data[key] = value;
|
|
70
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Data-driven (@cases): return the list of rows at `key` (after env-overlay merge),
|
|
109
|
+
* each stamped with `__label` for the test title / report. Throws if missing or not a list.
|
|
110
|
+
*/
|
|
111
|
+
cases(key: string): Array<Record<string, any>> {
|
|
112
|
+
const list = this.data[key];
|
|
113
|
+
if (!Array.isArray(list)) {
|
|
114
|
+
throw new Error(`@cases dataset "${key}" not found or not a list in test-data (got ${typeof list}).`);
|
|
115
|
+
}
|
|
116
|
+
return list.map((row: any, i: number) => {
|
|
117
|
+
const r: Record<string, any> = (row && typeof row === 'object' && !Array.isArray(row)) ? { ...row } : { value: row };
|
|
118
|
+
r.__label = String(r.case ?? r.name ?? r.label ?? `row ${i + 1}`);
|
|
119
|
+
return r;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Data-driven (@cases): a view whose get() prefers the given row's columns and falls
|
|
125
|
+
* back to the shared data. Used inside the per-row test() loop.
|
|
126
|
+
*/
|
|
127
|
+
withRow(row: Record<string, any>): TestDataLoader {
|
|
128
|
+
const view = new TestDataLoader({ ...this.data }); // clone → per-row set() stays isolated
|
|
129
|
+
view.row = row;
|
|
130
|
+
return view;
|
|
131
|
+
}
|
|
71
132
|
}
|
|
72
133
|
|
|
73
134
|
function loadYamlSync(filePath: string): Record<string, any> | null {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Spec triển khai cho lần refactor lớn: chuyển từ **"Context Engine + Skill" (workflow tuyến tính)** sang **Orchestration + Harness Engine**.
|
|
4
4
|
> Mục tiêu chính: **nâng chất lượng testcase** (đo & gate thay vì nhờ AI). Bất biến: `generate`, auto-fix selector, `delivery`, reports.
|
|
5
|
-
> Tài liệu nền: `
|
|
5
|
+
> Tài liệu nền: `docs/spec/sungen_direction_solution.md`, `docs/spec/sungen_refactor_spec.md`.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -153,7 +153,7 @@ Hết ngân sách repair (N vòng) mà vẫn FAIL:
|
|
|
153
153
|
└─► KHÔNG lặp vô hạn. Ghi gap vào audit-report + đưa QA checkpoint (human-in-the-loop).
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
**Quy tắc "khám phá thêm khi nông":** nếu assertion-depth sensor báo một viewpoint critical chỉ assert "see page/section" (vụ home: Cart/Detail/Filter), Orchestrator phải nhận diện đây là **cross-screen** → khuyến nghị chuyển sang **flow** (`add-flow`) và/hoặc cần **năng lực DSL capture biến** (xem `
|
|
156
|
+
**Quy tắc "khám phá thêm khi nông":** nếu assertion-depth sensor báo một viewpoint critical chỉ assert "see page/section" (vụ home: Cart/Detail/Filter), Orchestrator phải nhận diện đây là **cross-screen** → khuyến nghị chuyển sang **flow** (`add-flow`) và/hoặc cần **năng lực DSL capture biến** (xem `docs/spec/sungen_refactor_spec.md` §5.4) — thay vì giả vờ pass bằng assertion nông.
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
@@ -180,7 +180,7 @@ Bốn sensor + một gate. Input: `.feature` (qua GherkinParser) + `test-viewpoi
|
|
|
180
180
|
- Orchestrator đưa phản hồi này lại bước sinh. **Ngân sách N vòng** (mặc định 2–3, như vòng auto-fix selector). Hết ngân sách → báo gap, không lặp vô hạn.
|
|
181
181
|
|
|
182
182
|
### 5.4 QA Checkpoint
|
|
183
|
-
- QA accept/reject/edit/add viewpoint. Mọi quyết định ghi `feedback.jsonl` (xem `
|
|
183
|
+
- QA accept/reject/edit/add viewpoint. Mọi quyết định ghi `feedback.jsonl` (xem `docs/spec/sungen_refactor_spec.md` §8).
|
|
184
184
|
|
|
185
185
|
---
|
|
186
186
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"dist",
|
|
83
83
|
"bin",
|
|
84
84
|
"src",
|
|
85
|
-
"docs",
|
|
85
|
+
"docs/orchestration-spec.md",
|
|
86
86
|
"README.md",
|
|
87
87
|
"LICENSE"
|
|
88
88
|
]
|
|
@@ -38,11 +38,15 @@ export function registerChallengeCommand(program: Command): void {
|
|
|
38
38
|
if (report.collectionClaimSingular.length) {
|
|
39
39
|
for (const f of report.collectionClaimSingular) L(` ⚠ ${f.scenario}\n → ${f.suggestion}`);
|
|
40
40
|
} else L(' ✓ none');
|
|
41
|
-
L(' ②
|
|
41
|
+
L(' ② Data-driven — scenarios that should be one `@cases` (collapse data-variants / cover EP classes)');
|
|
42
|
+
if (report.dataDriven.length) {
|
|
43
|
+
for (const f of report.dataDriven) L(` ⚠ ${f.scenario}\n → ${f.suggestion}`);
|
|
44
|
+
} else L(' ✓ none');
|
|
45
|
+
L(' ③ Coverage — over-covered / shallow');
|
|
42
46
|
if (report.overCovered.length) for (const o2 of report.overCovered) L(` • ${o2.bucket}: ${o2.note}`);
|
|
43
47
|
if (report.shallowThemes.length) L(` • shallow themes: ${report.shallowThemes.join(', ')}`);
|
|
44
48
|
if (!report.overCovered.length && !report.shallowThemes.length) L(' ✓ balanced');
|
|
45
|
-
L('
|
|
49
|
+
L(' ④ Novelty — prompts for the `sungen-challenge` agent (≤20% of official, no auto-merge)');
|
|
46
50
|
for (const p of report.noveltyPrompts) L(` • ${p}`);
|
|
47
51
|
L(' ── Exploration readiness ──');
|
|
48
52
|
for (const e of report.explorationReadiness) L(` • ${e}`);
|
|
@@ -200,11 +200,12 @@ function discoverLocaleVariants(cwd: string, target: DeliveryTarget): LocaleVari
|
|
|
200
200
|
const prefix = `${target.featureBaseName}-test-result`;
|
|
201
201
|
const variants: LocaleVariant[] = [];
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
// Base variant: per-target result file if present, else fall back to the global
|
|
204
|
+
// test-results/results.json (what playwright.config writes by default) via resolveResultsPath.
|
|
204
205
|
variants.push({
|
|
205
206
|
locale: '',
|
|
206
207
|
displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
|
|
207
|
-
resultsPath:
|
|
208
|
+
resultsPath: resolveResultsPath(cwd, target),
|
|
208
209
|
});
|
|
209
210
|
|
|
210
211
|
if (fs.existsSync(genDir)) {
|