pi_coder/test-utils.ts

516 lines
20 KiB
TypeScript
Raw Permalink Normal View History

// Unit-Tests für reine Hilfsfunktionen aus pi-coder-judge-extension.ts
//
// Ausführung (TypeScript):
// npx ts-node test-utils.ts
//
// Ausführung ohne ts-node (schneller):
// node --input-type=module < <(sed 's/: string//g; s/: unknown//g; s/: void//g; s/: boolean//g' test-utils.ts)
//
// Oder: Funktionen aus dieser Datei kopieren und als .js ausführen.
// ── Funktionen (aus Extension kopiert, kein pi-API-Import nötig) ─────────────
function normalizeForComparison(s: string): string {
return s.trim().replace(/\s+/g, " ").replace(/[.,;:!?]+$/g, "").toLowerCase();
}
function parseVerdict(text: string): string {
const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i);
return m ? m[1].toUpperCase() : "UNREADABLE";
}
function parseBlockers(text: string): string {
const m = text.match(
/(?:\*\*Blocker\*\*|##\s*Blocker|[-*]\s*Blocker)[:\n]([\s\S]*?)(?:\n(?:\*\*Major\*\*|##\s*Major|[-*]\s*Major)|\n(?:\*\*Minor\*\*|##\s*Minor|[-*]\s*Minor)|$)/i
);
return m ? m[1].trim() : "";
}
// ── Test-Harness ──────────────────────────────────────────────────────────────
let passed = 0;
let failed = 0;
function expect(actual: unknown, expected: unknown, label: string): void {
if (actual === expected) {
console.log(`${label}`);
passed++;
} else {
console.error(`${label}`);
console.error(` erwartet: ${JSON.stringify(expected)}`);
console.error(` erhalten: ${JSON.stringify(actual)}`);
failed++;
}
}
// ── normalizeForComparison ────────────────────────────────────────────────────
console.log("\nnormalizeForComparison()");
expect(normalizeForComparison(" Foo Bar "), "foo bar",
"trimmt führende/nachfolgende Leerzeichen");
expect(normalizeForComparison("Foo.\n"), "foo",
"entfernt trailing Punkt + Newline");
expect(normalizeForComparison("A B"), "a b",
"kollabiert mehrfache Leerzeichen");
expect(normalizeForComparison("Foo:"), "foo",
"entfernt trailing Doppelpunkt");
expect(normalizeForComparison("Foo;"), "foo",
"entfernt trailing Semikolon");
expect(normalizeForComparison("Foo!"), "foo",
"entfernt trailing Ausrufezeichen");
expect(normalizeForComparison("Foo?"), "foo",
"entfernt trailing Fragezeichen");
expect(normalizeForComparison("UPPER CASE"), "upper case",
"konvertiert zu Kleinbuchstaben");
// Loop-Detection: gleiche Blocker nach Normalisierung erkannt
expect(
normalizeForComparison("missing error handling.") ===
normalizeForComparison("missing error handling"),
true,
"Loop-Detection: trailing Punkt macht keinen Unterschied"
);
expect(
normalizeForComparison("null check missing\n") ===
normalizeForComparison("null check missing"),
true,
"Loop-Detection: Newline am Ende macht keinen Unterschied"
);
expect(
normalizeForComparison("Fehler bei Import.") ===
normalizeForComparison("Fehler bei Import"),
true,
"Loop-Detection: mehrfache Leerzeichen + Punkt machen keinen Unterschied"
);
expect(
normalizeForComparison("Blocker A") === normalizeForComparison("Blocker B"),
false,
"Loop-Detection: verschiedene Blocker werden NICHT als gleich erkannt"
);
// ── parseVerdict ──────────────────────────────────────────────────────────────
console.log("\nparseVerdict()");
expect(parseVerdict("Urteil: PASS"), "PASS",
"erkennt PASS");
expect(parseVerdict("Urteil: PASS WITH CONCERNS"), "PASS WITH CONCERNS",
"erkennt PASS WITH CONCERNS (vor PASS gematcht)");
expect(parseVerdict("Urteil: FAIL"), "FAIL",
"erkennt FAIL");
expect(parseVerdict("kein Urteil hier"), "UNREADABLE",
"gibt UNREADABLE zurück wenn kein Urteil");
expect(parseVerdict("urteil: pass"), "PASS",
"case-insensitiv: 'urteil: pass'");
expect(parseVerdict("urteil: Pass With Concerns"), "PASS WITH CONCERNS",
"case-insensitiv: gemischte Groß-/Kleinschreibung");
expect(parseVerdict("Das ist mein Urteil: PASS — und mehr Text dahinter"), "PASS",
"ignoriert Text nach dem Urteil");
expect(parseVerdict("Urteil:PASS"), "PASS",
"toleriert fehlenden Leerzeichen nach Doppelpunkt");
expect(parseVerdict(""), "UNREADABLE",
"leerer String → UNREADABLE");
// ── parseBlockers ─────────────────────────────────────────────────────────────
console.log("\nparseBlockers()");
expect(
parseBlockers("**Blocker**:\n- fehlende Validierung\n**Major**:\n- anderes Problem"),
"- fehlende Validierung",
"erkennt **Blocker** mit Bold-Syntax"
);
expect(
parseBlockers("## Blocker\nNull-Check fehlt\n## Major\nanderes"),
"Null-Check fehlt",
"erkennt ## Blocker mit Heading-Syntax"
);
expect(
parseBlockers("- Blocker:\n- fehlender Import\n- Minor:\n- Stil"),
"- fehlender Import",
"erkennt - Blocker mit Bullet-Syntax"
);
expect(
parseBlockers(" Blocker\nKein Logging\n- Minor\nKleinigkeit"),
"Kein Logging",
"erkennt Blocker (Gedankenstrich)"
);
expect(
parseBlockers("Urteil: PASS\n\nAlles ok."),
"",
"gibt leeren String zurück wenn kein Blocker-Abschnitt"
);
expect(
parseBlockers("**Blocker**:\nkeine\n**Minor**:\n- Stil"),
"keine",
"extrahiert 'keine' als Blocker-Text"
);
// Mehrzeiliger Blocker
const multilineInput = `**Blocker**:
- Import fehlt
- Funktion nicht definiert
**Major**:
- weitere Sache`;
const multilineResult = parseBlockers(multilineInput);
expect(
multilineResult.includes("Import fehlt") && multilineResult.includes("Funktion nicht definiert"),
true,
"extrahiert mehrzeiligen Blocker vollständig"
);
// ── toolExecutionLabel ────────────────────────────────────────────────────────
function toolExecutionLabel(toolName, args) {
switch (toolName) {
case "edit": return `Editiere ${args.path ?? "Datei"}`;
case "write": return `Schreibe ${args.path ?? "Datei"} neu…`;
case "read": return `Lese ${args.path ?? "Datei"}`;
case "grep": return `Suche in ${args.path ?? args.pattern ?? "Dateien"}`;
case "find": return `Suche Dateien: ${args.pattern ?? ""}`;
case "ls": return `Verzeichnis: ${args.path ?? "."}`;
case "bash": {
const cmd = String(args.command ?? "").trim().replace(/\n[\s\S]*/s, "");
if (/git\s+commit/.test(cmd)) return "Git-Commit…";
if (/git\s+add/.test(cmd)) return "Stage Änderungen…";
if (/git\s+tag/.test(cmd)) return "Git-Tag setzen…";
if (/pytest|npm test|cargo test|go test|make test/.test(cmd)) return "Tests laufen…";
if (/git\s+(diff|log|show|tag -l)/.test(cmd)) return "Git-History lesen…";
if (/patch\s+-p1/.test(cmd)) return "Wende Patch an…";
if (/curl/.test(cmd)) return "HTTP-Request…";
return `Shell: ${cmd.slice(0, 55)}${cmd.length > 55 ? "…" : ""}`;
}
case "apply_patch": return "Wende Patch an…";
default: return "";
}
}
console.log("\ntoolExecutionLabel()");
expect(toolExecutionLabel("edit", { path: "src/main.ts" }), "Editiere src/main.ts…",
"edit: gibt Pfad zurück");
expect(toolExecutionLabel("edit", {}), "Editiere Datei…",
"edit: Fallback 'Datei' wenn kein Pfad");
expect(toolExecutionLabel("write", { path: "README.md" }), "Schreibe README.md neu…",
"write: gibt Pfad zurück");
expect(toolExecutionLabel("read", { path: "foo.py" }), "Lese foo.py…",
"read: gibt Pfad zurück");
expect(toolExecutionLabel("bash", { command: "git commit -m 'fix'" }), "Git-Commit…",
"bash: git commit → Git-Commit");
expect(toolExecutionLabel("bash", { command: "git add -A" }), "Stage Änderungen…",
"bash: git add → Stage Änderungen");
expect(toolExecutionLabel("bash", { command: "git tag v1.0.0" }), "Git-Tag setzen…",
"bash: git tag → Git-Tag setzen");
expect(toolExecutionLabel("bash", { command: "pytest tests/" }), "Tests laufen…",
"bash: pytest → Tests laufen");
expect(toolExecutionLabel("bash", { command: "cargo test" }), "Tests laufen…",
"bash: cargo test → Tests laufen");
expect(toolExecutionLabel("bash", { command: "git log --oneline" }), "Git-History lesen…",
"bash: git log → Git-History lesen");
expect(toolExecutionLabel("bash", { command: "patch -p1 < foo.patch" }), "Wende Patch an…",
"bash: patch -p1 → Wende Patch an");
expect(toolExecutionLabel("bash", { command: "curl https://api.example.com" }), "HTTP-Request…",
"bash: curl → HTTP-Request");
expect(toolExecutionLabel("bash", { command: "ls ." }), "Shell: ls .",
"bash: unbekannter Befehl kurz → kein abschließendes …");
expect(toolExecutionLabel("bash", { command: "a".repeat(60) }), `Shell: ${"a".repeat(55)}`,
"bash: Befehl > 55 Zeichen → abgeschnitten mit …");
expect(toolExecutionLabel("apply_patch", {}), "Wende Patch an…",
"apply_patch → Wende Patch an");
expect(toolExecutionLabel("unknown_tool", {}), "",
"unbekanntes Tool → leerer String");
// ── getLastAssistantText ──────────────────────────────────────────────────────
function getLastAssistantText(ctx) {
const entries = ctx.sessionManager.getBranch();
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.type === "message") {
const msg = entry.message;
if (msg?.role === "assistant" && Array.isArray(msg.content)) {
return msg.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
}
}
}
return "";
}
function makeCtx(entries) {
return { sessionManager: { getBranch: () => entries } };
}
function makeMsg(role, content) {
return { type: "message", message: { role, content } };
}
console.log("\ngetLastAssistantText()");
expect(
getLastAssistantText(makeCtx([])),
"",
"leere Session → leerer String"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [{ type: "text", text: "Hallo" }]),
])),
"Hallo",
"eine assistant-Nachricht → deren Text"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("user", [{ type: "text", text: "Frage" }]),
makeMsg("assistant", [{ type: "text", text: "Antwort" }]),
])),
"Antwort",
"user + assistant → gibt assistant-Text zurück"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [{ type: "text", text: "Erste" }]),
makeMsg("assistant", [{ type: "text", text: "Letzte" }]),
])),
"Letzte",
"mehrere assistant-Nachrichten → gibt die letzte zurück"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [{ type: "tool_use", id: "x", name: "bash", input: {} }]),
])),
"",
"assistant-Nachricht ohne text-Content → leerer String"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [
{ type: "text", text: "Teil 1" },
{ type: "tool_use", id: "x", name: "edit", input: {} },
{ type: "text", text: "Teil 2" },
]),
])),
"Teil 1\nTeil 2",
"gemischte text/tool_use-Inhalte → nur text-Teile mit \\n verbunden"
);
// ── detectBumpType ────────────────────────────────────────────────────────────
function detectBumpType(lines) {
if (lines.some(l => /^feat!:|BREAKING CHANGE/.test(l))) return "major";
if (lines.some(l => /^feat(\(.+\))?:/.test(l))) return "minor";
return "patch";
}
console.log("\ndetectBumpType()");
expect(detectBumpType(["feat!: neue API"]), "major",
"feat!: → major");
expect(detectBumpType(["BREAKING CHANGE: auth umgebaut"]), "major",
"BREAKING CHANGE → major");
expect(detectBumpType(["feat: CSV-Export"]), "minor",
"feat: → minor");
expect(detectBumpType(["feat(parser): neues Feature"]), "minor",
"feat(scope): → minor");
expect(detectBumpType(["fix: Crash behoben"]), "patch",
"fix: → patch");
expect(detectBumpType(["chore: cleanup"]), "patch",
"chore: → patch");
expect(detectBumpType(["feat: kleine Änderung", "feat!: breaking"]), "major",
"major hat Vorrang vor minor in gemischter Liste");
expect(detectBumpType([]), "patch",
"leere Commit-Liste → patch");
// ── parseSemVer ───────────────────────────────────────────────────────────────
function parseSemVer(tag) {
const m = tag.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
return m ? [+m[1], +m[2], +m[3]] : null;
}
function expectDeep(actual, expected, label) {
expect(JSON.stringify(actual), JSON.stringify(expected), label);
}
console.log("\nparseSemVer()");
expectDeep(parseSemVer("v1.2.3"), [1, 2, 3], "v1.2.3 → [1, 2, 3]");
expectDeep(parseSemVer("1.2.3"), [1, 2, 3], "1.2.3 ohne v → [1, 2, 3]");
expectDeep(parseSemVer("v0.0.1"), [0, 0, 1], "v0.0.1 → [0, 0, 1]");
expectDeep(parseSemVer("v10.20.300"), [10, 20, 300], "v10.20.300 → dreistellige Zahlen");
expectDeep(parseSemVer("nicht-semver"), null, "ungültiger Tag → null");
expectDeep(parseSemVer("v1.2"), null, "unvollständig v1.2 → null");
expectDeep(parseSemVer(""), null, "leerer String → null");
// ── stripOptimizeFlags ────────────────────────────────────────────────────────
function stripOptimizeFlags(args) {
return (args || "")
.replace(/--rounds\s+\d+/, "")
.replace(/--test-timeout\s+\d+/, "")
.replace(/--with-doku/, "")
.replace(/--continue/, "")
.replace(/--interactive/, "")
.replace(/--no-tests/, "")
.replace(/--approve-concerns/, "")
.replace(/--test-cmd\s+"[^"]*"/, "")
.replace(/--test-cmd\s+\S+/, "")
.trim();
}
console.log("\nstripOptimizeFlags()");
expect(stripOptimizeFlags("mein Auftrag --rounds 3"), "mein Auftrag",
"--rounds N wird entfernt");
expect(stripOptimizeFlags("--with-doku Auftrag"), "Auftrag",
"--with-doku wird entfernt");
expect(stripOptimizeFlags("Auftrag --no-tests --approve-concerns"), "Auftrag",
"--no-tests und --approve-concerns werden entfernt");
expect(stripOptimizeFlags('Auftrag --test-cmd "pytest tests/"'), "Auftrag",
'--test-cmd "..." wird entfernt');
expect(stripOptimizeFlags("Auftrag --test-timeout 60"), "Auftrag",
"--test-timeout N wird entfernt");
expect(stripOptimizeFlags("--continue --interactive Auftrag"), "Auftrag",
"--continue und --interactive werden entfernt");
expect(stripOptimizeFlags("Nur ein Auftrag"), "Nur ein Auftrag",
"keine Flags → Auftrag unverändert");
// ── parseOptimizeOptions ─────────────────────────────────────────────────────
function parseOptimizeOptions(args) {
const roundsMatch = (args || "").match(/--rounds\s+(\d+)/);
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 2;
const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/);
const testCmd = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null;
const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/);
const testTimeout = testTimeoutMatch ? Math.max(1, parseInt(testTimeoutMatch[1], 10)) : 120;
return { maxRounds, testCmd, testTimeout };
}
console.log("\nparseOptimizeOptions()");
expect(parseOptimizeOptions("--rounds 5").maxRounds, 5,
"--rounds 5 → maxRounds = 5");
expect(parseOptimizeOptions("--rounds 0").maxRounds, 1,
"--rounds 0 → maxRounds = 1 (Math.max-Clamp)");
expect(parseOptimizeOptions("--rounds abc").maxRounds, 2,
"--rounds abc → Regex matcht nicht → Default 2");
expect(parseOptimizeOptions("").maxRounds, 2,
"kein --rounds → Default 2");
expect(parseOptimizeOptions("--test-timeout 30").testTimeout, 30,
"--test-timeout 30 → testTimeout = 30");
expect(parseOptimizeOptions("--test-timeout 0").testTimeout, 1,
"--test-timeout 0 → testTimeout = 1 (Math.max-Clamp)");
expect(parseOptimizeOptions("").testTimeout, 120,
"kein --test-timeout → Default 120");
expect(parseOptimizeOptions('--test-cmd "pytest -v"').testCmd, "pytest -v",
'--test-cmd "..." → testCmd ohne Anführungszeichen');
expect(parseOptimizeOptions("--test-cmd pytest").testCmd, "pytest",
"--test-cmd ohne Anführungszeichen → testCmd = 'pytest'");
expect(parseOptimizeOptions("auftrag --rounds 3 --test-timeout 60").maxRounds, 3,
"mehrere Flags kombiniert: maxRounds korrekt");
// ── calcVersionStrings ────────────────────────────────────────────────────────
function calcVersionStrings(current, bump) {
const [maj, min, pat] = current ?? [0, 0, 0];
const initial = !current;
const versions = initial
? { patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" }
: { patch: `v${maj}.${min}.${pat + 1}`, minor: `v${maj}.${min + 1}.0`, major: `v${maj + 1}.0.0` };
const recommended = initial ? "minor" : bump;
const labels = ["patch", "minor", "major"].map(
t => `${t}${versions[t]}${t === recommended ? " (empfohlen)" : ""}`
);
return { versions, recommended, labels };
}
console.log("\ncalcVersionStrings()");
expectDeep(
calcVersionStrings(null, "patch").versions,
{ patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" },
"initial (null): alle drei Standard-Startwerte"
);
expect(calcVersionStrings(null, "patch").recommended, "minor",
"initial: recommended ist immer minor (unabhängig vom Bump)");
expectDeep(
calcVersionStrings([1, 2, 3], "patch").versions,
{ patch: "v1.2.4", minor: "v1.3.0", major: "v2.0.0" },
"[1,2,3]: patch/minor/major korrekt hochgezählt"
);
expect(calcVersionStrings([1, 2, 3], "patch").recommended, "patch",
"[1,2,3] patch-Bump → recommended = patch");
expect(calcVersionStrings([1, 2, 3], "minor").recommended, "minor",
"[1,2,3] minor-Bump → recommended = minor");
expect(calcVersionStrings([1, 2, 3], "major").recommended, "major",
"[1,2,3] major-Bump → recommended = major");
{
const labels = calcVersionStrings([1, 2, 3], "minor").labels;
expect(labels[1], "minor → v1.3.0 (empfohlen)",
"empfohlenes Label trägt Suffix '(empfohlen)'");
expect(labels[0], "patch → v1.2.4",
"nicht-empfohlenes Label hat keinen Suffix");
}
expectDeep(
calcVersionStrings([0, 9, 9], "major").versions,
{ patch: "v0.9.10", minor: "v0.10.0", major: "v1.0.0" },
"[0,9,9]: zweistellige Zahlen korrekt hochgezählt"
);
// ── parseShipVerdict ──────────────────────────────────────────────────────────
function parseShipVerdict(text) {
return text.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? "";
}
console.log("\nparseShipVerdict()");
expect(parseShipVerdict("Urteil: SHIP"), "SHIP",
"erkennt SHIP");
expect(parseShipVerdict("Urteil: NO-SHIP"), "NO-SHIP",
"erkennt NO-SHIP");
expect(parseShipVerdict("urteil: ship"), "SHIP",
"case-insensitiv: 'urteil: ship'");
expect(parseShipVerdict("kein Urteil hier"), "",
"kein Urteil → leerer String");
expect(parseShipVerdict("Urteil: PASS"), "",
"PASS ist kein gültiges SHIP-Token → leerer String");
expect(parseShipVerdict(""), "",
"leerer String → leerer String");
// ── Ergebnis ──────────────────────────────────────────────────────────────────
console.log(`\n${"─".repeat(50)}`);
console.log(`Gesamt: ${passed + failed} Tests — ${passed} bestanden, ${failed} fehlgeschlagen`);
if (failed > 0) {
process.exit(1);
}