diff --git a/test-utils.ts b/test-utils.ts index cf3f369..3199b52 100644 --- a/test-utils.ts +++ b/test-utils.ts @@ -183,6 +183,329 @@ expect( "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)}`);