@sun-asterisk/sungen 3.0.1 → 3.1.0
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/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/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/code-generator.d.ts +4 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +31 -2
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +5 -0
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/database-patterns.js +94 -0
- package/dist/generators/test-generator/patterns/database-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 +6 -1
- package/dist/generators/test-generator/patterns/index.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/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/flow-plan.js +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +4 -1
- package/dist/harness/script-check.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/dist/orchestrator/templates/specs-db.d.ts +18 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-db.js +171 -0
- package/dist/orchestrator/templates/specs-db.js.map +1 -0
- package/dist/orchestrator/templates/specs-db.ts +147 -0
- package/docs/orchestration-spec.md +3 -3
- package/package.json +2 -2
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -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/code-generator.ts +29 -2
- package/src/generators/test-generator/patterns/database-patterns.ts +95 -0
- package/src/generators/test-generator/patterns/index.ts +3 -0
- package/src/generators/test-generator/template-engine.ts +2 -2
- 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/flow-plan.ts +1 -1
- package/src/harness/script-check.ts +4 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/src/orchestrator/templates/specs-db.ts +147 -0
|
@@ -11,6 +11,7 @@ import { scrollPatterns } from './scroll-patterns';
|
|
|
11
11
|
import { scopePatterns } from './scope-patterns';
|
|
12
12
|
import { tablePatterns } from './table-patterns';
|
|
13
13
|
import { capturePatterns } from './capture-patterns';
|
|
14
|
+
import { databasePatterns } from './database-patterns';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Pattern Registry - manages all step patterns
|
|
@@ -36,6 +37,7 @@ export class PatternRegistry {
|
|
|
36
37
|
this.patterns.push(...scopePatterns);
|
|
37
38
|
this.patterns.push(...tablePatterns);
|
|
38
39
|
this.patterns.push(...capturePatterns);
|
|
40
|
+
this.patterns.push(...databasePatterns);
|
|
39
41
|
|
|
40
42
|
// Sort by priority (higher first)
|
|
41
43
|
this.patterns.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
@@ -165,4 +167,5 @@ export { keyboardPatterns } from './keyboard-patterns';
|
|
|
165
167
|
export { scrollPatterns } from './scroll-patterns';
|
|
166
168
|
export { scopePatterns } from './scope-patterns';
|
|
167
169
|
export { tablePatterns } from './table-patterns';
|
|
170
|
+
export { databasePatterns, isDbStep } from './database-patterns';
|
|
168
171
|
export * from './types';
|
|
@@ -229,8 +229,8 @@ export class TemplateEngine {
|
|
|
229
229
|
this.baseContext = {};
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string {
|
|
233
|
-
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport });
|
|
232
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
|
|
233
|
+
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb });
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
renderTestFile(data: {
|
package/src/harness/audit.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The score is INTENTIONALLY weighted toward business-critical coverage/depth
|
|
6
6
|
* (not breadth), so it surfaces the gaps a count-based view hides. See
|
|
7
|
-
* docs/orchestration-spec.md §5 and
|
|
7
|
+
* docs/orchestration-spec.md §5 and docs/spec/sungen_refactor_spec.md.
|
|
8
8
|
*/
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Classifies each scenario's execution mode + each @manual case by reason code
|
|
5
5
|
* (M1–M9), maps capability-reasons to drivers, and emits the manual-reason KPI.
|
|
6
6
|
* Never installs anything (that's `sungen capability add`). See
|
|
7
|
-
*
|
|
7
|
+
* docs/spec/sungen_phase2b_spec.md.
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Driver Catalog (metadata only — NO driver code is bundled here).
|
|
2
2
|
# Lets Sungen RECOMMEND/RESOLVE a driver that may not be installed yet, and tells
|
|
3
|
-
# `sungen capability add` which package to install. See
|
|
3
|
+
# `sungen capability add` which package to install. See docs/spec/sungen_phase2a_spec.md.
|
|
4
4
|
#
|
|
5
5
|
# kind: platform → the runtime/codegen adapter for a target (pick ONE per project)
|
|
6
6
|
# kind: capability → an extra ability added on top of a platform (Phase 3)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Each page-type lists must-cover themes. A theme is "covered" when the project's
|
|
7
7
|
# viewpoint-overview (or generated scenarios) contains one of its keywords.
|
|
8
|
-
# See docs/orchestration-spec.md §5.2 and
|
|
8
|
+
# See docs/orchestration-spec.md §5.2 and docs/spec/sungen_refactor_spec.md §9.
|
|
9
9
|
#
|
|
10
10
|
# `depth:` (optional, harness-roadmap P1) marks a theme as DATA-correctness:
|
|
11
11
|
# requires: data-assertion → scenarios on this theme must assert DATA (not just
|
package/src/harness/flow-plan.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* leg's SELECTOR READINESS + capability, folds in the manual-reason taxonomy
|
|
6
6
|
* (capability-plan) and the run-test contract (flow-check), and emits a run-test
|
|
7
7
|
* PLAN. Automates the manual diagnosis done while healing cart-and-filter.
|
|
8
|
-
* See
|
|
8
|
+
* See docs/spec/sungen_phase2c_spec.md.
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
@@ -68,7 +68,10 @@ export function analyzeFaithfulness(specSrc: string, automatedTitles: Set<string
|
|
|
68
68
|
for (const blk of extractTestBlocks(specSrc)) {
|
|
69
69
|
if (!automatedTitles.has(blk.title)) continue; // only non-@manual scenarios
|
|
70
70
|
const body = blk.body;
|
|
71
|
-
|
|
71
|
+
// An assertion is a Playwright `expect(...)` OR a Data Driver DB assertion
|
|
72
|
+
// (`db.assertRow/assertNoRow/assertCount/...`) — a DB check is a real oracle, so a
|
|
73
|
+
// DB-only scenario (no UI expect) is NOT a bypass.
|
|
74
|
+
if (!body.some((l) => /expect\(|\bdb\.assert\w*\s*\(/.test(l))) assertionlessTests.push(blk.title);
|
|
72
75
|
// hollow step: a `// step` whose region (until the NEXT step-comment / block end)
|
|
73
76
|
// contains no executable code. The region — not just the next line — is checked,
|
|
74
77
|
// so block-style steps (`// Assert all … { … expect … }`) are correctly counted.
|
|
@@ -81,4 +81,4 @@ domain rủi ro+defect?→ YES: Defect-first
|
|
|
81
81
|
→ hỏi QA; QA chưa phản hồi → OUTPUT kèm ASSUMPTION LIST rõ ràng (không stall)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
See `docs/orchestration-spec.md` for the full flow and `
|
|
84
|
+
See `docs/orchestration-spec.md` for the full flow and `docs/spec/sungen_refactor_spec.md` for the design rationale.
|
|
@@ -81,4 +81,4 @@ domain rủi ro+defect?→ YES: Defect-first
|
|
|
81
81
|
→ hỏi QA; QA chưa phản hồi → OUTPUT kèm ASSUMPTION LIST rõ ràng (không stall)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
See `docs/orchestration-spec.md` for the full flow and `
|
|
84
|
+
See `docs/orchestration-spec.md` for the full flow and `docs/spec/sungen_refactor_spec.md` for the design rationale.
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
|
|
143
|
+
function desc(filter: Record<string, any>): string {
|
|
144
|
+
return Object.entries(filter).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const db = new DataSource();
|