2026-04-04 12:44:15 -03:00
|
|
|
package state
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-04 22:24:56 -03:00
|
|
|
"os"
|
2026-04-04 12:44:15 -03:00
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// TestWriteReadIntentRoundTrip runs one orchestration or CLI step.
|
|
|
|
|
// Signature: TestWriteReadIntentRoundTrip(t *testing.T).
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-04 12:44:15 -03:00
|
|
|
func TestWriteReadIntentRoundTrip(t *testing.T) {
|
|
|
|
|
p := filepath.Join(t.TempDir(), "intent.json")
|
|
|
|
|
if err := MustWriteIntent(p, IntentShuttingDown, "ups-threshold", "daemon"); err != nil {
|
|
|
|
|
t.Fatalf("write intent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
in, err := ReadIntent(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("read intent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if in.State != IntentShuttingDown {
|
|
|
|
|
t.Fatalf("expected state %q, got %q", IntentShuttingDown, in.State)
|
|
|
|
|
}
|
|
|
|
|
if in.Source != "daemon" {
|
|
|
|
|
t.Fatalf("expected source daemon, got %q", in.Source)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// TestMustWriteIntentRejectsUnknownState runs one orchestration or CLI step.
|
|
|
|
|
// Signature: TestMustWriteIntentRejectsUnknownState(t *testing.T).
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-04 12:44:15 -03:00
|
|
|
func TestMustWriteIntentRejectsUnknownState(t *testing.T) {
|
|
|
|
|
p := filepath.Join(t.TempDir(), "intent.json")
|
|
|
|
|
if err := MustWriteIntent(p, "weird", "x", "y"); err == nil {
|
|
|
|
|
t.Fatalf("expected invalid state error")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 22:24:56 -03:00
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// TestReadIntentAutoHealsCorruptJSON runs one orchestration or CLI step.
|
|
|
|
|
// Signature: TestReadIntentAutoHealsCorruptJSON(t *testing.T).
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-04 22:24:56 -03:00
|
|
|
func TestReadIntentAutoHealsCorruptJSON(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
p := filepath.Join(dir, "intent.json")
|
|
|
|
|
if err := os.WriteFile(p, []byte("{broken"), 0o640); err != nil {
|
|
|
|
|
t.Fatalf("write corrupt intent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
in, err := ReadIntent(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("read intent with auto-heal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if in.State != "" {
|
|
|
|
|
t.Fatalf("expected empty state after heal, got %q", in.State)
|
|
|
|
|
}
|
|
|
|
|
raw, err := os.ReadFile(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("read healed intent file: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(raw) != "{}\n" {
|
|
|
|
|
t.Fatalf("expected healed intent payload '{}', got %q", string(raw))
|
|
|
|
|
}
|
|
|
|
|
matches, err := filepath.Glob(filepath.Join(dir, "intent.json.corrupt-*"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("glob backup files: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(matches) != 1 {
|
|
|
|
|
t.Fatalf("expected 1 backup file, got %d (%v)", len(matches), matches)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 13:29:42 -03:00
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// TestParseIntentOutputParsesStructuredLine runs one orchestration or CLI step.
|
|
|
|
|
// Signature: TestParseIntentOutputParsesStructuredLine(t *testing.T).
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-05 13:29:42 -03:00
|
|
|
func TestParseIntentOutputParsesStructuredLine(t *testing.T) {
|
2026-04-07 13:13:58 -03:00
|
|
|
raw := `[ananke] 2026/04/05 11:24:49 intent=normal reason="guard-test-clear-2" source=drill updated_at=2026-04-05T16:24:33Z`
|
2026-04-05 13:29:42 -03:00
|
|
|
in, err := ParseIntentOutput(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("parse intent output: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if in.State != IntentNormal {
|
|
|
|
|
t.Fatalf("expected state=%q got=%q", IntentNormal, in.State)
|
|
|
|
|
}
|
|
|
|
|
if in.Reason != "guard-test-clear-2" {
|
|
|
|
|
t.Fatalf("expected reason parsed, got %q", in.Reason)
|
|
|
|
|
}
|
|
|
|
|
if in.Source != "drill" {
|
|
|
|
|
t.Fatalf("expected source parsed, got %q", in.Source)
|
|
|
|
|
}
|
|
|
|
|
if in.UpdatedAt.IsZero() {
|
|
|
|
|
t.Fatalf("expected updated_at parsed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// TestParseIntentOutputHandlesNone runs one orchestration or CLI step.
|
|
|
|
|
// Signature: TestParseIntentOutputHandlesNone(t *testing.T).
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-05 13:29:42 -03:00
|
|
|
func TestParseIntentOutputHandlesNone(t *testing.T) {
|
2026-04-07 13:13:58 -03:00
|
|
|
in, err := ParseIntentOutput(`[ananke] 2026/04/05 11:24:49 intent=none`)
|
2026-04-05 13:29:42 -03:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("parse none intent output: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if in.State != "" {
|
|
|
|
|
t.Fatalf("expected empty intent for none, got state=%q", in.State)
|
|
|
|
|
}
|
|
|
|
|
}
|