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")
}
}