When upgrading gt on an existing installation without .installed.json, formulas that exist but don't match embedded were incorrectly marked as "modified" (implying user customization). Now they're marked "untracked" and are safe to update since there's no record of user modification. This improves the upgrade experience: - "modified" = tracked file user changed (skip update) - "untracked" = file exists but not tracked (safe to update) Adds 3 new tests for untracked scenarios. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
749 lines
20 KiB
Go
749 lines
20 KiB
Go
package formula
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestGetEmbeddedFormulas verifies embedded formulas can be read and hashed.
|
|
func TestGetEmbeddedFormulas(t *testing.T) {
|
|
embedded, err := getEmbeddedFormulas()
|
|
if err != nil {
|
|
t.Fatalf("getEmbeddedFormulas() error: %v", err)
|
|
}
|
|
if len(embedded) == 0 {
|
|
t.Error("should have embedded formulas")
|
|
}
|
|
|
|
// Verify at least one known formula exists
|
|
if _, ok := embedded["mol-deacon-patrol.formula.toml"]; !ok {
|
|
t.Error("should contain mol-deacon-patrol.formula.toml")
|
|
}
|
|
|
|
// Verify hashes are valid hex strings
|
|
for name, hash := range embedded {
|
|
if len(hash) != 64 {
|
|
t.Errorf("%s hash has wrong length: %d", name, len(hash))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProvisionFormulas_FreshInstall tests provisioning to an empty directory.
|
|
func TestProvisionFormulas_FreshInstall(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
count, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
if count == 0 {
|
|
t.Error("should have provisioned at least one formula")
|
|
}
|
|
|
|
// Verify formulas directory was created
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
if _, err := os.Stat(formulasDir); os.IsNotExist(err) {
|
|
t.Error("formulas directory should exist")
|
|
}
|
|
|
|
// Verify .installed.json was created
|
|
installedPath := filepath.Join(formulasDir, ".installed.json")
|
|
if _, err := os.Stat(installedPath); os.IsNotExist(err) {
|
|
t.Error(".installed.json should exist")
|
|
}
|
|
|
|
// Verify installed record contains the right checksums
|
|
installed, err := loadInstalledRecord(formulasDir)
|
|
if err != nil {
|
|
t.Fatalf("loadInstalledRecord() error: %v", err)
|
|
}
|
|
if len(installed.Formulas) != count {
|
|
t.Errorf("installed record has %d entries, want %d", len(installed.Formulas), count)
|
|
}
|
|
}
|
|
|
|
// TestProvisionFormulas_SkipsExisting tests that existing files are not overwritten.
|
|
func TestProvisionFormulas_SkipsExisting(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create formulas directory with a custom formula
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
if err := os.MkdirAll(formulasDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
customContent := []byte("# Custom user formula\nformula = \"mol-deacon-patrol\"\n")
|
|
customPath := filepath.Join(formulasDir, "mol-deacon-patrol.formula.toml")
|
|
if err := os.WriteFile(customPath, customContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Provision formulas
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Verify custom content was NOT overwritten
|
|
content, err := os.ReadFile(customPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(content) != string(customContent) {
|
|
t.Error("existing formula should not have been overwritten")
|
|
}
|
|
}
|
|
|
|
// TestCheckFormulaHealth_AllOK tests when all formulas are up to date.
|
|
func TestCheckFormulaHealth_AllOK(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision fresh
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Check health
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("CheckFormulaHealth() error: %v", err)
|
|
}
|
|
|
|
if report.Outdated != 0 {
|
|
t.Errorf("Outdated = %d, want 0", report.Outdated)
|
|
}
|
|
if report.Missing != 0 {
|
|
t.Errorf("Missing = %d, want 0", report.Missing)
|
|
}
|
|
if report.Modified != 0 {
|
|
t.Errorf("Modified = %d, want 0", report.Modified)
|
|
}
|
|
if report.OK == 0 {
|
|
t.Error("OK should be > 0")
|
|
}
|
|
}
|
|
|
|
// TestCheckFormulaHealth_UserModified tests detection of user-modified formulas.
|
|
func TestCheckFormulaHealth_UserModified(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision fresh
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Modify a formula
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
formulaPath := filepath.Join(formulasDir, "mol-deacon-patrol.formula.toml")
|
|
modifiedContent := []byte("# User modified this\nformula = \"mol-deacon-patrol\"\nversion = 999\n")
|
|
if err := os.WriteFile(formulaPath, modifiedContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check health
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("CheckFormulaHealth() error: %v", err)
|
|
}
|
|
|
|
if report.Modified != 1 {
|
|
t.Errorf("Modified = %d, want 1", report.Modified)
|
|
}
|
|
|
|
// Verify the specific formula is marked as modified
|
|
found := false
|
|
for _, f := range report.Formulas {
|
|
if f.Name == "mol-deacon-patrol.formula.toml" {
|
|
if f.Status != "modified" {
|
|
t.Errorf("mol-deacon-patrol status = %q, want %q", f.Status, "modified")
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("mol-deacon-patrol.formula.toml not found in report")
|
|
}
|
|
}
|
|
|
|
// TestCheckFormulaHealth_Missing tests detection of deleted formulas.
|
|
func TestCheckFormulaHealth_Missing(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision fresh
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Delete a formula
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
formulaPath := filepath.Join(formulasDir, "mol-deacon-patrol.formula.toml")
|
|
if err := os.Remove(formulaPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check health
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("CheckFormulaHealth() error: %v", err)
|
|
}
|
|
|
|
if report.Missing != 1 {
|
|
t.Errorf("Missing = %d, want 1", report.Missing)
|
|
}
|
|
}
|
|
|
|
// TestCheckFormulaHealth_Outdated simulates an outdated formula.
|
|
func TestCheckFormulaHealth_Outdated(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision fresh
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Simulate "old" installed record by changing the installed hash for a formula
|
|
// This mimics what happens when a new binary has updated formula content
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
installed, err := loadInstalledRecord(formulasDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
embedded, err := getEmbeddedFormulas()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Pick a formula that exists
|
|
var targetFormula string
|
|
for name := range installed.Formulas {
|
|
targetFormula = name
|
|
break
|
|
}
|
|
if targetFormula == "" {
|
|
t.Skip("no formulas installed")
|
|
}
|
|
|
|
// Write a file that simulates "old version" - content differs from embedded
|
|
formulaPath := filepath.Join(formulasDir, targetFormula)
|
|
oldContent := []byte("# Old version of formula\n")
|
|
if err := os.WriteFile(formulaPath, oldContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Update installed record to match the old content's hash
|
|
hash := sha256.Sum256(oldContent)
|
|
installed.Formulas[targetFormula] = hex.EncodeToString(hash[:])
|
|
|
|
if err := saveInstalledRecord(formulasDir, installed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Now: file matches what we "installed" but differs from embedded = outdated
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("CheckFormulaHealth() error: %v", err)
|
|
}
|
|
|
|
if report.Outdated != 1 {
|
|
t.Errorf("Outdated = %d, want 1", report.Outdated)
|
|
}
|
|
|
|
// Verify the embedded hash is different from installed
|
|
embeddedHash := embedded[targetFormula]
|
|
if embeddedHash == installed.Formulas[targetFormula] {
|
|
t.Error("embedded hash should differ from installed hash for this test")
|
|
}
|
|
}
|
|
|
|
// TestUpdateFormulas_UpdatesOutdated tests that outdated formulas are updated.
|
|
func TestUpdateFormulas_UpdatesOutdated(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision fresh
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Simulate outdated formula
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
installed, err := loadInstalledRecord(formulasDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var targetFormula string
|
|
for name := range installed.Formulas {
|
|
targetFormula = name
|
|
break
|
|
}
|
|
if targetFormula == "" {
|
|
t.Skip("no formulas installed")
|
|
}
|
|
|
|
// Write old content
|
|
formulaPath := filepath.Join(formulasDir, targetFormula)
|
|
oldContent := []byte("# Old version\n")
|
|
if err := os.WriteFile(formulaPath, oldContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Update installed record with old content's hash
|
|
hash := sha256.Sum256(oldContent)
|
|
installed.Formulas[targetFormula] = hex.EncodeToString(hash[:])
|
|
if err := saveInstalledRecord(formulasDir, installed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Run update
|
|
updated, skipped, reinstalled, err := UpdateFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("UpdateFormulas() error: %v", err)
|
|
}
|
|
|
|
if updated != 1 {
|
|
t.Errorf("updated = %d, want 1", updated)
|
|
}
|
|
if skipped != 0 {
|
|
t.Errorf("skipped = %d, want 0", skipped)
|
|
}
|
|
if reinstalled != 0 {
|
|
t.Errorf("reinstalled = %d, want 0", reinstalled)
|
|
}
|
|
|
|
// Verify file was updated
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("CheckFormulaHealth() error: %v", err)
|
|
}
|
|
if report.Outdated != 0 {
|
|
t.Errorf("after update, Outdated = %d, want 0", report.Outdated)
|
|
}
|
|
}
|
|
|
|
// TestUpdateFormulas_SkipsModified tests that user-modified formulas are skipped.
|
|
func TestUpdateFormulas_SkipsModified(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision fresh
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Modify a formula (user customization)
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
installed, err := loadInstalledRecord(formulasDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var targetFormula string
|
|
for name := range installed.Formulas {
|
|
targetFormula = name
|
|
break
|
|
}
|
|
if targetFormula == "" {
|
|
t.Skip("no formulas installed")
|
|
}
|
|
|
|
// Write different content that doesn't match installed hash
|
|
formulaPath := filepath.Join(formulasDir, targetFormula)
|
|
modifiedContent := []byte("# User customized this formula\nformula = \"custom\"\n")
|
|
if err := os.WriteFile(formulaPath, modifiedContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Run update - should skip the modified formula
|
|
_, skipped, _, err := UpdateFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("UpdateFormulas() error: %v", err)
|
|
}
|
|
|
|
if skipped != 1 {
|
|
t.Errorf("skipped = %d, want 1", skipped)
|
|
}
|
|
|
|
// Verify file was NOT changed
|
|
content, err := os.ReadFile(formulaPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(content) != string(modifiedContent) {
|
|
t.Error("modified formula should not have been changed")
|
|
}
|
|
}
|
|
|
|
// TestUpdateFormulas_ReinstallsMissing tests that deleted formulas are reinstalled.
|
|
func TestUpdateFormulas_ReinstallsMissing(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision fresh
|
|
_, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Delete a formula
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
installed, err := loadInstalledRecord(formulasDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var targetFormula string
|
|
for name := range installed.Formulas {
|
|
targetFormula = name
|
|
break
|
|
}
|
|
if targetFormula == "" {
|
|
t.Skip("no formulas installed")
|
|
}
|
|
|
|
formulaPath := filepath.Join(formulasDir, targetFormula)
|
|
if err := os.Remove(formulaPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Run update
|
|
_, _, reinstalled, err := UpdateFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("UpdateFormulas() error: %v", err)
|
|
}
|
|
|
|
if reinstalled != 1 {
|
|
t.Errorf("reinstalled = %d, want 1", reinstalled)
|
|
}
|
|
|
|
// Verify file was restored
|
|
if _, err := os.Stat(formulaPath); os.IsNotExist(err) {
|
|
t.Error("missing formula should have been reinstalled")
|
|
}
|
|
}
|
|
|
|
// TestUpdateFormulas_InstallsNew tests that new formulas are installed.
|
|
func TestUpdateFormulas_InstallsNew(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create directory structure but with empty installed record
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
if err := os.MkdirAll(formulasDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write empty installed record
|
|
emptyInstalled := &InstalledRecord{Formulas: make(map[string]string)}
|
|
if err := saveInstalledRecord(formulasDir, emptyInstalled); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Run update - should install all formulas as "new"
|
|
updated, skipped, reinstalled, err := UpdateFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("UpdateFormulas() error: %v", err)
|
|
}
|
|
|
|
// All formulas should be installed
|
|
embedded, err := getEmbeddedFormulas()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
total := updated + reinstalled
|
|
if total != len(embedded) {
|
|
t.Errorf("total installed = %d, want %d", total, len(embedded))
|
|
}
|
|
if skipped != 0 {
|
|
t.Errorf("skipped = %d, want 0", skipped)
|
|
}
|
|
}
|
|
|
|
// TestInstalledRecordPersistence tests that the installed record survives across operations.
|
|
func TestInstalledRecordPersistence(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Provision
|
|
count, err := ProvisionFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("ProvisionFormulas() error: %v", err)
|
|
}
|
|
|
|
// Load and verify
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
installed, err := loadInstalledRecord(formulasDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(installed.Formulas) != count {
|
|
t.Errorf("installed has %d formulas, want %d", len(installed.Formulas), count)
|
|
}
|
|
|
|
// Verify file is valid JSON
|
|
installedPath := filepath.Join(formulasDir, ".installed.json")
|
|
data, err := os.ReadFile(installedPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var decoded InstalledRecord
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Errorf("installed.json is not valid JSON: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCheckFormulaHealth_NewFormula tests detection of new formulas that were never installed.
|
|
func TestCheckFormulaHealth_NewFormula(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create formulas directory with empty installed record
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
if err := os.MkdirAll(formulasDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write empty installed record - simulates pre-existing install without this formula
|
|
emptyInstalled := &InstalledRecord{Formulas: make(map[string]string)}
|
|
if err := saveInstalledRecord(formulasDir, emptyInstalled); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check health - all embedded formulas should be "new"
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("CheckFormulaHealth() error: %v", err)
|
|
}
|
|
|
|
embedded, _ := getEmbeddedFormulas()
|
|
if report.New != len(embedded) {
|
|
t.Errorf("New = %d, want %d", report.New, len(embedded))
|
|
}
|
|
if report.OK != 0 {
|
|
t.Errorf("OK = %d, want 0", report.OK)
|
|
}
|
|
}
|
|
|
|
// TestCheckFormulaHealth_Untracked tests detection of files that exist but aren't
|
|
// in .installed.json and don't match embedded (e.g., from older gt version).
|
|
func TestCheckFormulaHealth_Untracked(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Get embedded formulas
|
|
embedded, err := getEmbeddedFormulas()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create formulas directory without .installed.json
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
if err := os.MkdirAll(formulasDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write formula files with different content (simulating older version)
|
|
for name := range embedded {
|
|
oldContent := []byte("# old version of " + name + "\n[molecule]\nid = \"test\"\n")
|
|
if err := os.WriteFile(filepath.Join(formulasDir, name), oldContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Check health - all should be "untracked" (not "modified" since not tracked)
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("CheckFormulaHealth() error: %v", err)
|
|
}
|
|
|
|
if report.Untracked != len(embedded) {
|
|
t.Errorf("Untracked = %d, want %d", report.Untracked, len(embedded))
|
|
}
|
|
if report.Modified != 0 {
|
|
t.Errorf("Modified = %d, want 0 (untracked files shouldn't be marked as modified)", report.Modified)
|
|
}
|
|
if report.OK != 0 {
|
|
t.Errorf("OK = %d, want 0", report.OK)
|
|
}
|
|
|
|
// Verify all formulas have status "untracked"
|
|
for _, f := range report.Formulas {
|
|
if f.Status != "untracked" {
|
|
t.Errorf("formula %s status = %q, want %q", f.Name, f.Status, "untracked")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUpdateFormulas_UpdatesUntracked tests that untracked files get updated.
|
|
func TestUpdateFormulas_UpdatesUntracked(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Get embedded formulas
|
|
embedded, err := getEmbeddedFormulas()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create formulas directory without .installed.json
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
if err := os.MkdirAll(formulasDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write formula files with different content (simulating older version)
|
|
for name := range embedded {
|
|
oldContent := []byte("# old version of " + name + "\n[molecule]\nid = \"test\"\n")
|
|
if err := os.WriteFile(filepath.Join(formulasDir, name), oldContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Run update - should update all untracked formulas
|
|
updated, skipped, reinstalled, err := UpdateFormulas(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("UpdateFormulas() error: %v", err)
|
|
}
|
|
|
|
// All untracked files should be updated (counted as "updated", not "reinstalled")
|
|
if updated != len(embedded) {
|
|
t.Errorf("updated = %d, want %d", updated, len(embedded))
|
|
}
|
|
if skipped != 0 {
|
|
t.Errorf("skipped = %d, want 0", skipped)
|
|
}
|
|
if reinstalled != 0 {
|
|
t.Errorf("reinstalled = %d, want 0", reinstalled)
|
|
}
|
|
|
|
// Verify files now match embedded
|
|
for name, expectedHash := range embedded {
|
|
content, err := os.ReadFile(filepath.Join(formulasDir, name))
|
|
if err != nil {
|
|
t.Fatalf("reading %s: %v", name, err)
|
|
}
|
|
actualHash := computeHash(content)
|
|
if actualHash != expectedHash {
|
|
t.Errorf("%s hash mismatch after update", name)
|
|
}
|
|
}
|
|
|
|
// Verify .installed.json was created with correct hashes
|
|
installed, err := loadInstalledRecord(formulasDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, expectedHash := range embedded {
|
|
if installed.Formulas[name] != expectedHash {
|
|
t.Errorf(".installed.json hash for %s = %q, want %q",
|
|
name, installed.Formulas[name], expectedHash)
|
|
}
|
|
}
|
|
|
|
// Re-run health check - should be all OK now
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if report.OK != len(embedded) {
|
|
t.Errorf("after update, OK = %d, want %d", report.OK, len(embedded))
|
|
}
|
|
if report.Untracked != 0 {
|
|
t.Errorf("after update, Untracked = %d, want 0", report.Untracked)
|
|
}
|
|
}
|
|
|
|
// TestCheckFormulaHealth_MixedScenarios tests a mix of OK, untracked, and modified.
|
|
func TestCheckFormulaHealth_MixedScenarios(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Get embedded formulas
|
|
embedded, err := getEmbeddedFormulas()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(embedded) < 3 {
|
|
t.Skip("need at least 3 formulas for this test")
|
|
}
|
|
|
|
formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
|
|
if err := os.MkdirAll(formulasDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Prepare installed record with only some formulas tracked
|
|
installed := &InstalledRecord{Formulas: make(map[string]string)}
|
|
|
|
i := 0
|
|
var okFormula, untrackedFormula, modifiedFormula string
|
|
for name := range embedded {
|
|
switch i {
|
|
case 0:
|
|
// First formula: write matching content, track it -> should be OK
|
|
okFormula = name
|
|
content, _ := formulasFS.ReadFile("formulas/" + name)
|
|
if err := os.WriteFile(filepath.Join(formulasDir, name), content, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
installed.Formulas[name] = computeHash(content)
|
|
|
|
case 1:
|
|
// Second formula: write old content, don't track -> should be untracked
|
|
untrackedFormula = name
|
|
oldContent := []byte("# untracked old version\n[molecule]\nid = \"test\"\n")
|
|
if err := os.WriteFile(filepath.Join(formulasDir, name), oldContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Don't add to installed record
|
|
|
|
case 2:
|
|
// Third formula: write different content, track with original hash -> should be modified
|
|
modifiedFormula = name
|
|
originalContent, _ := formulasFS.ReadFile("formulas/" + name)
|
|
originalHash := computeHash(originalContent)
|
|
modifiedContent := []byte("# user modified version\n[molecule]\nid = \"custom\"\n")
|
|
if err := os.WriteFile(filepath.Join(formulasDir, name), modifiedContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
installed.Formulas[name] = originalHash // Track with original hash
|
|
}
|
|
i++
|
|
if i >= 3 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := saveInstalledRecord(formulasDir, installed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check health
|
|
report, err := CheckFormulaHealth(tmpDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Find status of each test formula
|
|
statusMap := make(map[string]string)
|
|
for _, f := range report.Formulas {
|
|
statusMap[f.Name] = f.Status
|
|
}
|
|
|
|
if statusMap[okFormula] != "ok" {
|
|
t.Errorf("formula %s status = %q, want %q", okFormula, statusMap[okFormula], "ok")
|
|
}
|
|
if statusMap[untrackedFormula] != "untracked" {
|
|
t.Errorf("formula %s status = %q, want %q", untrackedFormula, statusMap[untrackedFormula], "untracked")
|
|
}
|
|
if statusMap[modifiedFormula] != "modified" {
|
|
t.Errorf("formula %s status = %q, want %q", modifiedFormula, statusMap[modifiedFormula], "modified")
|
|
}
|
|
}
|