test: Unit-Tests auf 97 erweitern (toolExecutionLabel, getLastAssistantText, SemVer, Flags, ShipVerdict)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-05-29 19:06:29 +02:00
commit fb4e96919a

View file

@ -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)}`);