15 "github.com/stretchr/testify/require"
18 // syncerChildDirVariable names the chat directory of the writer child process
19 // of TestKilledWriterLosesNoLoggedMessage.
20 const syncerChildDirVariable = "ONECHAT_TEST_CHILD_DIR"
22 func TestFlushWaitsForASyncThatStartsAfterTheWrite(t *testing.T) {
23 gate := newSyncerGatedSync()
24 s := newSyncer(gate.Sync)
26 gate.WaitStarted(t, 1)
28 // This write comes during the first sync, so that sync does not cover
32 flushed := make(chan error)
34 flushed <- s.Flush(t.Context())
38 gate.WaitStarted(t, 2)
41 case err := <-flushed:
42 t.Fatalf("flush returned before the second sync: %v", err)
43 case <-time.After(20 * time.Millisecond):
47 require.NoError(t, <-flushed)
48 require.NoError(t, s.Close())
49 require.Equal(t, 2, gate.Count())
52 func TestOneSyncCoversTheWritesDuringTheSyncBeforeIt(t *testing.T) {
53 gate := newSyncerGatedSync()
54 s := newSyncer(gate.Sync)
56 gate.WaitStarted(t, 1)
63 gate.WaitStarted(t, 2)
65 require.NoError(t, s.Flush(t.Context()))
66 require.Equal(t, 2, gate.Count())
67 require.NoError(t, s.Close())
70 func TestFlushWithoutWritesNeedsNoSync(t *testing.T) {
71 gate := newSyncerGatedSync()
72 s := newSyncer(gate.Sync)
73 require.NoError(t, s.Flush(t.Context()))
74 require.NoError(t, s.Close())
75 require.Equal(t, 0, gate.Count())
78 func TestAFailedSyncFailsEachLaterFlush(t *testing.T) {
79 s := newSyncer(func() error {
80 return errors.New("disk error")
84 require.ErrorContains(t, s.Flush(t.Context()), "disk error")
87 require.ErrorContains(t, s.Flush(t.Context()), "disk error")
88 require.ErrorContains(t, s.Close(), "disk error")
91 func TestMemoryFlushSyncsTheChat(t *testing.T) {
92 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer(), discardLog())
93 require.NoError(t, err)
95 _, err = memory.Append("tool", "shell: ls")
96 require.NoError(t, err)
97 require.NoError(t, memory.Flush(t.Context()))
98 require.NoError(t, memory.Close())
101 func TestKilledWriterLosesNoLoggedMessage(t *testing.T) {
103 for round := range 3 {
104 acknowledged := syncerRunKilledWriter(t, root, 200+100*round)
106 memory, err := openMemory(t.Context(), root, time.Now, newFakeSummarizer(), discardLog())
107 require.NoError(t, err)
108 require.GreaterOrEqual(t, memory.log.Len(), acknowledged)
110 for id := range memory.log.Len() {
111 message, err := memory.log.Message(id)
112 require.NoError(t, err)
113 require.Equal(t, syncerWriterKind(id), message.Kind)
114 require.Equal(t, syncerWriterText(id), message.Text)
117 require.NoError(t, memory.Close())
121 // TestWriterChild is the child process of TestKilledWriterLosesNoLoggedMessage.
122 // It appends messages to the chat in the directory of syncerChildDirVariable
123 // and prints the ID of each message after its Append returns, until it is
125 func TestWriterChild(t *testing.T) {
126 root := os.Getenv(syncerChildDirVariable)
128 t.Skip("runs only as a child process")
131 memory, err := openMemory(t.Context(), root, time.Now, newFakeSummarizer(), discardLog())
132 require.NoError(t, err)
134 for id := memory.log.Len(); ; id++ {
135 _, err = memory.Append(syncerWriterKind(id), syncerWriterText(id))
136 require.NoError(t, err)
138 _, err = fmt.Println(id)
139 require.NoError(t, err)
143 // SyncerGatedSync is a sync function that blocks until the test releases it.
144 type SyncerGatedSync struct {
147 started chan struct{}
148 release chan struct{}
151 func (g *SyncerGatedSync) Sync() error {
157 g.started <- struct{}{}
164 // WaitStarted waits until sync number n has started.
165 func (g *SyncerGatedSync) 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 *SyncerGatedSync) Release() {
177 g.release <- struct{}{}
180 func (g *SyncerGatedSync) Count() int {
182 defer g.mutex.Unlock()
187 func newSyncerGatedSync() *SyncerGatedSync {
188 return &SyncerGatedSync{
189 started: make(chan struct{}, 100),
190 release: make(chan struct{}),
194 // syncerRunKilledWriter starts the writer child on the chat in root, kills it
195 // with SIGKILL after it acknowledged count messages, and returns the number of
196 // messages that it acknowledged.
197 func syncerRunKilledWriter(t *testing.T, root string, count int) int {
200 command := exec.Command(os.Args[0], "-test.run=^TestWriterChild$")
201 command.Env = append(os.Environ(), syncerChildDirVariable+"="+root)
202 stdout, err := command.StdoutPipe()
203 require.NoError(t, err)
204 require.NoError(t, command.Start())
207 scanner := bufio.NewScanner(stdout)
208 for acknowledged < count && scanner.Scan() {
209 _, err = strconv.Atoi(strings.TrimSpace(scanner.Text()))
215 require.NoError(t, command.Process.Kill())
218 _, err = strconv.Atoi(strings.TrimSpace(scanner.Text()))
224 require.Error(t, command.Wait())
225 require.Equal(t, count, min(acknowledged, count), "the child stopped early")
230 // syncerWriterKind returns the kind of message id of the writer child.
231 func syncerWriterKind(id int) string {
232 return []string{"user", "tool", "echo", "reply"}[id%4]
235 // syncerWriterText returns the text of message id of the writer child. Some
236 // texts are large, so a kill can come during the write of one line.
237 func syncerWriterText(id int) string {
243 return strings.Repeat(strconv.Itoa(id%10), size)