15 "github.com/stretchr/testify/require"
18 // childDirVariable names the chat directory of the writer child process of
19 // TestKilledWriterLosesNoLoggedMessage.
20 const childDirVariable = "ONECHAT_TEST_CHILD_DIR"
22 func TestFlushWaitsForASyncThatStartsAfterTheWrite(t *testing.T) {
23 gate := newGatedSync()
24 s := newSyncer(gate.Sync)
26 gate.WaitStarted(t, 1)
28 // This write comes during the first sync, so that sync does not cover
31 flushed := make(chan error)
33 flushed <- s.Flush(t.Context())
37 gate.WaitStarted(t, 2)
39 case err := <-flushed:
40 t.Fatalf("flush returned before the second sync: %v", err)
41 case <-time.After(20 * time.Millisecond):
45 require.NoError(t, <-flushed)
46 require.NoError(t, s.Close())
47 require.Equal(t, 2, gate.Count())
50 func TestOneSyncCoversTheWritesDuringTheSyncBeforeIt(t *testing.T) {
51 gate := newGatedSync()
52 s := newSyncer(gate.Sync)
54 gate.WaitStarted(t, 1)
60 gate.WaitStarted(t, 2)
62 require.NoError(t, s.Flush(t.Context()))
63 require.Equal(t, 2, gate.Count())
64 require.NoError(t, s.Close())
67 func TestFlushWithoutWritesNeedsNoSync(t *testing.T) {
68 gate := newGatedSync()
69 s := newSyncer(gate.Sync)
70 require.NoError(t, s.Flush(t.Context()))
71 require.NoError(t, s.Close())
72 require.Equal(t, 0, gate.Count())
75 func TestAFailedSyncFailsEachLaterFlush(t *testing.T) {
76 s := newSyncer(func() error {
77 return errors.New("disk error")
81 require.ErrorContains(t, s.Flush(t.Context()), "disk error")
84 require.ErrorContains(t, s.Flush(t.Context()), "disk error")
85 require.ErrorContains(t, s.Close(), "disk error")
88 func TestMemoryFlushSyncsTheChat(t *testing.T) {
89 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
90 require.NoError(t, err)
92 _, err = memory.Append("tool", "shell: ls")
93 require.NoError(t, err)
94 require.NoError(t, memory.Flush(t.Context()))
95 require.NoError(t, memory.Close())
98 func TestKilledWriterLosesNoLoggedMessage(t *testing.T) {
100 for round := range 3 {
101 acknowledged := runKilledWriter(t, root, 200+100*round)
103 memory, err := openMemory(t.Context(), root, time.Now, newFakeSummarizer())
104 require.NoError(t, err)
105 require.GreaterOrEqual(t, memory.log.Len(), acknowledged)
106 for id := range memory.log.Len() {
107 message, err := memory.log.Message(id)
108 require.NoError(t, err)
109 require.Equal(t, writerKind(id), message.Kind)
110 require.Equal(t, writerText(id), message.Text)
113 require.NoError(t, memory.Close())
117 // TestWriterChild is the child process of TestKilledWriterLosesNoLoggedMessage.
118 // It appends messages to the chat in the directory of childDirVariable and
119 // prints the ID of each message after its Append returns, until it is killed.
120 func TestWriterChild(t *testing.T) {
121 root := os.Getenv(childDirVariable)
123 t.Skip("runs only as a child process")
126 memory, err := openMemory(t.Context(), root, time.Now, newFakeSummarizer())
127 require.NoError(t, err)
129 for id := memory.log.Len(); ; id++ {
130 _, err = memory.Append(writerKind(id), writerText(id))
131 require.NoError(t, err)
133 _, err = fmt.Println(id)
134 require.NoError(t, err)
138 // gatedSync is a sync function that blocks until the test releases it.
139 type gatedSync struct {
142 started chan struct{}
143 release chan struct{}
146 func newGatedSync() *gatedSync {
148 started: make(chan struct{}, 100),
149 release: make(chan struct{}),
153 func (g *gatedSync) Sync() error {
158 g.started <- struct{}{}
164 // WaitStarted waits until sync number n has started.
165 func (g *gatedSync) WaitStarted(t *testing.T, n int) {
168 require.Eventually(t, func() bool {
169 return g.Count() >= n
170 }, time.Second, time.Millisecond)
173 // Release lets the running sync end.
174 func (g *gatedSync) Release() {
176 g.release <- struct{}{}
179 func (g *gatedSync) Count() int {
181 defer g.mutex.Unlock()
186 // runKilledWriter starts the writer child on the chat in root, kills it with
187 // SIGKILL after it acknowledged count messages, and returns the number of
188 // messages that it acknowledged.
189 func runKilledWriter(t *testing.T, root string, count int) int {
192 command := exec.Command(os.Args[0], "-test.run=^TestWriterChild$")
193 command.Env = append(os.Environ(), childDirVariable+"="+root)
194 stdout, err := command.StdoutPipe()
195 require.NoError(t, err)
196 require.NoError(t, command.Start())
199 scanner := bufio.NewScanner(stdout)
200 for acknowledged < count && scanner.Scan() {
201 _, err = strconv.Atoi(strings.TrimSpace(scanner.Text()))
207 require.NoError(t, command.Process.Kill())
209 _, err = strconv.Atoi(strings.TrimSpace(scanner.Text()))
215 require.Error(t, command.Wait())
216 require.Equal(t, count, min(acknowledged, count), "the child stopped early")
221 // writerKind returns the kind of message id of the writer child.
222 func writerKind(id int) string {
223 return []string{"user", "tool", "echo", "reply"}[id%4]
226 // writerText returns the text of message id of the writer child. Some texts
227 // are large, so a kill can come during the write of one line.
228 func writerText(id int) string {
234 return strings.Repeat(strconv.Itoa(id%10), size)