Files
admin 5270bad76e
gates / gates (push) Successful in 23s
v0.252.0 — the sentences the program builds follow the language (R-557 slice 2 release A)
Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still
Hungarian literals in Go, so an English household clicked an English button and was
answered in Hungarian. 226 of them move into the bundle here.

A flash was the hard part: it travels inside the redirect URL and is rendered by a
DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by
an older controller carries prose and is shown verbatim — never a raw key, never dropped.

Also converted: page data and view-model text, the internal/api JSON answers, the alert
banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at
display, and the four page titles built around an app name (R-566 closed).

Hungarian is byte-identical, and that is measured rather than read:
scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses
a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict.
Its own first version filtered the capture through an ASCII-Hungarian word list and missed
seven real literals — the R-565 class. The filter is gone.

Nothing on the wire moved, and wire goldens now hold it there: the report's health
warnings and every notify event message stay Hungarian, because the hub MAILS the
controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those.

MinAgent: 0.131.0 (unchanged). No hub release needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 10:15:56 +02:00

112 lines
4.6 KiB
Go

package web
import (
"os"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
)
// R-487 — a removed app whose unit was kept renders on the backups page with the restore that brings
// it back; a page without such rows carries none of it (the negative control).
func TestR487_BackupsPageListsARemovedAppWithItsRestore(t *testing.T) {
data := splitTestData()
data["AppBackupRows"] = []AppBackupRow{
{StackName: "calibre-web", DisplayName: "Calibre-Web"},
{StackName: "gone-app", DisplayName: "Gone", Removed: true, RemovedUnitTime: "2026-09-13T00:30:00Z", StorageLabel: "HDD"},
}
html := renderBackupPage(t, "backups_apps", data)
for _, want := range []string{
`name="stack_name" value="gone-app"`,
`name="snapshot_id" value="helyi"`,
`action="/backup/restore"`,
`class="layer-badge">Megtartva<`,
} {
if !strings.Contains(html, want) {
t.Errorf("removed row must render %q", want)
}
}
if strings.Count(html, `class="layer-badge">Megtartva<`) != 1 {
t.Error("the kept badge must render for the removed row only")
}
control := renderBackupPage(t, "backups_apps", splitTestData())
if strings.Contains(control, "Megtartva") || strings.Contains(control, `value="helyi"`) {
t.Error("negative control: a page with no removed app must not offer the restore")
}
}
// R-487 — the restore picker lists removed apps in their own group.
func TestR487_RestorePickerListsRemovedApps(t *testing.T) {
data := splitTestData()
data["RemovedApps"] = []backup.RemovedAppUnit{{StackName: "gone-app", DisplayName: "Gone"}}
html := renderBackupPage(t, "backups_restore", data)
if !strings.Contains(html, `<option value="gone-app" data-removed="true"`) {
t.Error("removed app must be an option in the picker")
}
if strings.Contains(renderBackupPage(t, "backups_restore", splitTestData()), "data-removed") {
t.Error("negative control: no removed apps, no group")
}
}
// r487Provider is the smallest stack provider that can say "this app is not deployed".
type r487Provider struct{ deployed []string }
func (p *r487Provider) GetStackComposePath(name string) (string, bool) {
for _, s := range p.deployed {
if s == name {
return "/stacks/" + name + "/docker-compose.yml", true
}
}
return "", false
}
func (p *r487Provider) ListDeployedStacks() []backup.StackSummary {
out := make([]backup.StackSummary, 0, len(p.deployed))
for _, s := range p.deployed {
out = append(out, backup.StackSummary{Name: s})
}
return out
}
func (p *r487Provider) GetStackHDDMounts(string) []string { return nil }
func (p *r487Provider) GetStackHDDPath(string) string { return "" }
func (p *r487Provider) GetImportRoot() string { return "" }
func (p *r487Provider) GetDockerVolumes(string) []string { return nil }
func (p *r487Provider) StopStack(string) error { return nil }
func (p *r487Provider) StartStack(string) error { return nil }
func (p *r487Provider) RefreshAndIsRunning(string) bool { return true }
func (p *r487Provider) GetStackRecoveryInfo(string) (backup.RecoveryInfo, bool) {
return backup.RecoveryInfo{}, false
}
func (p *r487Provider) RecoverStackSecrets(string, []string) map[string]string { return nil }
func (p *r487Provider) RecreateStackDefinitionFromUnit(string, string, map[string]string) error {
return nil
}
func (p *r487Provider) StartStackServices(string, []string) error { return nil }
func (p *r487Provider) GetStackClassifiedBinds(string) ([]backup.ClassifiedBind, bool) {
return nil, false
}
// R-487 — the page's row builder appends a removed app's kept unit after the deployed rows, from the
// real manager over a real drive layout (the seam is the provider, and it says only "not deployed").
func TestR487_BuildAppBackupRowsAppendsRemovedApps(t *testing.T) {
s := testServer(t)
sys := t.TempDir()
cfg := &config.Config{}
cfg.Paths.SystemDataPath = sys
mgr := backup.NewManager(cfg, s.settings, s.logger)
mgr.SetStackProvider(&r487Provider{})
s.backupMgr = mgr
unit := backup.RecoveryUnitPath(backup.NamespaceRootFor(sys, sys), "gone-app")
if err := os.MkdirAll(unit, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(backup.UnitManifestFile(unit), []byte(`{"schema_version":2,"app_name":"gone-app","display_name":"Gone","created_at":"2026-09-12T02:15:29Z"}`), 0o644); err != nil {
t.Fatal(err)
}
rows := s.buildAppBackupRows(&backup.FullBackupStatus{}, "hu")
if len(rows) != 1 || rows[0].StackName != "gone-app" || !rows[0].Removed || rows[0].DisplayName != "Gone" || rows[0].RemovedUnitTime == "" {
t.Fatalf("want one removed row for gone-app, got %+v", rows)
}
}