package main import ( "context" "strings" "testing" "time" "github.com/kami/maven/internal/ipc" ) // Off is the default and it must mean "nothing to write with", not "permission // to refuse later". A daemonAPI with no seedStore writes no fact at all. func TestSeedRefusedWithoutTheFlag(t *testing.T) { d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}} _, err := d.SeedEvent(context.Background(), ipc.SeedEventReq{ Key: "cat_water_fountain", Value: "заправил", Ts: time.Now(), }) if err == nil { t.Fatal("seed succeeded with no seedStore") } if !strings.Contains(err.Error(), "-allow-seed") { t.Errorf("error does not name the flag: %v", err) } } // The whole point of the task: four seeds spread past the detector's floor // produce a proposal against the real daemon path, which is what nobody could // do before (Vikunja #518). Three seeds must NOT propose — MinEvents is four, // and a test that only checked the happy end would pass on an off-by-one. func TestSeedFourEventsProposesARoutine(t *testing.T) { ctx := context.Background() d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}, seedStore: newTestStore(t)} now := time.Now() var last ipc.SeedEventResp // Oldest first, three hours apart — past MinIntervalDays (two hours). for i := 3; i >= 0; i-- { var err error last, err = d.SeedEvent(ctx, ipc.SeedEventReq{ Key: "cat_water_fountain", Value: "заправил", Ts: now.Add(-time.Duration(i) * 3 * time.Hour), }) if err != nil { t.Fatalf("seed %d: %v", i, err) } if !last.Extracted { t.Fatalf("seed %d: no event extracted from a lexicon verb", i) } if i > 0 && last.Proposed { t.Fatalf("proposed after only %d events, MinEvents is 4", 4-i) } } if !last.Proposed { t.Fatal("four spaced events did not propose a routine") } if last.Action != "refill" || last.Object != "cat_water_fountain" { t.Errorf("wrong pair: %s/%s", last.Action, last.Object) } if last.IntervalDays < 0.1 { t.Errorf("interval %v — the detector saw a burst, not a rhythm", last.IntervalDays) } // The proposal is readable through the same list the /routines page uses, // which is the wiring an eval-lab fixture would not have proved. proposed, err := d.seedStore.ListProposedRoutines(ctx) if err != nil { t.Fatalf("list: %v", err) } if len(proposed) != 1 { t.Fatalf("expected 1 proposed routine, got %d", len(proposed)) } } // A value outside the action lexicon writes the fact and says it seeded // nothing. Silence here would read as four working seeds and a broken // detector. func TestSeedReportsWhenExtractionDeclines(t *testing.T) { d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}, seedStore: newTestStore(t)} resp, err := d.SeedEvent(context.Background(), ipc.SeedEventReq{ Key: "mood", Value: "ok", Ts: time.Now(), }) if err != nil { t.Fatalf("seed: %v", err) } if resp.FactID == 0 { t.Error("fact was not written") } if resp.Extracted || resp.EventID != 0 || resp.Proposed { t.Errorf("claimed an event for a non-action value: %+v", resp) } } // A seed with no timestamp is refused rather than defaulting to now: the only // reason this seam exists is the caller choosing when, so a zero Ts is a bug in // the caller and must not silently write a fact at the wrong time. func TestSeedRequiresAnExplicitTimestamp(t *testing.T) { d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}, seedStore: newTestStore(t)} if _, err := d.SeedEvent(context.Background(), ipc.SeedEventReq{ Key: "cat_water_fountain", Value: "заправил", }); err == nil { t.Fatal("seed accepted a zero timestamp") } }