1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29:36 +01:00
effigenix/test-automation/web-ui/scripts/generate-tests-from-issue.ts
2026-03-27 17:52:56 +01:00

220 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* generate-tests-from-issue.ts
*
* Generiert einen Playwright-Spec-Stub aus einem GitHub Issue.
*
* Aufruf (via justfile):
* just generate-test 65
*
* Direkt:
* npx tsx scripts/generate-tests-from-issue.ts 65
*
* Voraussetzung: `gh` CLI muss authentifiziert sein.
* nix shell nixpkgs#gh -c gh auth status
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
// ── Konfiguration ──────────────────────────────────────────────────────────────
/** Repo-Pfad relativ zum Repo-Root */
const TESTS_DIR = path.join(__dirname, '..', 'tests', 'api');
/** Mapping: TC-Präfix → { Verzeichnis, Dateiname, API-Pfad, Beschreibung } */
const TC_MAP: Record<string, { dir: string; file: string; apiBase: string; label: string }> = {
'TC-CAT': { dir: 'masterdata', file: 'categories.spec.ts', apiBase: '/api/categories', label: 'Produktkategorien' },
'TC-SUP': { dir: 'masterdata', file: 'suppliers.spec.ts', apiBase: '/api/suppliers', label: 'Lieferanten' },
'TC-ART': { dir: 'masterdata', file: 'articles.spec.ts', apiBase: '/api/articles', label: 'Artikel' },
'TC-CUS': { dir: 'masterdata', file: 'customers.spec.ts', apiBase: '/api/customers', label: 'Kunden' },
'TC-B2B': { dir: 'masterdata', file: 'contracts.spec.ts', apiBase: '/api/customers/{id}/frame-contract', label: 'Rahmenverträge' },
'TC-AUTH': { dir: 'auth', file: 'authorization.spec.ts', apiBase: '/api/auth', label: 'Autorisierung' },
'TC-REC': { dir: 'production', file: 'recipes.spec.ts', apiBase: '/api/recipes', label: 'Rezepte' },
'TC-BAT': { dir: 'production', file: 'batches.spec.ts', apiBase: '/api/production/batches', label: 'Produktionschargen' },
'TC-ORD': { dir: 'production', file: 'orders.spec.ts', apiBase: '/api/production/production-orders', label: 'Produktionsaufträge' },
'TC-TRACE': { dir: 'production', file: 'traceability.spec.ts', apiBase: '/api/production/batches/{id}/trace-*', label: 'Rückverfolgung' },
'TC-STOCK': { dir: 'inventory', file: 'stock.spec.ts', apiBase: '/api/inventory/stocks', label: 'Lagerbestand' },
'TC-MOV': { dir: 'inventory', file: 'movements.spec.ts', apiBase: '/api/inventory/stock-movements', label: 'Warenbewegungen' },
'TC-RES': { dir: 'inventory', file: 'reservations.spec.ts', apiBase: '/api/inventory/stocks/{id}/reservations', label: 'Reservierungen' },
'TC-INV': { dir: 'inventory', file: 'inventory-counts.spec.ts', apiBase: '/api/inventory/inventory-counts', label: 'Inventurzählungen' },
};
// ── GitHub Issue laden ─────────────────────────────────────────────────────────
interface GitHubIssue {
number: number;
title: string;
body: string;
labels: { name: string }[];
}
function fetchIssue(issueNumber: string): GitHubIssue {
try {
const raw = execSync(
`nix shell nixpkgs#gh -c gh api repos/{owner}/{repo}/issues/${issueNumber}`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
);
return JSON.parse(raw);
} catch {
// Fallback ohne nix
try {
const raw = execSync(
`gh api repos/{owner}/{repo}/issues/${issueNumber}`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
);
return JSON.parse(raw);
} catch (err) {
throw new Error(
`GitHub Issue #${issueNumber} konnte nicht geladen werden.\n` +
`Stelle sicher, dass 'gh' authentifiziert ist: nix shell nixpkgs#gh -c gh auth status\n` +
String(err),
);
}
}
}
// ── Test-Cases aus Issue-Body extrahieren ──────────────────────────────────────
interface TestCase {
id: string; // z.B. "TC-SUP-01"
title: string; // z.B. "Lieferant erstellen Pflichtfelder"
}
function extractTestCases(body: string): TestCase[] {
const cases: TestCase[] = [];
// Erkennt: "TC-SUP-01: Beschreibung" oder "**TC-SUP-01**" oder "| TC-SUP-01 |"
const pattern = /\b(TC-[A-Z]+-\d{2,3})[:\s\|*]+([^\n|*]{3,80})/g;
let match: RegExpExecArray | null;
while ((match = pattern.exec(body)) !== null) {
const id = match[1].trim();
const title = match[2].trim().replace(/^\*+|\*+$/g, '').trim();
if (!cases.some(c => c.id === id)) {
cases.push({ id, title });
}
}
return cases;
}
/** Ermittelt den TC-Präfix aus einer Liste von TestCase-IDs */
function detectTcPrefix(cases: TestCase[]): string | null {
for (const tc of cases) {
// TC-TRACE vor TC-T* prüfen (länger zuerst)
for (const prefix of Object.keys(TC_MAP).sort((a, b) => b.length - a.length)) {
if (tc.id.startsWith(prefix)) return prefix;
}
}
return null;
}
// ── Spec-Datei generieren ──────────────────────────────────────────────────────
function generateSpec(issue: GitHubIssue, cases: TestCase[], prefix: string): string {
const meta = TC_MAP[prefix];
const describeName = `${prefix}: ${meta.label}`;
const testBlocks = cases.length > 0
? cases.map(tc => `
test('${tc.id}: ${tc.title}', async ({ request, adminToken }) => {
// TODO: Implementierung
// API: ${meta.apiBase}
const res = await request.get('${meta.apiBase.replace(/\{[^}]+\}/g, 'REPLACE_ID')}', {
headers: { Authorization: \`Bearer \${adminToken}\` },
});
expect(res.status()).toBe(200);
});`).join('\n')
: `
test.todo('TCs aus Issue #${issue.number} noch nicht implementiert');`;
return `import { test, expect } from '../../../fixtures/auth.fixture.js';
/**
* ${describeName}
* Quelle: GitHub Issue #${issue.number}
* Titel: ${issue.title}
*
* ACHTUNG: Automatisch generierter Stub Implementierung erforderlich!
*/
test.describe('${describeName}', () => {${testBlocks}
});
`;
}
// ── Ausgabe-Pfad bestimmen ─────────────────────────────────────────────────────
function resolveOutputPath(prefix: string, issueNumber: string): { full: string; relative: string } | null {
const meta = TC_MAP[prefix];
if (!meta) return null;
const dir = path.join(TESTS_DIR, meta.dir);
const full = path.join(dir, meta.file);
const relative = path.join('tests', 'api', meta.dir, meta.file);
return { full, relative };
}
// ── Interaktive Abfrage ────────────────────────────────────────────────────────
async function confirm(question: string): Promise<boolean> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer.trim().toLowerCase().startsWith('j') || answer.trim().toLowerCase() === 'y');
});
});
}
// ── Main ───────────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
const issueNumber = process.argv[2];
if (!issueNumber || !/^\d+$/.test(issueNumber)) {
console.error('Aufruf: npx tsx scripts/generate-tests-from-issue.ts <issue-nummer>');
process.exit(1);
}
console.log(`\nLade GitHub Issue #${issueNumber}...`);
const issue = fetchIssue(issueNumber);
console.log(`Titel: ${issue.title}`);
const cases = extractTestCases(issue.body ?? '');
if (cases.length > 0) {
console.log(`\nGefundene Test-Cases (${cases.length}):`);
cases.forEach(tc => console.log(` ${tc.id}: ${tc.title}`));
} else {
console.log('\nKeine TC-XXX-YY Muster im Issue gefunden Stub ohne Test-Cases wird generiert.');
}
const prefix = detectTcPrefix(cases);
if (!prefix) {
console.error('\nTC-Präfix konnte nicht erkannt werden. Unterstützte Präfixe: ' + Object.keys(TC_MAP).join(', '));
process.exit(1);
}
console.log(`\nErkannter Präfix: ${prefix}${TC_MAP[prefix].dir}/${TC_MAP[prefix].file}`);
const out = resolveOutputPath(prefix, issueNumber);
if (!out) {
console.error('Ausgabepfad konnte nicht ermittelt werden.');
process.exit(1);
}
if (fs.existsSync(out.full)) {
const overwrite = await confirm(`\nDatei ${out.relative} existiert bereits. Überschreiben? [j/N] `);
if (!overwrite) {
console.log('Abgebrochen.');
process.exit(0);
}
}
const content = generateSpec(issue, cases, prefix);
fs.mkdirSync(path.dirname(out.full), { recursive: true });
fs.writeFileSync(out.full, content, 'utf-8');
console.log(`\nSpec erstellt: ${out.relative}`);
console.log('Nächster Schritt: TODOs im generierten Test implementieren.');
}
main().catch(err => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});