112 lines
5.9 KiB
Go
112 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// novox/hq issue 639: the operator would not read the ask for ADR 0343 — thirty plain lines, every clause one per
|
|
// line. A record that needs the operator is asked with its brief: a headline that says what it changes for them, a
|
|
// summary, at most three points; who proposed it, the fingerprint and how to read the clauses are the Details.
|
|
|
|
// adr0343 is ADR 0343 as it was asked on 2026-10-13: thirteen clauses, and the brief its author would now give.
|
|
func adr0343() *Record {
|
|
r := &Record{Kind: KindDecision, Number: 343, Status: StatusProposed, Date: "2026-10-13", NeedsOperator: true,
|
|
Title: "A session's memory follows the operator: the agent module carries each project's memory folder to every node, and a file changed on two keeps both",
|
|
Author: "fix-367@node-b",
|
|
Brief: &Brief{Headline: "Your agents' memory follows you to every machine",
|
|
Summary: "What an agent learns in a project's memory is copied to every machine that runs the agent, so a session " +
|
|
"anywhere starts with it. A file changed on two machines keeps both versions and warns you; secrets in memory " +
|
|
"files are screened, though not perfectly.",
|
|
Points: []string{"Only memory folders travel: links and files over 128 KiB stay where they are",
|
|
"A conflict reaches you as a warning that needs you",
|
|
"A deleted file is marked, and a newer change elsewhere outlives the deletion"}}}
|
|
leads := []string{"Only the memory folders are carried", "Some memory files are not carried, and each is listed with why",
|
|
"The memory folders become paths the module's own process writes", "Memory moves only between homes of the same kind",
|
|
"The module's process on each node notices a change and puts it in the module's state",
|
|
"Every other node writes the new version where its own file holds no change it did not carry, and keeps the version replaced",
|
|
"When a file changed on two nodes, both versions are kept, the newer first",
|
|
"A conflict is sent to the operator as a warning that needs them",
|
|
"A deletion is carried as a removal marker, and a change outlives it",
|
|
"On a node's first carrying, every version learned before is kept", "One read-only tool shows what is carried",
|
|
"A value put straight into the state is not yet told apart from one the module put there", "How it is checked"}
|
|
for i, l := range leads {
|
|
r.Clauses = append(r.Clauses, Clause{N: i + 1, Text: "**" + l + ".** The statement.", Status: ClauseProposed, Source: SourceWritten})
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestADecisionAskReadsAtAGlance(t *testing.T) {
|
|
r := adr0343()
|
|
at := time.Date(2026, 10, 13, 14, 5, 0, 0, time.UTC)
|
|
a := composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12, Form: AskGlance})
|
|
if err := a.Check(at); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Headline != "Your agents' memory follows you to every machine" {
|
|
t.Errorf("the headline is not what it changes for the operator: %q", a.Headline)
|
|
}
|
|
if err := asks.Glance(a.Explanation); err != nil {
|
|
t.Errorf("%v:\n%s", err, a.Explanation)
|
|
}
|
|
if n := strings.Count(a.Explanation, "\n• "); n != 3 || !strings.HasPrefix(a.Explanation, "What an agent learns") {
|
|
t.Errorf("the summary and its %d points:\n%s", n, a.Explanation)
|
|
}
|
|
for _, c := range r.Clauses {
|
|
if strings.Contains(a.Explanation+a.Details, c.Lead()) {
|
|
t.Errorf("the ask lists the clause %d: %q", c.N, c.Lead())
|
|
}
|
|
}
|
|
details := strings.Split(a.Details, "\n")
|
|
if len(details) != 2 || !strings.Contains(details[0], "ADR 0343, proposed by fix-367@node-b; the knowledge base's own review approved it.") ||
|
|
!strings.Contains(details[0], "Fingerprint "+fingerprint(r.Digest())+".") ||
|
|
details[1] != "Read its 13 clauses with the knowledge base's get, number 343." {
|
|
t.Errorf("the details:\n%s", a.Details)
|
|
}
|
|
// The ask made before the brief is composed again as it was, so its warrant still holds after the change.
|
|
old := composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12})
|
|
if !strings.Contains(old.Explanation, "Its 13 clause(s):") || old.Headline != "Accept ADR 0343?" || old.Details != "" || old.Digest() == a.Digest() {
|
|
t.Errorf("an ask made before the brief is not composed as it was asked:\n%s", old.Explanation)
|
|
}
|
|
// Without a brief (a record proposed before the rule), the glance is the title, and no clause.
|
|
r.Brief = nil
|
|
a = composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12, Form: AskGlance})
|
|
if a.Headline != "Approve ADR 0343?" || asks.Glance(a.Explanation) != nil || strings.Contains(a.Explanation, "Only the memory folders") {
|
|
t.Errorf("without a brief: %q\n%s", a.Headline, a.Explanation)
|
|
}
|
|
}
|
|
|
|
// A brief is held to what the phone reads at a glance, each way it fails named.
|
|
func TestABriefIsHeldToAGlance(t *testing.T) {
|
|
good := adr0343().Brief
|
|
if got := briefProblems(good); len(got) != 0 {
|
|
t.Fatalf("the brief of ADR 0343 is refused: %v", got)
|
|
}
|
|
for name, c := range map[string]struct {
|
|
change func(b *Brief)
|
|
says string
|
|
}{
|
|
"no headline": {func(b *Brief) { b.Headline = "" }, "headline is empty"},
|
|
"long headline": {func(b *Brief) { b.Headline = strings.Repeat("memory ", 10) }, "longer than 60"},
|
|
"no summary": {func(b *Brief) { b.Summary = " " }, "summary is empty"},
|
|
"long summary": {func(b *Brief) { b.Summary = strings.Repeat("A short sentence. ", 20) }, "summary is"},
|
|
"four points": {func(b *Brief) { b.Points = append(b.Points, "one more") }, "4 points"},
|
|
"a path": {func(b *Brief) { b.Points[0] = "Carried from the agent's home under .claude/projects" }, "names a path"},
|
|
"two lines": {func(b *Brief) { b.Summary = "One.\nTwo." }, "more than one line"},
|
|
} {
|
|
b := *good
|
|
b.Points = append([]string{}, good.Points...)
|
|
c.change(&b)
|
|
if got := fmt.Sprint(briefProblems(&b)); !strings.Contains(got, c.says) {
|
|
t.Errorf("%s: %s", name, got)
|
|
}
|
|
}
|
|
if got := briefProblems(nil); len(got) != 1 || !strings.Contains(got[0], "carries a brief") {
|
|
t.Errorf("no brief: %v", got)
|
|
}
|
|
}
|