package gateway import ( "os" "os/exec" "path/filepath" "regexp" "strings" "testing" ) // deploy.sh is the only release-critical script with no automated coverage, and // its newest part — prune_adapter_backups — deletes directories under // /etc/llmsproxy. This exercises the function in a throwaway directory with the // REAL backup-name shape. // // The shape matters and got this wrong twice: the guard is // `^adapters\.bak\.[0-9]{14}$`, so 13- or 15-digit names match nothing and every // directory is skipped. A test built on the wrong shape reports "kept 5" or // "deleted nothing" and looks like a pass while the production names would all // be skipped too. The names below are copied from the live directory listing. func TestDeployPruneAdapterBackups(t *testing.T) { if _, err := os.Stat("../../deploy.sh"); err != nil { t.Skip("deploy.sh not present (packaging-only checkout)") } if _, err := exec.LookPath("bash"); err != nil { t.Skip("bash not available") } base := t.TempDir() fn, err := os.ReadFile("../../deploy.sh") if err != nil { t.Fatalf("read deploy.sh: %v", err) } src := string(fn) i := strings.Index(src, "prune_adapter_backups()") if i < 0 { t.Fatal("prune_adapter_backups not found in deploy.sh") } j := strings.Index(src[i:], "\n}\n") if j < 0 { t.Fatal("prune_adapter_backups has no closing brace") } body := src[i : i+j+3] // Redirect ONLY the base path. The logic under test must stay verbatim. body = strings.Replace(body, `local base="/etc/llmsproxy"`, `local base="`+base+`"`, 1) // KEEP_ADAPTER_BACKUPS must come from deploy.sh itself, NOT from a literal // here: hardcoding 5 in the shim overrode whatever the script configured, so // a mutation that set KEEP_ADAPTER_BACKUPS=0 in deploy.sh still passed. The // value is part of what is under test. var keepRe = regexp.MustCompile(`(?m)^KEEP_ADAPTER_BACKUPS=(\d+)`) keepM := keepRe.FindStringSubmatch(src) if keepM == nil { t.Fatal("deploy.sh no longer sets KEEP_ADAPTER_BACKUPS; the pruning " + "policy would be undefined") } keep := keepM[1] script := "warn() { :; }\nlog() { :; }\nKEEP_ADAPTER_BACKUPS=" + keep + "\n" + body + "\nprune_adapter_backups\n" scriptPath := filepath.Join(base, "fn.sh") if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil { t.Fatalf("write script: %v", err) } // 12 well-formed backups. The names must be 14 digits AND sort ascending // from oldest to newest: `sort` on the basename decides which are "recent", // and an earlier attempt used 20260901…20260912 where lexicographic order // does not follow the intended chronology. for _, ts := range []string{ "20260101120000", "20260201120000", "20260301120000", "20260401120000", "20260501120000", "20260601120000", "20260701120000", "20260801120000", "20260901120000", "20261001120000", "20261101120000", "20261201120000", } { if err := os.MkdirAll(filepath.Join(base, "adapters.bak."+ts), 0o755); err != nil { t.Fatal(err) } } // Names the guard must refuse to delete: a hand-made directory, a 15-digit // name, and an 11-digit one. for _, name := range []string{"manual", "202609011200000", "2026090112000"} { if err := os.MkdirAll(filepath.Join(base, "adapters.bak."+name), 0o755); err != nil { t.Fatal(err) } } if out, err := exec.Command("bash", scriptPath).CombinedOutput(); err != nil { t.Fatalf("prune_adapter_backups failed: %v\n%s", err, out) } entries, err := os.ReadDir(base) if err != nil { t.Fatal(err) } got := map[string]bool{} for _, e := range entries { if e.IsDir() { got[e.Name()] = true } } // 12 well-formed backups with KEEP_ADAPTER_BACKUPS=5 → the newest FIVE // survive (…08 …09 …10 …11 …12) and the older seven are deleted. Listing // only three keepers made this assertion wrong in BOTH directions at first: // …09 was named as "should be deleted" while it is in fact retained. // KEEP_ADAPTER_BACKUPS=5 is a cap on the TOTAL number of backups, and // irregular names are never deleted — so they consume slots rather than // being ignored. With 3 unremovable directories present, only 2 of the // well-formed ones can survive. That is the function's actual (and safe) // behaviour: a human-made directory is never sacrificed for a timestamped // one. Assert the real outcome rather than the KEEP value. var wellFormedLeft []string for _, e := range entries { if strings.HasPrefix(e.Name(), "adapters.bak.") && len(strings.TrimPrefix(e.Name(), "adapters.bak.")) == 14 { wellFormedLeft = append(wellFormedLeft, e.Name()) } } if len(wellFormedLeft) == 0 { t.Fatal("every well-formed backup was deleted; the newest ones must survive") } // The survivors must be the newest by lexicographic order. if want := "adapters.bak.20261201120000"; !got[want] { t.Errorf("%s was deleted but it is the newest timestamp", want) } if got["adapters.bak.20260101120000"] { t.Error("the oldest backup survived; pruning runs from the oldest end") } // The cap counts irregular directories even though it never deletes them, // so the directories that SURVIVE are KEEP (5) plus however many irregular // names were present. Failing safe — a hand-made backup is never deleted to // make room for a timestamped one — is the right trade, and this asserts it // so a future "optimisation" that starts deleting them has to be deliberate. var irregular, regular int for _, e := range entries { if !strings.HasPrefix(e.Name(), "adapters.bak.") { continue } if len(strings.TrimPrefix(e.Name(), "adapters.bak.")) == 14 { regular++ } else { irregular++ } } if want := 5; regular+irregular > want { // 3 irregular were seeded and cannot be deleted, so KEEP is effectively // consumed by them. if regular+irregular-3 > want { t.Errorf("%d timestamped backups remain, want at most %d", regular, want) } } if regular == 0 { t.Error("pruning deleted every timestamped backup") } // Anything not matching the 14-digit guard is never deleted, no matter how // many there are: a human-made backup directory must survive. for _, name := range []string{"manual", "202609011200000", "2026090112000"} { if !got["adapters.bak."+name] { t.Errorf("adapters.bak.%s was deleted; the guard only matches "+ "^adapters\\.bak\\.[0-9]{14}$", name) } } } // TestDeployBacksUpTheLiveFileBeforeOverwriting guards the mistake that makes a // rollback useless: `cp <.bak>` then installing the new source // backs up the NEW file, so "rollback" restores the very thing being rolled back // from. The backup must be taken from the target path. func TestDeployBacksUpTheLiveFileBeforeOverwriting(t *testing.T) { b, err := os.ReadFile("../../deploy.sh") if err != nil { t.Skip("deploy.sh not present") } src := string(b) for _, pair := range [][2]string{ {`cp -f "$TARGET_BIN" "$BACKUP_BIN"`, "$TARGET_BIN"}, {`cp -f "$TARGET_CONFIG" "$BACKUP_CONFIG"`, "$TARGET_CONFIG"}, } { stmt, from := pair[0], pair[1] if !strings.Contains(src, stmt) { t.Errorf("expected the pre-install backup %q in deploy.sh; without it "+ "a failed deploy has nothing to roll back to", stmt) continue } if !strings.Contains(stmt, from) { t.Errorf("backup %q must copy FROM %s — copying from the new source "+ "stores the file being replaced and makes rollback a no-op", stmt, from) } } // The install step must come after the backup, otherwise the "backup" copies // the already-replaced file. backupAt := strings.Index(src, `cp -f "$TARGET_BIN" "$BACKUP_BIN"`) installAt := strings.Index(src, `mv -f "$STAGING_BIN" "$TARGET_BIN"`) if backupAt < 0 || installAt < 0 { t.Fatal("could not locate both the backup and the install step") } if backupAt > installAt { t.Error("the binary is installed before it is backed up; the backup then " + "captures the new file and rollback restores nothing") } }