package client import ( "errors" "os" "path/filepath" "testing" "github.com/m3tam3re/agent-lib/internal/gitsource" "github.com/m3tam3re/agent-lib/internal/lockfile" ) type countingNotifier struct { calls int titles []string failWith error } func (c *countingNotifier) Send(title, body string) error { c.calls++ c.titles = append(c.titles, title) return c.failWith } func skipPlanEntry(t *testing.T) PlanEntry { return PlanEntry{ Action: ActSkip, Type: "skills", Name: "alpha", RelPath: "skills/alpha", DstPath: filepath.Join(t.TempDir(), "skills", "alpha"), NewHash: "sha256:x", Reason: "locally modified; keeping the local version (upstream changed)", } } func executeForTest(t *testing.T, plan []PlanEntry) (*SyncReport, error) { t.Helper() dir := t.TempDir() paths := StatePaths{Root: dir, Manifest: filepath.Join(dir, "manifest.json"), LogFile: filepath.Join(dir, "sync.log")} manifest := manifestWith(map[string]*ManifestItem{ "skills/alpha": {Name: "alpha", Type: "skills", Hash: "sha256:old"}, }) var tree *gitsource.GitTree return ExecutePlan(tree, plan, manifest, paths, "rev123", os.Stdout) } func TestToastFiredOnceForWarnings(t *testing.T) { orig := Notifier t.Cleanup(func() { Notifier = orig }) fake := &countingNotifier{} Notifier = fake _, err := executeForTest(t, []PlanEntry{skipPlanEntry(t), { Action: ActLeave, Type: "skills", Name: "mine", Reason: "unmanaged local item; left untouched", }}) if err != nil { t.Fatal(err) } if fake.calls != 1 { t.Errorf("exactly one toast per sync with warnings, got %d", fake.calls) } if len(fake.titles) != 1 || fake.titles[0] != "agent-lib sync" { t.Errorf("unexpected toast titles: %v", fake.titles) } } func TestNoToastForRoutineSync(t *testing.T) { orig := Notifier t.Cleanup(func() { Notifier = orig }) fake := &countingNotifier{} Notifier = fake _, err := executeForTest(t, []PlanEntry{{ Action: ActKeep, Type: "skills", Name: "alpha", RelPath: "skills/alpha", DstPath: filepath.Join(t.TempDir(), "skills", "alpha"), NewHash: "sha256:x", }}) if err != nil { t.Fatal(err) } if fake.calls != 0 { t.Errorf("routine sync must stay silent, got %d toasts", fake.calls) } } func TestToastFailureNeverFailsSync(t *testing.T) { orig := Notifier t.Cleanup(func() { Notifier = orig }) Notifier = &countingNotifier{failWith: errors.New("no interactive session")} report, err := executeForTest(t, []PlanEntry{skipPlanEntry(t)}) if err != nil { t.Fatalf("toast failure must not fail the sync: %v", err) } if report.Skipped != 1 { t.Errorf("skip must still be applied: %+v", report) } if _, err := os.Stat(filepath.Join(t.TempDir())); err != nil { t.Fatal(err) } } func TestPlanEntryFieldCoverage(t *testing.T) { e := skipPlanEntry(t) if e.Action != ActSkip || e.Type != lockfile.TypeSkill || e.Reason == "" { t.Fatalf("plan entry incomplete: %+v", e) } }