Repository navigation
Expand file tree
/
Copy pathattribution_test.go
More file actions
149 lines (140 loc) · 5.36 KB
/
Copy pathattribution_test.go
File metadata and controls
149 lines (140 loc) · 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package head
import (
"fmt"
"strings"
"testing"
"time"
"github.com/Agent-Field/codeaf/internal/store"
)
// agentRepliesAfter counts what the user actually saw. Every route ends in
// exactly one visible reply; four work orders is still one sentence answered.
func agentRepliesAfter(t *testing.T, graph *store.Store, session string, seq int64) []store.Message {
t.Helper()
messages, err := graph.Messages(session, seq, 100)
if err != nil {
t.Fatalf("read thread: %v", err)
}
replies := make([]store.Message, 0, 2)
for _, message := range messages {
if message.Role == store.RoleAgent {
replies = append(replies, message)
}
}
return replies
}
// Four jobs narrating into one thread arrived at the model in one voice, and the
// model is the party that has to decide what "it" means. Every job-anchored line
// now says which job said it, by the same short name the user reads on screen —
// never an id, because deixis only resolves when both parties use one vocabulary.
func TestThreadSliceNamesTheJobThatSpoke(t *testing.T) {
graph := openHeadStore(t)
deliverJob(t, graph, "names", "auth-fix", "Auth token refresh",
"patch the auth token refresh", "The refresh path now retries once.")
deliverJob(t, graph, "names", "parser-fix", "Parser rewrite",
"rewrite the config parser", "The parser accepts trailing commas now.")
postUser(t, graph, "names", "how are those going?")
latest := postUser(t, graph, "names", "and now?")
head := New(nil, graph)
recent, err := head.recentThread("names", latest.Seq)
if err != nil {
t.Fatal(err)
}
rendered := head.renderThread(recent)
for _, label := range []string{"Auth token refresh", "Parser rewrite"} {
if !strings.Contains(rendered, "["+label+"]") {
t.Fatalf("the slice never says %q spoke:\n%s", label, rendered)
}
}
if strings.Contains(rendered, "[auth-fix]") || strings.Contains(rendered, "[parser-fix]") {
t.Fatalf("the slice attributed a line by its id rather than its name:\n%s", rendered)
}
// The user's own turns stay unattributed: they belong to the conversation,
// not to any job.
for _, line := range strings.Split(rendered, "\n") {
if strings.HasPrefix(line, "user: ") {
continue
}
if strings.HasPrefix(line, "user [") {
t.Fatalf("a user turn was filed under a job: %q", line)
}
}
}
// The board led with a flat list of every addressable node, so sixteen parts and
// four jobs arrived as twenty peers. Jobs lead now, carrying their subtree's
// counts, and a part says whose part it is.
func TestBoardLeadsWithJobsAndNamesTheOwnerOfEveryPart(t *testing.T) {
graph := openHeadStore(t)
if err := graph.Splice(store.RootID, store.Subtree{Nodes: []store.NodeSpec{
{ID: "issue-41", Title: "Patch auth token refresh", Brief: "patch the auth token refresh", Stage: 1},
{ID: "issue-41-test", Parent: "issue-41", Brief: "add a regression test", Stage: 2},
}}, store.Provenance{Origin: store.OriginUser, SessionID: "board", Intent: "work issue 41"}); err != nil {
t.Fatal(err)
}
board := New(nil, graph).boardFor("board", "", nil, time.Now())
jobLine, partLine := "", ""
for _, line := range strings.Split(board, "\n") {
switch {
case strings.HasPrefix(line, "- issue-41 |"):
jobLine = line
case strings.HasPrefix(line, "- issue-41-test |"):
partLine = line
}
}
if !strings.Contains(jobLine, "queued") {
t.Fatalf("the job row carries no rolled-up counts: %q\n%s", jobLine, board)
}
if !strings.Contains(partLine, "part of Patch auth token refresh") {
t.Fatalf("the part never says whose part it is: %q\n%s", partLine, board)
}
if strings.Index(board, "- issue-41 |") > strings.Index(board, "- issue-41-test |") {
t.Fatalf("a part was rendered above its own job:\n%s", board)
}
}
// A running job speaks every two minutes. Three of them filled a twenty-message
// window in about thirteen minutes, and the user's own words fell out of a
// conversation they had never left. The person's turns are windowed on their
// own now, and no amount of narration can evict one.
func TestNarratorFloodCannotEvictTheUsersOwnWords(t *testing.T) {
graph := openHeadStore(t)
const asked = "find me somewhere in Lisbon that isn't too fancy"
postUser(t, graph, "flood", asked)
for _, id := range []string{"trip", "letter", "spreadsheet"} {
spliceSurgeryJob(t, graph, id, strings.ToUpper(id[:1])+id[1:], "work on the "+id)
}
// Fifteen minutes of three jobs on a two-minute heartbeat.
for round := 0; round < 8; round++ {
for _, id := range []string{"trip", "letter", "spreadsheet"} {
if _, err := graph.PostMessage(store.Message{
SessionID: "flood", Role: store.RoleAgent, NodeID: id,
Body: fmt.Sprintf("still working on the %s (round %d)", id, round),
}); err != nil {
t.Fatal(err)
}
}
}
latest := postUser(t, graph, "flood", "any luck?")
head := New(nil, graph)
recent, err := head.recentThread("flood", latest.Seq)
if err != nil {
t.Fatal(err)
}
kept := false
ambient := 0
for _, message := range recent {
if message.Body == asked {
kept = true
}
if strings.TrimSpace(message.NodeID) != "" {
ambient++
}
}
if !kept {
t.Fatalf("twenty-four heartbeats evicted the user's own ask:\n%+v", recent)
}
if ambient > ambientWindowMax {
t.Fatalf("ambient chatter held %d rows, over its own window of %d", ambient, ambientWindowMax)
}
if !strings.Contains(head.renderThread(recent), asked) {
t.Fatal("the rendered slice lost the sentence the follow-up is deictic to")
}
}