// 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); }