@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
|
@@ -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 {
|