Files
ai-control/src/messages.test.ts
T
marcusH df828daea9 Pools können auf ein vorhandenes Config-Verzeichnis verweisen
Wer claude schon benutzt, hat einen fertigen Login in ~/.claude. Bisher
begann ai-control trotzdem bei null: jeder Pool war ein neu angelegtes
Verzeichnis, der erste Sessionstart verlangte /login. Ein Pool kann jetzt
stattdessen auf ein bestehendes Verzeichnis zeigen.

pool.json trägt dafür optional `dir` (home-kontrahiert). Der Pool-Ordner
ist dann nur die Hülle für diese Datei; pool_config_dir() löst auf und
ist die einzige Stelle, über die Sessionstart, Verbrauchsstatistik,
Panel-Provisionierung und Keychain-Name gehen.

Zeigt ein Pool auf claudes Default-Verzeichnis, bleibt CLAUDE_CONFIG_DIR
ungesetzt: claude nimmt dieses Verzeichnis von sich aus und damit den
unsuffixierten Keychain-Eintrag, an dem der bestehende Login hängt (am
Keychain gegengeprüft — "Claude Code-credentials" ohne Suffix neben den
suffixierten Pool-Einträgen). Ein Verweis auf ein anderes Verzeichnis
setzt die Variable wie gewohnt.

Das verwiesene Verzeichnis gehört der App nicht: dort entsteht nur die
Panel-Ausstattung (Skill, MCP-Eintrag, Freigabe-Ergänzung, alles
additiv), keine settings.json-Grundausstattung. Löschen trifft nur die
Hülle; Runtime-Symlinks und das Zurücksetzen des Keychain-Eintrags sind
gesperrt. Ein Verzeichnis kann nur ein Pool belegen, Ziele innerhalb von
pools/ sind ausgeschlossen.

In der UI erscheint der Knopf nur, wenn ~/.claude existiert und noch
kein Pool darauf zeigt; die Liste zeigt den Pfad unter dem Namen und
statt "Zurücksetzen" den Hinweis, dass die Anmeldung claude gehört.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:43:00 +02:00

125 lines
3.8 KiB
TypeScript

// Übersetzungstabellen und der Vue-freie t()/applyI18n()-Helfer.
import { afterEach, describe, expect, it } from "vitest";
import { applyI18n, messages, storedLocale, t } from "./messages";
/// Alle Punkt-Pfade eines Nachrichtenbaums, für den Vollständigkeitsabgleich.
function keys(obj: object, prefix = ""): string[] {
return Object.entries(obj).flatMap(([k, v]) =>
typeof v === "object" && v !== null
? keys(v, `${prefix}${k}.`)
: [`${prefix}${k}`],
);
}
afterEach(() => {
try {
window.localStorage.removeItem("lang");
} catch {
/* Storage in der Testumgebung nicht verfügbar */
}
document.body.textContent = "";
});
describe("Nachrichtentabellen", () => {
it("führen dieselben Schlüssel in beiden Sprachen", () => {
expect(keys(messages.en).sort()).toEqual(keys(messages.de).sort());
});
it("lassen keinen Text unübersetzt stehen", () => {
// Gleiche Texte sind fast immer eine vergessene Übersetzung. Erlaubt sind
// nur Eigennamen und Zeichen ohne Sprache.
const erlaubt = new Set([
"Pool",
"Pools",
"Name",
"Theme",
"Wiki",
"MD",
"Autostart",
"Input",
"Output",
"Session",
"Cache ↑",
"Cache ↓",
"+ oAuth",
"+ apiKey",
"System",
"Terminal",
"Terminal — {name}",
".desktop-Starter",
".ai-control/ (Config + Icon)",
"Archiv-Berechtigung in .claude/settings.json",
"Todo-Hook in .claude/settings.json",
"OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
]);
const gleich = keys(messages.de).filter((k) => {
const de = pick(messages.de, k);
return de === pick(messages.en, k) && !erlaubt.has(de);
});
expect(gleich).toEqual([]);
});
});
function pick(obj: object, key: string): string {
return key
.split(".")
.reduce<never>((o, part) => (o as Record<string, never>)[part], obj as never);
}
describe("t()", () => {
it("löst Punkt-Pfade auf", () => {
expect(t("panel.tabDraft")).toBe("Dokument");
});
it("setzt Platzhalter ein", () => {
expect(t("search.noHits", { scope: "„x“" })).toBe("Keine Treffer für „x“");
});
it("lässt unbekannte Platzhalter stehen", () => {
expect(t("wiki.emptyTag", { falsch: "x" })).toBe("Keine Dokumente mit #{tag}.");
});
it("gibt bei fehlendem Schlüssel den Pfad zurück", () => {
expect(t("panel.gibtEsNicht")).toBe("panel.gibtEsNicht");
expect(t("nichts.da.tief")).toBe("nichts.da.tief");
});
it("folgt der gespeicherten Sprache", () => {
window.localStorage.setItem("lang", "en");
expect(storedLocale()).toBe("en");
expect(t("panel.tabDraft")).toBe("Document");
});
it("fällt bei unbekannter Sprache auf die Browsersprache zurück", () => {
window.localStorage.setItem("lang", "kli");
expect(storedLocale()).toBe("de"); // test-setup.ts pinnt navigator.language
});
});
describe("applyI18n()", () => {
it("beschriftet Text und Attribute", () => {
document.body.innerHTML = `
<button data-i18n="panel.tabWiki" data-i18n-title="panel.tabWikiTitle">alt</button>
<button data-i18n-aria="panel.close"></button>
<input data-i18n-placeholder="search.placeholder" />`;
applyI18n();
const btn = document.querySelector("button")!;
expect(btn.textContent).toBe("Wiki");
expect(btn.title).toBe("Archiv-Wiki");
expect(document.querySelectorAll("button")[1].getAttribute("aria-label")).toBe(
"Schließen",
);
expect(document.querySelector("input")!.placeholder).toBe(
"Archiv durchsuchen — #tag filtert",
);
expect(document.documentElement.lang).toBe("de");
});
it("lässt Markup ohne Marker unangetastet", () => {
document.body.innerHTML = `<span>unberührt</span>`;
applyI18n();
expect(document.querySelector("span")!.textContent).toBe("unberührt");
});
});