- PUT /api/agents/{id} und /texts: Stammdaten, Grenzen, Budget, Identity/Soul/
Systemprompt (L6/L7); "Jetzt ausfuehren" in der Agentenansicht (L9)
- Lizenzschluessel ueber die Weboberflaeche (L1): erst vom Lizenzserver
bestaetigt, dann gespeichert; hebt die Sperre ohne Neustart auf
- GET/PUT /api/settings: Instanz- und Anwendungseinstellungen inkl.
OpenRouter-Key (L3/L4/L5), Geheimnisse nur schreibend
- GET/PUT /api/verwaltung/settings: Deploymentcenter und Update-Kanal des Managers
- Einstellungsseite zeigt echte Werte statt fester Texte
- NonInteractiveLicensePrompt verwies auf --license-set-key, den es nie gab
- Sprachpakete werden bei jedem Lauf geprueft (LanguagePackTests)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.5 KiB
C#
93 lines
3.5 KiB
C#
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Web.Tests;
|
|
|
|
/// <summary>
|
|
/// #764 / #661: The WebUI texts come from language packs (<c>wwwroot/i18n/en.json</c> and
|
|
/// <c>de.json</c>). A key missing in one pack shows up as the bare key on screen — in the
|
|
/// language nobody tested in. So every run checks the packs, not a person after each change.
|
|
/// </summary>
|
|
public sealed partial class LanguagePackTests
|
|
{
|
|
private static readonly string WwwRoot = Path.Combine(
|
|
FindRepositoryRoot(), "src", "ClawdDotNet.Manager.Web", "wwwroot");
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, "ClawdDotNet.slnx")))
|
|
return dir.FullName;
|
|
}
|
|
|
|
throw new InvalidOperationException("Repository root not found above " + AppContext.BaseDirectory);
|
|
}
|
|
|
|
private static Dictionary<string, string> Pack(string language)
|
|
=> JsonSerializer.Deserialize<Dictionary<string, string>>(
|
|
File.ReadAllText(Path.Combine(WwwRoot, "i18n", language + ".json")))!;
|
|
|
|
[GeneratedRegex(@"\{(\w+)\}")]
|
|
private static partial Regex Placeholder();
|
|
|
|
// t("key" … — but not erstelleElement("div": the t must stand on its own.
|
|
[GeneratedRegex(@"(?<![\w.])t\(\s*[""']([\w.]+)[""']")]
|
|
private static partial Regex TranslateCall();
|
|
|
|
[Fact]
|
|
public void Both_packs_have_the_same_keys()
|
|
{
|
|
var en = Pack("en").Keys.ToHashSet();
|
|
var de = Pack("de").Keys.ToHashSet();
|
|
|
|
en.Except(de).ShouldBeEmpty("missing in de.json");
|
|
de.Except(en).ShouldBeEmpty("missing in en.json");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_text_keeps_its_placeholders_in_both_languages()
|
|
{
|
|
// "{name} saved" in English and "Gespeichert" in German would drop the name silently.
|
|
var en = Pack("en");
|
|
var de = Pack("de");
|
|
|
|
var mismatches = en.Keys.Intersect(de.Keys)
|
|
// Shows the trigger syntax itself ("{field}" / "{feld}" stands for any field name) and
|
|
// is called without arguments — the braces are text for the reader, not placeholders.
|
|
.Where(k => k != "trigger.instructionHint")
|
|
.Where(k => !Placeholder().Matches(en[k]).Select(m => m.Value).ToHashSet()
|
|
.SetEquals(Placeholder().Matches(de[k]).Select(m => m.Value)))
|
|
.ToList();
|
|
|
|
mismatches.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Every_key_the_code_asks_for_exists()
|
|
{
|
|
var en = Pack("en");
|
|
|
|
var missing = Directory.EnumerateFiles(WwwRoot, "*.js", SearchOption.AllDirectories)
|
|
.Where(f => !f.Contains(Path.DirectorySeparatorChar + "vendor" + Path.DirectorySeparatorChar))
|
|
.SelectMany(f => TranslateCall().Matches(File.ReadAllText(f))
|
|
.Select(m => (File: Path.GetFileName(f), Key: m.Groups[1].Value)))
|
|
.Where(x => !en.ContainsKey(x.Key))
|
|
// The example in the doc comment of i18n.js.
|
|
.Where(x => x.Key != "runs.count")
|
|
.Select(x => $"{x.File}: {x.Key}")
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
missing.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void No_text_is_empty()
|
|
{
|
|
foreach (var language in new[] { "en", "de" })
|
|
Pack(language).Where(p => string.IsNullOrWhiteSpace(p.Value)).Select(p => p.Key).ShouldBeEmpty(language);
|
|
}
|
|
}
|