71a9a59403
SeedEvent writes the fact at the caller's timestamp, extracts an event from it, and runs the same detectAndPropose the voice path runs. What a seed proves is therefore the daemon's own wiring, not the detector in isolation — which is what an eval-lab fixture would have proved, and is not what the four blocked tasks doubt. The flag is the real lock, not the authority rung. -allow-seed defaults off, and off means daemonAPI.seedStore is nil: the method has nothing to write with rather than permission to refuse. A box that can rewrite its own past says so in its boot log. Seeded facts carry source "seed:qa" and no Subject, so they never queue a Nexus resolution and stay identifiable for the wipe in V-494. Nothing else in the tree writes that source. Best-effort is not the shape here, unlike detectPattern: a seed that half worked is a QA result nobody can trust, so every step reports its own failure. Extraction declining is not a failure, and Extracted says so. Tests cover all four: refused with no flag, four spaced seeds propose and three do not, a value outside the lexicon writes the fact and claims no event, a zero timestamp is refused rather than defaulted to now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011x5DgnExQ5XZy8TZPs5bot
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
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")
|
|
}
|
|
}
|