@sun-asterisk/sungen 3.0.0-beta.74 → 3.0.0-beta.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +17 -3
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +30 -14
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/ingest.d.ts +3 -0
- package/dist/cli/commands/ingest.d.ts.map +1 -0
- package/dist/cli/commands/ingest.js +179 -0
- package/dist/cli/commands/ingest.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/templates/index.html +54 -54
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +15 -0
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +8 -0
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts +5 -0
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/capture-patterns.js +33 -0
- package/dist/generators/test-generator/patterns/capture-patterns.js.map +1 -1
- package/dist/harness/audit.d.ts +5 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +13 -2
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +6 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +13 -0
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/parse.d.ts +1 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +3 -0
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/provenance.d.ts +6 -0
- package/dist/harness/provenance.d.ts.map +1 -0
- package/dist/harness/provenance.js +65 -0
- package/dist/harness/provenance.js.map +1 -0
- package/dist/harness/sensors.d.ts +30 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +122 -0
- package/dist/harness/sensors.js.map +1 -1
- package/dist/ingest/baseline-audit.d.ts +38 -0
- package/dist/ingest/baseline-audit.d.ts.map +1 -0
- package/dist/ingest/baseline-audit.js +85 -0
- package/dist/ingest/baseline-audit.js.map +1 -0
- package/dist/ingest/gsheet-fetch.d.ts +9 -0
- package/dist/ingest/gsheet-fetch.d.ts.map +1 -0
- package/dist/ingest/gsheet-fetch.js +180 -0
- package/dist/ingest/gsheet-fetch.js.map +1 -0
- package/dist/ingest/index.d.ts +6 -0
- package/dist/ingest/index.d.ts.map +1 -0
- package/dist/ingest/index.js +22 -0
- package/dist/ingest/index.js.map +1 -0
- package/dist/ingest/legacy-parser.d.ts +39 -0
- package/dist/ingest/legacy-parser.d.ts.map +1 -0
- package/dist/ingest/legacy-parser.js +218 -0
- package/dist/ingest/legacy-parser.js.map +1 -0
- package/dist/ingest/reconcile.d.ts +30 -0
- package/dist/ingest/reconcile.d.ts.map +1 -0
- package/dist/ingest/reconcile.js +65 -0
- package/dist/ingest/reconcile.js.map +1 -0
- package/dist/ingest/to-gherkin.d.ts +33 -0
- package/dist/ingest/to-gherkin.d.ts.map +1 -0
- package/dist/ingest/to-gherkin.js +93 -0
- package/dist/ingest/to-gherkin.js.map +1 -0
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +2 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +13 -3
- package/src/cli/commands/delivery.ts +31 -15
- package/src/cli/commands/ingest.ts +141 -0
- package/src/cli/index.ts +2 -0
- package/src/dashboard/templates/index.html +54 -54
- package/src/generators/gherkin-parser/index.ts +17 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +8 -0
- package/src/generators/test-generator/patterns/capture-patterns.ts +38 -0
- package/src/harness/audit.ts +18 -4
- package/src/harness/capability-plan.ts +11 -0
- package/src/harness/parse.ts +4 -0
- package/src/harness/provenance.ts +33 -0
- package/src/harness/sensors.ts +189 -0
- package/src/ingest/baseline-audit.ts +100 -0
- package/src/ingest/gsheet-fetch.ts +152 -0
- package/src/ingest/index.ts +5 -0
- package/src/ingest/legacy-parser.ts +184 -0
- package/src/ingest/reconcile.ts +80 -0
- package/src/ingest/to-gherkin.ts +108 -0
- package/src/orchestrator/ai-rules-updater.ts +2 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +12 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +12 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch a Google Sheet's tabs as a sheet-bundle — runs under the USER's own Google
|
|
3
|
+
* identity (Application Default Credentials), read-only. This is NOT an AI context, so
|
|
4
|
+
* it is not subject to the "ineligible for generative AI contexts" DLP that blocks AI
|
|
5
|
+
* connectors; and read access (Viewer/Commenter is enough) is all it needs.
|
|
6
|
+
*
|
|
7
|
+
* `googleapis` is an OPTIONAL dependency (lazy-required) — the core install stays lean;
|
|
8
|
+
* users who want the Google fetch run `npm i googleapis` + authenticate once with
|
|
9
|
+
* `gcloud auth application-default login` (or set GOOGLE_APPLICATION_CREDENTIALS).
|
|
10
|
+
*/
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import * as readline from 'readline';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import { RawSheet } from './legacy-parser';
|
|
15
|
+
|
|
16
|
+
export interface SheetBundle { source: string; sheets: RawSheet[] }
|
|
17
|
+
|
|
18
|
+
const SCOPE = 'https://www.googleapis.com/auth/spreadsheets.readonly';
|
|
19
|
+
|
|
20
|
+
/** Accept a full Sheets URL or a bare spreadsheet ID. */
|
|
21
|
+
export function parseSpreadsheetId(idOrUrl: string): string {
|
|
22
|
+
const m = idOrUrl.match(/\/spreadsheets\/d\/([a-zA-Z0-9_-]+)/);
|
|
23
|
+
return m ? m[1] : idOrUrl.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ask(prompt: string): Promise<string> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
29
|
+
rl.question(prompt, (a) => { rl.close(); resolve(a.trim()); });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Load `googleapis`; if missing, ask to install it (default Yes) and install into the
|
|
34
|
+
* sungen package dir, then load it. Throws with manual instructions if declined/failed. */
|
|
35
|
+
async function ensureGoogleapis(): Promise<any> {
|
|
36
|
+
try {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
38
|
+
return require('googleapis').google;
|
|
39
|
+
} catch { /* not installed → offer to install */ }
|
|
40
|
+
|
|
41
|
+
const manual = '\n Install it manually: npm i googleapis\n' +
|
|
42
|
+
' Then authenticate: gcloud auth application-default login --scopes=' + SCOPE;
|
|
43
|
+
|
|
44
|
+
if (!process.stdin.isTTY) {
|
|
45
|
+
throw new Error('`--gsheet` needs the optional dependency `googleapis` (non-interactive shell — cannot prompt).' + manual);
|
|
46
|
+
}
|
|
47
|
+
const answer = (await ask(' `--gsheet` needs the `googleapis` package (read-only Google Sheets). Install it now? [Y/n] ')).toLowerCase();
|
|
48
|
+
if (answer && !/^y(es)?$/.test(answer)) {
|
|
49
|
+
throw new Error('Skipped install — `--gsheet` needs `googleapis`.' + manual);
|
|
50
|
+
}
|
|
51
|
+
const pkgDir = path.resolve(__dirname, '..', '..'); // dist/ingest|src/ingest → package root
|
|
52
|
+
console.log(' Installing googleapis (local to sungen, not saved to your project)…');
|
|
53
|
+
try {
|
|
54
|
+
// --no-save: keep sungen's core lean; this machine gets googleapis without declaring a dep.
|
|
55
|
+
execSync('npm install googleapis --no-save', { cwd: pkgDir, stdio: 'inherit' });
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error('npm install googleapis failed (permissions?).' + manual);
|
|
58
|
+
}
|
|
59
|
+
// Node caches the negative module lookup from process start, so requiring a just-installed
|
|
60
|
+
// package in THIS process is unreliable. Ask for a clean re-run (next process finds it).
|
|
61
|
+
try {
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
63
|
+
return require(require.resolve('googleapis', { paths: [pkgDir] })).google;
|
|
64
|
+
} catch {
|
|
65
|
+
console.log('\n ✓ googleapis installed. Please re-run the same `sungen ingest --gsheet …` command.\n');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Open the Google login in the browser (via gcloud ADC) so the user picks THEIR
|
|
71
|
+
* account, then store read-only credentials. Returns false if gcloud is unavailable. */
|
|
72
|
+
async function tryInteractiveLogin(): Promise<boolean> {
|
|
73
|
+
try {
|
|
74
|
+
execSync('gcloud --version', { stdio: 'ignore' });
|
|
75
|
+
} catch {
|
|
76
|
+
console.log('\n (gcloud not found — install the Google Cloud SDK, or export the sheet to .xlsx and use --legacy.)');
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const ans = (await ask(' Open Google login in your browser now (pick the account that can see the sheet)? [Y/n] ')).toLowerCase();
|
|
80
|
+
if (ans && !/^y(es)?$/.test(ans)) return false;
|
|
81
|
+
try {
|
|
82
|
+
execSync(`gcloud auth application-default login --scopes=${SCOPE},https://www.googleapis.com/auth/cloud-platform`, { stdio: 'inherit' });
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function fetchGoogleSheet(idOrUrl: string): Promise<SheetBundle> {
|
|
90
|
+
const google = await ensureGoogleapis();
|
|
91
|
+
const spreadsheetId = parseSpreadsheetId(idOrUrl);
|
|
92
|
+
const doFetch = async () => {
|
|
93
|
+
// ADC: the user's own credentials (gcloud ADC). Fresh client each attempt so a
|
|
94
|
+
// just-completed login is picked up.
|
|
95
|
+
const auth = new google.auth.GoogleAuth({ scopes: [SCOPE] });
|
|
96
|
+
const sheets = google.sheets({ version: 'v4', auth });
|
|
97
|
+
const m = await sheets.spreadsheets.get({ spreadsheetId, fields: 'properties.title,sheets.properties.title' });
|
|
98
|
+
const tabs: string[] = (m.data?.sheets || []).map((s: any) => s.properties.title);
|
|
99
|
+
const r = await sheets.spreadsheets.values.batchGet({ spreadsheetId, ranges: tabs, majorDimension: 'ROWS' });
|
|
100
|
+
return { m, r };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let meta: any, resp: any;
|
|
104
|
+
try {
|
|
105
|
+
({ m: meta, r: resp } = await doFetch());
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
const msg = String(e?.message || e);
|
|
108
|
+
const noCreds = /default credentials|could not load|invalid_grant|reauth|unauthenticated/i.test(msg);
|
|
109
|
+
// Tool-driven browser login: on missing/expired creds, offer to open the Google
|
|
110
|
+
// login (gcloud ADC) so the user picks THEIR account, then retry once.
|
|
111
|
+
if (noCreds && process.stdin.isTTY && await tryInteractiveLogin()) {
|
|
112
|
+
({ m: meta, r: resp } = await doFetch());
|
|
113
|
+
} else if (noCreds) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
'Google login needed (no usable credentials for your account).\n' +
|
|
116
|
+
' Run: gcloud auth application-default login --scopes=' + SCOPE + '\n' +
|
|
117
|
+
' (opens a browser — pick the account that can see the sheet). Then re-run.\n' +
|
|
118
|
+
' No gcloud? Export the sheet to .xlsx and use --legacy instead.',
|
|
119
|
+
);
|
|
120
|
+
} else if (/access_denied|app is blocked|blocked this access|disallowed_useragent|admin|policy|org_internal|403/i.test(msg)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Blocked by your organization\'s Google Workspace policy (admin App-access control):\n' +
|
|
123
|
+
' "' + msg.split('\n')[0] + '"\n' +
|
|
124
|
+
' Your org blocks OAuth apps from reading Sheets (a sensitive scope). This affects gcloud,\n' +
|
|
125
|
+
' a custom OAuth app, AND the AI connector equally — it is an admin decision, not a sungen bug.\n' +
|
|
126
|
+
' → Use the local path instead: have an owner export the sheet to .xlsx and run\n' +
|
|
127
|
+
' sungen ingest --legacy <file>.xlsx\n' +
|
|
128
|
+
' (no API, no admin needed). Or ask a Workspace admin to trust an app for the\n' +
|
|
129
|
+
' spreadsheets.readonly scope (Admin Console → Security → API controls).',
|
|
130
|
+
);
|
|
131
|
+
} else if (/no access|permission|not found|insufficient|API has not been used/i.test(msg)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'Google access failed: ' + msg.split('\n')[0] + '\n' +
|
|
134
|
+
' Ensure the logged-in account has Viewer/Commenter on the sheet and the Sheets API is enabled.\n' +
|
|
135
|
+
' If your org blocks programmatic access to this file, ask an owner to export .xlsx → use --legacy.',
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const title: string = meta.data?.properties?.title || spreadsheetId;
|
|
143
|
+
const tabNames: string[] = (meta.data?.sheets || []).map((s: any) => s.properties.title);
|
|
144
|
+
const ranges: any[] = resp.data?.valueRanges || [];
|
|
145
|
+
|
|
146
|
+
const out: RawSheet[] = tabNames.map((name, i) => ({
|
|
147
|
+
name,
|
|
148
|
+
rows: (ranges[i]?.values || []).map((row: any[]) => row.map((c) => (c == null ? '' : String(c)))),
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
return { source: title, sheets: out };
|
|
152
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy testcase ingest (P-A) — parse a manual testcase workbook (CSV or XLSX)
|
|
3
|
+
* into a normalized inventory. Deterministic: pure function of the input file
|
|
4
|
+
* (no network, no MCP). The AI skill handles the Google-Sheets/MCP fetch upstream.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
export type SheetType = 'testcase' | 'viewpoint-matrix' | 'ui-checklist' | 'unknown';
|
|
10
|
+
export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
11
|
+
|
|
12
|
+
export interface LegacyTestcase {
|
|
13
|
+
id: string;
|
|
14
|
+
page: string;
|
|
15
|
+
category: string;
|
|
16
|
+
subCategory?: string;
|
|
17
|
+
precondition?: string;
|
|
18
|
+
testData?: string;
|
|
19
|
+
steps: string;
|
|
20
|
+
expected: string;
|
|
21
|
+
priority: Priority;
|
|
22
|
+
type?: string;
|
|
23
|
+
result?: string;
|
|
24
|
+
sheet: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SheetInfo { name: string; type: SheetType; rows: number }
|
|
28
|
+
|
|
29
|
+
export interface LegacyInventory {
|
|
30
|
+
source: { file: string };
|
|
31
|
+
sheets: SheetInfo[];
|
|
32
|
+
testcases: LegacyTestcase[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- quote-aware CSV parser (embedded commas + newlines) ----
|
|
36
|
+
export function parseCSV(text: string): string[][] {
|
|
37
|
+
const rows: string[][] = []; let row: string[] = [], field = '', q = false;
|
|
38
|
+
for (let i = 0; i < text.length; i++) {
|
|
39
|
+
const c = text[i];
|
|
40
|
+
if (q) {
|
|
41
|
+
if (c === '"') { if (text[i + 1] === '"') { field += '"'; i++; } else q = false; }
|
|
42
|
+
else field += c;
|
|
43
|
+
} else if (c === '"') q = true;
|
|
44
|
+
else if (c === ',') { row.push(field); field = ''; }
|
|
45
|
+
else if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; }
|
|
46
|
+
else if (c !== '\r') field += c;
|
|
47
|
+
}
|
|
48
|
+
if (field.length || row.length) { row.push(field); rows.push(row); }
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const norm = (s: string | undefined) => (s || '').replace(/\*/g, '').trim().toLowerCase();
|
|
53
|
+
|
|
54
|
+
function detectSheetType(rows: string[][]): { type: SheetType; headerRow: number } {
|
|
55
|
+
for (let i = 0; i < Math.min(rows.length, 40); i++) {
|
|
56
|
+
const cells = rows[i].map(norm);
|
|
57
|
+
if (cells.some((c) => /^tc\s*id$/.test(c)) && cells.some((c) => /steps?|expected/.test(c)))
|
|
58
|
+
return { type: 'testcase', headerRow: i };
|
|
59
|
+
if (cells.some((c) => /item type/.test(c)) || (cells[0] === 'id' && cells.some((c) => /^item/.test(c))))
|
|
60
|
+
return { type: 'ui-checklist', headerRow: i };
|
|
61
|
+
if (cells.some((c) => /^id\/function$/.test(c)) || cells.some((c) => /viewpoint/.test(c)))
|
|
62
|
+
return { type: 'viewpoint-matrix', headerRow: i };
|
|
63
|
+
}
|
|
64
|
+
return { type: 'unknown', headerRow: -1 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function colMap(header: string[]): Record<string, number> {
|
|
68
|
+
const m: Record<string, number> = {};
|
|
69
|
+
header.forEach((h, i) => { const k = norm(h); if (k && !(k in m)) m[k] = i; });
|
|
70
|
+
return m;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toPriority(raw: string): Priority {
|
|
74
|
+
const p = (raw || '').toLowerCase();
|
|
75
|
+
if (/high|cao/.test(p)) return 'high';
|
|
76
|
+
if (/low|thấp/.test(p)) return 'low';
|
|
77
|
+
if (/medium|normal|trung/.test(p)) return 'normal';
|
|
78
|
+
return 'unknown';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseTestcaseSheet(rows: string[][], headerRow: number, sheet: string): LegacyTestcase[] {
|
|
82
|
+
const cm = colMap(rows[headerRow]);
|
|
83
|
+
if (cm['tc id'] == null) return [];
|
|
84
|
+
const col = (r: string[], key: string) => (cm[key] != null ? (r[cm[key]] || '').trim() : '');
|
|
85
|
+
const out: LegacyTestcase[] = [];
|
|
86
|
+
// Fill-down ONLY the grouping columns (genuinely merged across rows). Precondition is
|
|
87
|
+
// kept PER-ROW: filling it down would bleed one row's "needs API/DB/login" into unrelated
|
|
88
|
+
// rows and falsely inflate the capability/driver signal.
|
|
89
|
+
const fd = { page: '', category: '', sub: '' };
|
|
90
|
+
for (let i = headerRow + 1; i < rows.length; i++) {
|
|
91
|
+
const r = rows[i];
|
|
92
|
+
const id = col(r, 'tc id');
|
|
93
|
+
if (!id || /^(total|module|test environment)/i.test(id)) continue;
|
|
94
|
+
fd.page = col(r, 'page name') || fd.page;
|
|
95
|
+
fd.category = col(r, 'category') || fd.category;
|
|
96
|
+
fd.sub = col(r, 'sub-category') || fd.sub;
|
|
97
|
+
const steps = col(r, 'steps');
|
|
98
|
+
const expected = col(r, 'expected results') || col(r, 'expected result');
|
|
99
|
+
if (!steps && !expected) continue; // skip noise / spacer rows
|
|
100
|
+
out.push({
|
|
101
|
+
id, sheet, page: fd.page, category: fd.category || '(uncategorized)',
|
|
102
|
+
subCategory: fd.sub || undefined, precondition: col(r, 'pre-condition') || undefined,
|
|
103
|
+
testData: col(r, 'test data') || undefined, steps, expected,
|
|
104
|
+
priority: toPriority(col(r, 'priority')),
|
|
105
|
+
type: col(r, 'testcase type') || undefined,
|
|
106
|
+
result: col(r, 'test result') || col(r, 'test result\nchrome (100%)') || undefined,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface RawSheet { name: string; rows: string[][] }
|
|
113
|
+
|
|
114
|
+
/** Load sheets from one input file: CSV (1 sheet), XLSX (all sheets), or a JSON
|
|
115
|
+
* sheet-bundle (the shape the Google-Sheets/MCP skill produces). */
|
|
116
|
+
async function loadSheets(filePath: string): Promise<RawSheet[]> {
|
|
117
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
118
|
+
if (ext === '.csv') {
|
|
119
|
+
return [{ name: path.basename(filePath, '.csv'), rows: parseCSV(fs.readFileSync(filePath, 'utf8')) }];
|
|
120
|
+
}
|
|
121
|
+
if (ext === '.json') {
|
|
122
|
+
// Bundle shapes: { sheets: [{name, rows}] } | [{name, rows}] | { "<tab>": [[..]] }
|
|
123
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
124
|
+
const arr = Array.isArray(data) ? data : Array.isArray(data.sheets) ? data.sheets : null;
|
|
125
|
+
if (arr) return arr.map((s: any) => ({ name: String(s.name), rows: s.rows as string[][] }));
|
|
126
|
+
if (data && typeof data === 'object')
|
|
127
|
+
return Object.entries(data).map(([name, rows]) => ({ name, rows: rows as string[][] }));
|
|
128
|
+
throw new Error('Invalid JSON sheet-bundle: expected { sheets: [{name, rows}] }');
|
|
129
|
+
}
|
|
130
|
+
if (ext === '.xlsx') {
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
132
|
+
const ExcelJS = require('exceljs');
|
|
133
|
+
const wb = new ExcelJS.Workbook();
|
|
134
|
+
await wb.xlsx.readFile(filePath);
|
|
135
|
+
const out: RawSheet[] = [];
|
|
136
|
+
wb.eachSheet((ws: any) => {
|
|
137
|
+
const rows: string[][] = [];
|
|
138
|
+
ws.eachRow((row: any) => {
|
|
139
|
+
const cells: string[] = [];
|
|
140
|
+
row.eachCell({ includeEmpty: true }, (cell: any) => { cells.push(cell.text != null ? String(cell.text) : ''); });
|
|
141
|
+
rows.push(cells);
|
|
142
|
+
});
|
|
143
|
+
out.push({ name: ws.name, rows });
|
|
144
|
+
});
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Unsupported file type: ${ext} (use .csv, .xlsx, or a .json sheet-bundle)`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Classify sheets without parsing testcases — for `--list-sheets`. */
|
|
151
|
+
export async function listSheets(filePaths: string | string[]): Promise<SheetInfo[]> {
|
|
152
|
+
const files = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
153
|
+
const out: SheetInfo[] = [];
|
|
154
|
+
for (const f of files) {
|
|
155
|
+
for (const s of await loadSheets(f)) {
|
|
156
|
+
const { type } = detectSheetType(s.rows);
|
|
157
|
+
out.push({ name: s.name, type, rows: s.rows.length });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Parse one or more inputs (CSV/XLSX/JSON-bundle) into an inventory. Optionally
|
|
164
|
+
* restrict to a set of tab names (`onlySheets`) — workbooks carry many tabs. */
|
|
165
|
+
export async function parseLegacyFile(filePaths: string | string[], onlySheets?: string[]): Promise<LegacyInventory> {
|
|
166
|
+
const files = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
167
|
+
const want = onlySheets && onlySheets.length ? new Set(onlySheets.map((s) => s.trim().toLowerCase())) : null;
|
|
168
|
+
|
|
169
|
+
const inv: LegacyInventory = {
|
|
170
|
+
source: { file: files.map((f) => path.basename(f)).join(', ') },
|
|
171
|
+
sheets: [], testcases: [],
|
|
172
|
+
};
|
|
173
|
+
for (const f of files) {
|
|
174
|
+
for (const s of await loadSheets(f)) {
|
|
175
|
+
if (want && !want.has(s.name.trim().toLowerCase())) continue;
|
|
176
|
+
const { type, headerRow } = detectSheetType(s.rows);
|
|
177
|
+
let tcs: LegacyTestcase[] = [];
|
|
178
|
+
if (type === 'testcase' && headerRow >= 0) tcs = parseTestcaseSheet(s.rows, headerRow, s.name);
|
|
179
|
+
inv.sheets.push({ name: s.name, type, rows: tcs.length || s.rows.length });
|
|
180
|
+
inv.testcases.push(...tcs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return inv;
|
|
184
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewpoint Reconciliation (P-C) — the link between a legacy suite and the Harness.
|
|
3
|
+
*
|
|
4
|
+
* A 1:1 convert reproduces the legacy suite's blind spots. Reconciliation runs the
|
|
5
|
+
* harness viewpoint-gate over the legacy-derived scenarios to answer: which catalog-
|
|
6
|
+
* expected viewpoints does the legacy suite ALREADY cover, and which are MISSING
|
|
7
|
+
* (blind spots the old QA never wrote)? It emits a draft viewpoint overview that
|
|
8
|
+
* seeds `/sungen:create-test` + the harness coverage-gate, so the generated suite is
|
|
9
|
+
* raised to catalog quality instead of merely mirroring the legacy.
|
|
10
|
+
*/
|
|
11
|
+
import { LegacyInventory } from './legacy-parser';
|
|
12
|
+
import { legacyToScenarioInfo } from './baseline-audit';
|
|
13
|
+
import { loadCatalog, viewpointGate } from '../harness/sensors';
|
|
14
|
+
|
|
15
|
+
export interface Reconciliation {
|
|
16
|
+
pageType: string | null;
|
|
17
|
+
coverageRatio: number;
|
|
18
|
+
themesCovered: number;
|
|
19
|
+
themesTotal: number;
|
|
20
|
+
blindSpots: { theme: string; status: 'missing' | 'shallow' }[]; // catalog themes the legacy suite lacks/under-covers
|
|
21
|
+
universalGaps: string[];
|
|
22
|
+
legacyViewpoints: { vpGroup: string; count: number }[]; // what the legacy suite implies
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function vpGroupOf(category: string): string {
|
|
26
|
+
const c = (category || '').toLowerCase();
|
|
27
|
+
if (/search|filter|điều kiện|条件/.test(c)) return 'SEARCH';
|
|
28
|
+
if (/input|validation|nhập|入力/.test(c)) return 'VAL';
|
|
29
|
+
if (/display|hiển thị|表示|data/.test(c)) return 'DISPLAY';
|
|
30
|
+
if (/layout|responsive|tab order|ui|giao diện/.test(c)) return 'UI';
|
|
31
|
+
if (/flow|navigation|遷移|chuyển|event/.test(c)) return 'NAV';
|
|
32
|
+
if (/paging|sort|phân trang/.test(c)) return 'LIST';
|
|
33
|
+
return (c.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').toUpperCase().slice(0, 8)) || 'GEN';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function reconcileViewpoints(inv: LegacyInventory): Reconciliation {
|
|
37
|
+
const scenarios = inv.testcases.map(legacyToScenarioInfo);
|
|
38
|
+
const catalog = loadCatalog();
|
|
39
|
+
const gate = viewpointGate(scenarios, [], catalog);
|
|
40
|
+
|
|
41
|
+
const groups: Record<string, number> = {};
|
|
42
|
+
for (const tc of inv.testcases) {
|
|
43
|
+
const g = vpGroupOf(tc.category);
|
|
44
|
+
groups[g] = (groups[g] || 0) + 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
pageType: gate.pageType,
|
|
49
|
+
coverageRatio: gate.coverageRatio,
|
|
50
|
+
themesCovered: gate.themesCovered,
|
|
51
|
+
themesTotal: gate.themesTotal,
|
|
52
|
+
blindSpots: gate.gaps.map((g) => ({ theme: g.theme, status: g.status })),
|
|
53
|
+
universalGaps: gate.universalGaps,
|
|
54
|
+
legacyViewpoints: Object.entries(groups).map(([vpGroup, count]) => ({ vpGroup, count }))
|
|
55
|
+
.sort((a, b) => b.count - a.count),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A draft test-viewpoint.md: legacy-derived viewpoints + catalog blind-spots to fill. */
|
|
60
|
+
export function renderViewpointOverview(featureName: string, r: Reconciliation): string {
|
|
61
|
+
const L: string[] = [];
|
|
62
|
+
L.push(`# Test Viewpoint — ${featureName} (drafted from legacy + reconciled with the catalog)`, '');
|
|
63
|
+
L.push('> Auto-drafted by `sungen ingest`. Rows from the legacy suite are listed first;');
|
|
64
|
+
L.push('> rows marked **⚠ BLIND-SPOT** are catalog-expected viewpoints the legacy suite does');
|
|
65
|
+
L.push('> NOT cover — add scenarios for them via `/sungen:create-test`. Refine before use.', '');
|
|
66
|
+
L.push(`Detected page-type: \`${r.pageType ?? 'unknown'}\` · legacy covers ${r.themesCovered}/${r.themesTotal} catalog themes (${(r.coverageRatio * 100).toFixed(0)}%)`, '');
|
|
67
|
+
L.push('## Priority Viewpoints', '', '| VP | Priority | Reason |', '|---|---|---|');
|
|
68
|
+
let n = 1;
|
|
69
|
+
for (const v of r.legacyViewpoints) {
|
|
70
|
+
L.push(`| VP-${v.vpGroup}-${String(n).padStart(3, '0')} | Medium | legacy suite: ${v.count} testcase(s) |`);
|
|
71
|
+
n++;
|
|
72
|
+
}
|
|
73
|
+
for (const b of r.blindSpots) {
|
|
74
|
+
L.push(`| VP-GAP-${String(n).padStart(3, '0')} | High | ⚠ BLIND-SPOT — catalog expects "${b.theme}" (${b.status}); legacy has none |`);
|
|
75
|
+
n++;
|
|
76
|
+
}
|
|
77
|
+
if (r.universalGaps.length)
|
|
78
|
+
L.push('', `> Universal reminders not covered: ${r.universalGaps.join(', ')}`);
|
|
79
|
+
return L.join('\n') + '\n';
|
|
80
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy inventory → Gherkin DRAFT + traceability + gap report (P-B).
|
|
3
|
+
*
|
|
4
|
+
* Deterministic SCAFFOLDING only: it preserves every legacy testcase as a scenario
|
|
5
|
+
* with its source (TC id + verbatim Steps/Expected as comments), the right priority +
|
|
6
|
+
* @manual tags (from the capability classifier), and a Given seeded from the
|
|
7
|
+
* precondition. The free-text → real `[Reference] type` step mapping is a SEMANTIC step
|
|
8
|
+
* done by the AI layer (`/sungen:create-test`) on top of this draft — the draft makes
|
|
9
|
+
* that refinement traceable and correctly scoped, it is not final runnable Gherkin.
|
|
10
|
+
*/
|
|
11
|
+
import { LegacyInventory, LegacyTestcase } from './legacy-parser';
|
|
12
|
+
import { classifyReason, MANUAL_REASONS } from '../harness/capability-plan';
|
|
13
|
+
|
|
14
|
+
export interface TraceEntry {
|
|
15
|
+
tcId: string;
|
|
16
|
+
scenario: string;
|
|
17
|
+
vpGroup: string;
|
|
18
|
+
mode: 'ui' | 'cross-screen' | 'manual-capability' | 'manual-keep';
|
|
19
|
+
reasonCode: string; // '' for ui
|
|
20
|
+
drivers: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConvertResult {
|
|
24
|
+
feature: string; // the draft .feature text
|
|
25
|
+
trace: TraceEntry[];
|
|
26
|
+
gap: {
|
|
27
|
+
total: number; ui: number; crossScreen: number;
|
|
28
|
+
manualCapability: number; manualKeep: number; noExpected: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Map a legacy category phrase → a VP group code (best-effort; falls back to a slug).
|
|
33
|
+
function vpGroupOf(category: string): string {
|
|
34
|
+
const c = (category || '').toLowerCase();
|
|
35
|
+
if (/search|filter|điều kiện|条件/.test(c)) return 'SEARCH';
|
|
36
|
+
if (/input|validation|nhập|入力/.test(c)) return 'VAL';
|
|
37
|
+
if (/display|hiển thị|表示|data/.test(c)) return 'DISPLAY';
|
|
38
|
+
if (/layout|responsive|tab order|ui|giao diện/.test(c)) return 'UI';
|
|
39
|
+
if (/flow|navigation|遷移|chuyển|event/.test(c)) return 'NAV';
|
|
40
|
+
if (/paging|sort|phân trang/.test(c)) return 'LIST';
|
|
41
|
+
const slug = c.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').toUpperCase().slice(0, 8);
|
|
42
|
+
return slug || 'GEN';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function oneLine(s: string | undefined, max = 60): string {
|
|
46
|
+
return (s || '').replace(/\s+/g, ' ').trim().slice(0, max);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function classify(tc: LegacyTestcase): { mode: TraceEntry['mode']; code: string; drivers: string[] } {
|
|
50
|
+
const code = classifyReason(`${tc.precondition || ''} ${tc.steps} ${tc.expected} ${tc.testData || ''}`);
|
|
51
|
+
if (!code) return { mode: 'ui', code: '', drivers: [] };
|
|
52
|
+
if (code === 'XS') return { mode: 'cross-screen', code, drivers: [] };
|
|
53
|
+
const def = MANUAL_REASONS[code];
|
|
54
|
+
if (def?.cls === 'keep') return { mode: 'manual-keep', code, drivers: [] };
|
|
55
|
+
return { mode: 'manual-capability', code, drivers: def?.drivers || [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function inventoryToGherkin(inv: LegacyInventory, featureName: string): ConvertResult {
|
|
59
|
+
const tcs = inv.testcases;
|
|
60
|
+
const trace: TraceEntry[] = [];
|
|
61
|
+
const gap = { total: tcs.length, ui: 0, crossScreen: 0, manualCapability: 0, manualKeep: 0, noExpected: 0 };
|
|
62
|
+
const seq: Record<string, number> = {};
|
|
63
|
+
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
lines.push('@legacy');
|
|
66
|
+
lines.push(`Feature: ${featureName} (drafted from legacy testcases: ${inv.source.file})`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push(' # Auto-drafted by `sungen ingest --emit-gherkin` from a legacy manual testcase suite.');
|
|
69
|
+
lines.push(' # Each scenario carries its source TC id (@legacy:<id>) + the original');
|
|
70
|
+
lines.push(' # precondition/steps/expected as comments. REFINE the When/Then into real');
|
|
71
|
+
lines.push(' # [Reference] type steps with /sungen:create-test — the free-text → step');
|
|
72
|
+
lines.push(' # mapping is an AI step; this draft only fixes structure + traceability.');
|
|
73
|
+
lines.push('');
|
|
74
|
+
|
|
75
|
+
for (const tc of tcs) {
|
|
76
|
+
const vp = vpGroupOf(tc.category);
|
|
77
|
+
seq[vp] = (seq[vp] || 0) + 1;
|
|
78
|
+
const code = `VP-${vp}-${String(seq[vp]).padStart(3, '0')}`;
|
|
79
|
+
const desc = oneLine(tc.subCategory || tc.steps) || tc.id;
|
|
80
|
+
const title = `${code} ${desc}`;
|
|
81
|
+
const { mode, code: reason, drivers } = classify(tc);
|
|
82
|
+
if (!tc.expected) gap.noExpected++;
|
|
83
|
+
if (mode === 'ui') gap.ui++;
|
|
84
|
+
else if (mode === 'cross-screen') gap.crossScreen++;
|
|
85
|
+
else if (mode === 'manual-capability') gap.manualCapability++;
|
|
86
|
+
else gap.manualKeep++;
|
|
87
|
+
|
|
88
|
+
const tags = [`@${tc.priority === 'unknown' ? 'normal' : tc.priority}`];
|
|
89
|
+
if (mode !== 'ui') tags.push('@manual');
|
|
90
|
+
tags.push(`@legacy:${tc.id}`);
|
|
91
|
+
|
|
92
|
+
lines.push(` ${tags.join(' ')}`);
|
|
93
|
+
lines.push(` Scenario: ${title}`);
|
|
94
|
+
if (mode === 'manual-capability') lines.push(` # @manual:${reason} — needs ${drivers.join('/') || 'capability'} (driver candidate)`);
|
|
95
|
+
else if (mode === 'manual-keep') lines.push(` # @manual:${reason} — judgment floor, keep manual`);
|
|
96
|
+
else if (mode === 'cross-screen') lines.push(` # cross-screen → model as a flow (/sungen:add-flow)`);
|
|
97
|
+
if (tc.precondition) lines.push(` # precondition: ${oneLine(tc.precondition, 120)}`);
|
|
98
|
+
lines.push(` # steps: ${oneLine(tc.steps, 160)}`);
|
|
99
|
+
lines.push(` # expected: ${oneLine(tc.expected, 160)}`);
|
|
100
|
+
if (tc.precondition) lines.push(` Given User is on [${featureName}] page`);
|
|
101
|
+
lines.push(' # TODO(create-test): map the steps above to real When/Then [Reference] steps');
|
|
102
|
+
lines.push('');
|
|
103
|
+
|
|
104
|
+
trace.push({ tcId: tc.id, scenario: title, vpGroup: vp, mode, reasonCode: reason, drivers });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { feature: lines.join('\n'), trace, gap };
|
|
108
|
+
}
|
|
@@ -47,6 +47,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
47
47
|
['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
|
|
48
48
|
['claude-skill-tc-review.md', '.claude/skills/sungen-tc-review/SKILL.md'],
|
|
49
49
|
['claude-skill-harness-audit.md', '.claude/skills/sungen-harness-audit/SKILL.md'],
|
|
50
|
+
['claude-skill-ingest-legacy.md', '.claude/skills/sungen-ingest-legacy/SKILL.md'],
|
|
50
51
|
['claude-skill-viewpoint.md', '.claude/skills/sungen-viewpoint/SKILL.md'],
|
|
51
52
|
['claude-skill-viewpoint-group-a-data-entry.md', '.claude/skills/sungen-viewpoint/group-a-data-entry.md'],
|
|
52
53
|
['claude-skill-viewpoint-group-b-data-ops.md', '.claude/skills/sungen-viewpoint/group-b-data-ops.md'],
|
|
@@ -78,6 +79,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
78
79
|
['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
|
|
79
80
|
['github-skill-sungen-tc-review.md', '.github/skills/sungen-tc-review/SKILL.md'],
|
|
80
81
|
['github-skill-sungen-harness-audit.md', '.github/skills/sungen-harness-audit/SKILL.md'],
|
|
82
|
+
['github-skill-sungen-ingest-legacy.md', '.github/skills/sungen-ingest-legacy/SKILL.md'],
|
|
81
83
|
['github-skill-sungen-viewpoint.md', '.github/skills/sungen-viewpoint/SKILL.md'],
|
|
82
84
|
['github-skill-sungen-viewpoint-group-a-data-entry.md', '.github/skills/sungen-viewpoint/group-a-data-entry.md'],
|
|
83
85
|
['github-skill-sungen-viewpoint-group-b-data-ops.md', '.github/skills/sungen-viewpoint/group-b-data-ops.md'],
|
|
@@ -78,6 +78,16 @@ The CLI reads the **per-target result file first** (co-located with `.spec.ts`),
|
|
|
78
78
|
|
|
79
79
|
---
|
|
80
80
|
|
|
81
|
+
## XLSX sheets — Auto / Manual split
|
|
82
|
+
|
|
83
|
+
The `.xlsx` is split into two sheets so QA manages the sets separately:
|
|
84
|
+
- **`Auto`** — automatable test cases (`Auto` + `Not compiled`).
|
|
85
|
+
- **`Manual`** — `@manual` test cases (always present, header-only when there are none).
|
|
86
|
+
|
|
87
|
+
Multi-locale (no `SUNGEN_ENV`): one **`<LOCALE> Auto`** sheet per locale + a single shared **`Manual`** sheet (manual TCs are locale-invariant). The **CSV stays one file with every row** — the `Testcase type` column distinguishes Auto vs Manual.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
81
91
|
## Excluded from CSV
|
|
82
92
|
|
|
83
93
|
- `@steps:<name>` **base** scenarios — these are setup-only, inlined into `@extend:...` scenarios at compile time
|
|
@@ -74,6 +74,18 @@ User switch to [T] frame | [main] frame
|
|
|
74
74
|
# 8. Page: User see [T] page
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### Collection / all-card (P5)
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
# Every element's TEXT matches a value (filter correctness, all items belong):
|
|
81
|
+
User see all [Result Product Name] text contains {{category_term}}
|
|
82
|
+
# Every CONTAINER holds a CHILD element (structural per-card proof — prove "each card has X"):
|
|
83
|
+
User see all [Product Card] contain [Product Name]
|
|
84
|
+
User see all [Product Card] contain [Add To Cart] button
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use the all-card form whenever a title claims *every / each* card/row exposes something — a single `User see [Add To Cart] button` does NOT prove "each card" and the harness Claim-Proof gate will flag it.
|
|
88
|
+
|
|
77
89
|
### Table
|
|
78
90
|
|
|
79
91
|
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sungen-ingest-legacy
|
|
3
|
+
description: 'Import a legacy manual testcase suite from Google Sheets (multi-tab) or a local file into Sungen — fetch via MCP, then sungen ingest. Use when the user wants to convert/evaluate an existing manual testcase spreadsheet.'
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# sungen-ingest-legacy
|
|
8
|
+
|
|
9
|
+
Bring an existing **manual testcase workbook** into Sungen for evaluation + conversion. The
|
|
10
|
+
fetch (Google login + pick file) is done here via MCP; the parsing/audit is deterministic
|
|
11
|
+
(`sungen ingest`). **Security:** the workbook is the user's project data — read it on
|
|
12
|
+
consent, keep the output in their project, never upload or commit the content.
|
|
13
|
+
|
|
14
|
+
## Flow
|
|
15
|
+
|
|
16
|
+
1. **Locate the source.**
|
|
17
|
+
- **Google Sheets (recommended):** use the Google Drive MCP. Authenticate if needed, then
|
|
18
|
+
`search_files` / list to find the workbook; confirm the file with the user.
|
|
19
|
+
- **Local:** if the user points to a `.xlsx`/`.csv`, skip to step 4.
|
|
20
|
+
|
|
21
|
+
2. **List the tabs.** Read the workbook's sheet/tab names (Drive MCP file metadata or a values
|
|
22
|
+
read). A legacy workbook usually has **many** tabs — some are testcases, some are
|
|
23
|
+
viewpoint/UI matrices.
|
|
24
|
+
|
|
25
|
+
3. **Assemble a JSON sheet-bundle.** For each tab, read its cell values (a 2-D array of
|
|
26
|
+
strings) and write a local bundle in the user project (e.g.
|
|
27
|
+
`qa/screens/<screen>/requirements/legacy/bundle.json`):
|
|
28
|
+
```json
|
|
29
|
+
{ "source": "<workbook name>", "sheets": [ { "name": "<tab>", "rows": [["TC ID","Page",…],["TC-01",…]] } ] }
|
|
30
|
+
```
|
|
31
|
+
Record only the **source link** in `requirements/legacy/source.yaml` — never the content.
|
|
32
|
+
|
|
33
|
+
4. **Classify the tabs.** Run:
|
|
34
|
+
```bash
|
|
35
|
+
sungen ingest --legacy <bundle.json|file.xlsx|file.csv> --list-sheets
|
|
36
|
+
```
|
|
37
|
+
It prints each tab + detected type (`testcase` / `viewpoint-matrix` / `ui-checklist`).
|
|
38
|
+
|
|
39
|
+
5. **Confirm which tabs to ingest.** Use `AskUserQuestion` to let the user pick the
|
|
40
|
+
**testcase** tabs (matrix/UI tabs feed the viewpoint layer later, not the inventory).
|
|
41
|
+
|
|
42
|
+
6. **Ingest + reconcile.**
|
|
43
|
+
```bash
|
|
44
|
+
sungen ingest --legacy <source> --screen <screen> --sheets "<Tab A>,<Tab B>" --emit-gherkin
|
|
45
|
+
```
|
|
46
|
+
Produces: `inventory.json` (+ baseline audit), `*.legacy-draft.feature` + `legacy-trace.json`
|
|
47
|
+
(parity: `@legacy:<id>` per scenario), and `test-viewpoint.draft.md` with **blind-spots**
|
|
48
|
+
(catalog-expected viewpoints the legacy suite lacks).
|
|
49
|
+
|
|
50
|
+
7. **Hand off to quality.** Tell the user the next step is `/sungen:create-test <screen>` —
|
|
51
|
+
it discovers + refines the draft into real `[Reference]` steps and fills the blind-spots;
|
|
52
|
+
then `sungen audit <screen>` gates quality. A 1:1 convert is NOT the deliverable; the
|
|
53
|
+
harness raises the legacy floor to catalog quality.
|
|
54
|
+
|
|
55
|
+
## Governance block (important)
|
|
56
|
+
|
|
57
|
+
Many orgs mark confidential files as **"ineligible for generative AI contexts"** — the
|
|
58
|
+
Google Drive MCP will then **refuse** to read the file (metadata + download both error).
|
|
59
|
+
This is the org's DLP policy, not a bug, and it is the *expected* outcome for a
|
|
60
|
+
confidential testcase suite. When you hit it, **do not retry** — fall back:
|
|
61
|
+
|
|
62
|
+
> "This sheet is restricted by your org's data policy, so I can't read it through the
|
|
63
|
+
> AI connector. Two ways to proceed, both running as **you**, not AI:
|
|
64
|
+
> (1) `sungen ingest --gsheet <url>` — fetches under your own Google identity
|
|
65
|
+
> (read-only; Viewer/Commenter is enough). It offers to install `googleapis`
|
|
66
|
+
> and to open the Google login in your browser (pick your account), then
|
|
67
|
+
> retries automatically. Needs the gcloud SDK for the browser login.
|
|
68
|
+
> (2) Export it manually (**File → Download → Microsoft Excel `.xlsx`**) and I'll run
|
|
69
|
+
> `sungen ingest --legacy <file>.xlsx`."
|
|
70
|
+
|
|
71
|
+
The local-file path is deterministic and **never sends the content through AI** — the
|
|
72
|
+
correct, governance-compliant channel for confidential data. The MCP auto-pick is only
|
|
73
|
+
for files the org does *not* restrict.
|
|
74
|
+
|
|
75
|
+
## Notes
|
|
76
|
+
- Multiple local CSVs (one per tab) also work: `--legacy tab1.csv tab2.csv …`.
|
|
77
|
+
- Re-run only re-fetches when the user asks; otherwise reuse the saved bundle.
|
|
78
|
+
- Do not invent testcases. Only ingest what the workbook contains; the *augmentation*
|
|
79
|
+
(blind-spots) happens in `/sungen:create-test`, flagged for human review.
|