ajhahn.de
← eeco
Go 215 lines
package workflow

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/ajhahnde/eeco/internal/queue"
)

func TestLoadHistory_MissingFileIsEmpty(t *testing.T) {
	dir := t.TempDir()
	h, err := LoadHistory(dir)
	if err != nil {
		t.Fatalf("missing file must not error, got %v", err)
	}
	if len(h.Records) != 0 {
		t.Errorf("missing file: got %d records, want 0", len(h.Records))
	}
}

func TestLoadHistory_CorruptFileDegradesToEmpty(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, HistoryFilename), []byte("{not json"), 0o644); err != nil {
		t.Fatal(err)
	}
	h, err := LoadHistory(dir)
	if err != nil {
		t.Fatalf("corrupt file must degrade silently, got %v", err)
	}
	if len(h.Records) != 0 {
		t.Errorf("corrupt file: got %d records, want 0", len(h.Records))
	}
}

func TestHistory_SaveLoadRoundTrip(t *testing.T) {
	dir := t.TempDir()
	in := History{Records: []HistoryRecord{
		{
			SignalKind:      SignalCommitType,
			SignalKey:       "fix",
			CountAtProposal: 5,
			QueueKind:       "evolve",
			QueueTitle:      "Workflow candidate: fix-workflow",
			ProposedAt:      "2026-05-24T10:00:00Z",
		},
		{
			SignalKind:      SignalCommitType,
			SignalKey:       "docs",
			CountAtProposal: 4,
			QueueKind:       "evolve",
			QueueTitle:      "Workflow candidate: docs-workflow",
			ProposedAt:      "2026-05-24T10:00:00Z",
			Resolved:        true,
			ResolvedAt:      "2026-05-25T09:00:00Z",
		},
	}}
	if err := SaveHistory(dir, in); err != nil {
		t.Fatal(err)
	}
	out, err := LoadHistory(dir)
	if err != nil {
		t.Fatal(err)
	}
	if len(out.Records) != len(in.Records) {
		t.Fatalf("len mismatch: got %d, want %d", len(out.Records), len(in.Records))
	}
	for i, r := range in.Records {
		if out.Records[i] != r {
			t.Errorf("record %d round-trip: got %+v, want %+v", i, out.Records[i], r)
		}
	}
}

func TestHistory_OmitemptyForResolvedDefaults(t *testing.T) {
	dir := t.TempDir()
	in := History{Records: []HistoryRecord{{
		SignalKind:      SignalCommitType,
		SignalKey:       "fix",
		CountAtProposal: 3,
		QueueKind:       "evolve",
		QueueTitle:      "Workflow candidate: fix-workflow",
		ProposedAt:      "2026-05-24T10:00:00Z",
	}}}
	if err := SaveHistory(dir, in); err != nil {
		t.Fatal(err)
	}
	b, err := os.ReadFile(filepath.Join(dir, HistoryFilename))
	if err != nil {
		t.Fatal(err)
	}
	s := string(b)
	if strings.Contains(s, "\"resolved\"") || strings.Contains(s, "\"resolved_at\"") {
		t.Errorf("default record must omit resolved fields on wire:\n%s", s)
	}
}

func TestHasProposed(t *testing.T) {
	h := History{Records: []HistoryRecord{
		{SignalKind: SignalCommitType, SignalKey: "fix"},
		{SignalKind: SignalCommitType, SignalKey: "docs", Resolved: true},
	}}
	cases := []struct {
		kind, key string
		want      bool
	}{
		{SignalCommitType, "fix", true},
		{SignalCommitType, "docs", true},
		{SignalCommitType, "feat", false},
		{"other-kind", "fix", false},
	}
	for _, tc := range cases {
		if got := h.HasProposed(tc.kind, tc.key); got != tc.want {
			t.Errorf("HasProposed(%q,%q) = %v, want %v", tc.kind, tc.key, got, tc.want)
		}
	}
}

func TestReconcileHistory_TicksOpenItemToResolved(t *testing.T) {
	dir := t.TempDir()
	// Pre-fill queue with an open item, then mark it resolved by
	// rewriting the queue file with `- [x]`.
	if err := queue.Append(dir, queue.Item{
		Kind:    "evolve",
		Title:   "Workflow candidate: fix-workflow",
		Project: "proj",
		Date:    time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
	body, err := os.ReadFile(filepath.Join(dir, queue.Filename))
	if err != nil {
		t.Fatal(err)
	}
	rewritten := strings.Replace(string(body), "- [ ] **evolve**", "- [x] **evolve**", 1)
	if err := os.WriteFile(filepath.Join(dir, queue.Filename), []byte(rewritten), 0o644); err != nil {
		t.Fatal(err)
	}

	h := History{Records: []HistoryRecord{{
		SignalKind: SignalCommitType,
		SignalKey:  "fix",
		QueueKind:  "evolve",
		QueueTitle: "Workflow candidate: fix-workflow",
	}}}
	now := time.Date(2026, 5, 25, 12, 0, 0, 0, time.UTC)
	out, changed := ReconcileHistory(dir, h, now)
	if !changed {
		t.Fatalf("changed must be true when an open item flipped")
	}
	if !out.Records[0].Resolved {
		t.Errorf("record must flip to resolved")
	}
	if out.Records[0].ResolvedAt == "" {
		t.Errorf("ResolvedAt must be set on flip")
	}
}

func TestReconcileHistory_AlreadyResolvedStays(t *testing.T) {
	dir := t.TempDir()
	h := History{Records: []HistoryRecord{{
		SignalKind: SignalCommitType,
		SignalKey:  "fix",
		QueueKind:  "evolve",
		QueueTitle: "Workflow candidate: fix-workflow",
		Resolved:   true,
		ResolvedAt: "2026-05-20T10:00:00Z",
	}}}
	out, changed := ReconcileHistory(dir, h, time.Now())
	if changed {
		t.Errorf("an already-resolved record must not trigger change")
	}
	if out.Records[0].ResolvedAt != "2026-05-20T10:00:00Z" {
		t.Errorf("ResolvedAt must not be overwritten: got %q", out.Records[0].ResolvedAt)
	}
}

func TestReconcileHistory_OpenItemStaysUnresolved(t *testing.T) {
	dir := t.TempDir()
	if err := queue.Append(dir, queue.Item{
		Kind:    "evolve",
		Title:   "Workflow candidate: fix-workflow",
		Project: "proj",
		Date:    time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
	h := History{Records: []HistoryRecord{{
		SignalKind: SignalCommitType,
		SignalKey:  "fix",
		QueueKind:  "evolve",
		QueueTitle: "Workflow candidate: fix-workflow",
	}}}
	out, changed := ReconcileHistory(dir, h, time.Now())
	if changed {
		t.Errorf("an open queue item must not flip the record")
	}
	if out.Records[0].Resolved {
		t.Errorf("record must stay unresolved while queue row is open")
	}
}

func TestQueueResolved_MissingFile(t *testing.T) {
	dir := t.TempDir()
	ok, err := queue.Resolved(dir, "evolve", "missing")
	if err != nil {
		t.Fatalf("missing queue must not error, got %v", err)
	}
	if ok {
		t.Errorf("missing queue: got ok=true, want false")
	}
}