13 "github.com/stretchr/testify/require"
14 "go.mau.fi/whatsmeow/types"
17 func TestWhatsAppImportReadsMadeUpExport(t *testing.T) {
18 path := whatsappImportSample(t)
19 location := time.FixedZone("TEST", -3*60*60)
20 chat, messages, err := whatsappImportRead(path, "Sam Owner", location)
21 require.NoError(t, err)
22 require.Equal(t, "Book Club", chat.Name)
23 require.True(t, chat.Group)
24 require.Len(t, messages, 19)
25 require.Equal(t, "Book Club", messages[0].Sender)
26 require.False(t, messages[0].Mine)
27 require.Equal(t, "2025-05-20T12:35:39-03:00", messages[0].Time.Format(time.RFC3339))
28 require.Contains(t, messages[0].Text, "end-to-end encrypted")
29 require.Equal(t, "image omitted", messages[5].Text)
30 require.True(t, messages[6].Mine)
31 require.Equal(t, "~\u202fBen Carter", messages[7].Sender)
32 require.Equal(t, "Sure.\nThe room costs $40.00 more.\nI will send the booking tomorrow. 👍🏻", messages[7].Text)
33 require.Contains(t, messages[8].Text, "<This message was edited>")
34 require.Equal(t, "Booking.pdf • 3 pages document omitted", messages[9].Text)
35 require.Equal(t, "audio omitted", messages[10].Text)
36 require.True(t, messages[11].Mine)
37 require.Equal(t, "This message was deleted.", messages[11].Text)
38 require.Equal(t, "video omitted", messages[12].Text)
39 require.Equal(t, "Contact card omitted", messages[13].Text)
40 require.Equal(t, "sticker omitted", messages[14].Text)
41 require.Contains(t, messages[15].Text, "changed the group name")
42 require.Equal(t, "A line with [brackets] and: a colon in the middle", messages[16].Text)
44 for _, message := range messages {
45 require.NotContains(t, message.Text, "\r")
46 require.NotContains(t, message.Text, "\u200e")
47 require.NotContains(t, message.Sender, "\u200e")
51 func TestWhatsAppImportSplitsDaysAndMapsOnlyOwner(t *testing.T) {
52 path := whatsappImportSample(t)
53 names := map[string]WhatsAppChat{
61 plan, err := whatsappImportPlan(t.Context(), []string{path}, nil, nil, names, "Sam Owner", time.UTC)
62 require.NoError(t, err)
63 require.Len(t, plan.added, 5)
64 require.Equal(t, "2025-05-20T13:06:44Z", plan.added[0].Date)
65 require.Equal(t, "2025-05-21T09:23:00Z", plan.added[1].Date)
66 require.Equal(t, "2025-12-31T23:59:59Z", plan.added[3].Date)
67 require.Equal(t, "2026-01-01T00:00:01Z", plan.added[4].Date)
68 require.True(t, strings.HasPrefix(plan.added[0].Text, "[WhatsApp] Book Club, group example@g.us, Tuesday 2025-05-20 (imported)\n"))
69 require.Contains(t, plan.added[0].Text, "12:35 Book Club: Messages and calls")
70 require.Contains(t, plan.added[0].Text, "13:02 You: Nice.")
71 require.NotContains(t, plan.added[0].Text, "Sam Owner:")
72 require.Contains(t, plan.added[0].Text, "13:05 ~\u202fBen Carter: Sure.\nThe room costs")
73 require.NotContains(t, plan.added[0].Text, whatsappBatchEmptyReply)
74 require.Equal(t, "2025-05-20T12:35:39Z", plan.first.Format(time.RFC3339))
75 require.Equal(t, "2026-01-01T00:00:01Z", plan.last.Format(time.RFC3339))
78 func TestWhatsAppImportSkipsHeldDaysAndMessagesAfterLiveCutoff(t *testing.T) {
79 path := whatsappImportSample(t)
80 old := []DatedMessage{
83 Text: "[WhatsApp] Book Club, private chat other@lid, Sunday 2025-06-08\n10:00 Ada: Later",
84 Date: "2025-06-08T10:01:00Z",
88 Text: "[WhatsApp] Book Club, private chat other@lid, Tuesday 2025-06-03\n18:41 Ada: First\n18:42 Ada: Last",
89 Date: "2025-06-03T18:43:00Z",
93 Text: "[WhatsApp] Book Club, Tuesday 2025-05-20 (imported)\n12:35 Book Club: System",
94 Date: "2025-05-20T13:06:44Z",
98 plan, err := whatsappImportPlan(t.Context(), []string{path}, old, nil, map[string]WhatsAppChat{}, "Sam Owner", time.UTC)
99 require.NoError(t, err)
100 require.Equal(t, "2025-06-03T18:41:00Z", plan.cutoff.Format(time.RFC3339))
101 require.Equal(t, 1, plan.skipped)
102 require.Len(t, plan.added, 2)
103 require.Equal(t, "2025-06-03T18:40:05Z", plan.added[1].Date)
104 require.NotContains(t, plan.added[1].Text, "18:41")
105 require.NotContains(t, plan.added[1].Text, ", group ")
106 require.Equal(t, len(old)+2, plan.messages)
109 func TestWhatsAppImportCutoffUsesLocalTimeAndExcludesBoundary(t *testing.T) {
110 path := whatsappImportZip(t, "Local", "[03/06/2025, 18:40:59] Ada: Before\n[03/06/2025, 18:41:00] Ada: Boundary\n[03/06/2025, 18:41:01] Ada: After\n")
111 old := []DatedMessage{{
113 Text: "[WhatsApp] Local, group live@g.us, Tuesday 2025-06-03\n18:41 Ada: First",
114 Date: "2025-06-04T01:00:00Z",
117 plan, err := whatsappImportPlan(t.Context(), []string{path}, old, nil, map[string]WhatsAppChat{}, "", time.FixedZone("TEST", -3*60*60))
118 require.NoError(t, err)
119 require.Equal(t, "2025-06-03T18:41:00-03:00", plan.cutoff.Format(time.RFC3339))
120 require.Len(t, plan.added, 1)
121 require.Contains(t, plan.added[0].Text, "Before")
122 require.NotContains(t, plan.added[0].Text, "Boundary")
123 require.NotContains(t, plan.added[0].Text, "After")
126 func TestWhatsAppImportKeepsSystemSenderEvenWithOwnerName(t *testing.T) {
127 path := whatsappImportZip(t, "Owner", "[01/01/2025, 00:00:00] Owner: System line\n")
128 _, messages, err := whatsappImportRead(path, "Owner", time.UTC)
129 require.NoError(t, err)
130 require.False(t, messages[0].Mine)
133 func TestWhatsAppImportRefusesOverlappingInputs(t *testing.T) {
134 path := whatsappImportSample(t)
135 _, err := whatsappImportPlan(t.Context(), []string{path, path}, nil, nil, map[string]WhatsAppChat{}, "", time.UTC)
136 require.ErrorContains(t, err, "inputs overlap")
139 func TestWhatsAppImportSkipsAllDaysOnALaterStep(t *testing.T) {
140 path := whatsappImportSample(t)
141 first, err := whatsappImportPlan(t.Context(), []string{path}, nil, nil, nil, "Sam Owner", time.UTC)
142 require.NoError(t, err)
144 second, err := whatsappImportPlan(t.Context(), []string{path}, first.added, nil, nil, "Sam Owner", time.UTC)
145 require.NoError(t, err)
146 require.Empty(t, second.added)
147 require.Equal(t, 5, second.skipped)
148 require.True(t, second.cutoff.IsZero())
151 func TestWhatsAppImportMergesChatsByDate(t *testing.T) {
152 later := whatsappImportZip(t, "Later", "[02/01/2025, 08:00:00] Ada: Later\n")
153 earlier := whatsappImportZip(t, "Earlier", "[01/01/2025, 08:00:00] Bea: Earlier\n")
154 plan, err := whatsappImportPlan(t.Context(), []string{later, earlier}, nil, nil, nil, "", time.UTC)
155 require.NoError(t, err)
156 require.Len(t, plan.added, 2)
157 require.Contains(t, plan.added[0].Text, "[WhatsApp] Earlier,")
158 require.Contains(t, plan.added[1].Text, "[WhatsApp] Later,")
161 func TestWhatsAppImportRefusesMissingOrDuplicateChatText(t *testing.T) {
162 testCases := []struct {
168 name: "missing chat text is refused",
169 members: []string{"other.txt"},
170 want: "_chat.txt is missing",
173 name: "duplicate chat text is refused",
174 members: []string{"_chat.txt", "nested/_chat.txt"},
175 want: "more than one _chat.txt",
179 for _, tc := range testCases {
180 t.Run(tc.name, func(t *testing.T) {
181 path := filepath.Join(t.TempDir(), "Example.zip")
182 file, err := os.Create(path)
183 require.NoError(t, err)
185 archive := zip.NewWriter(file)
186 for _, member := range tc.members {
187 _, err = archive.Create(member)
188 require.NoError(t, err)
191 require.NoError(t, archive.Close())
192 require.NoError(t, file.Close())
194 _, _, err = whatsappImportRead(path, "", time.UTC)
195 require.ErrorContains(t, err, tc.want)
200 func TestWhatsAppImportRefusesMalformedExports(t *testing.T) {
201 testCases := []struct {
207 name: "text before the first date is refused",
208 text: "not a message\n",
209 want: "before the first",
212 name: "invalid local dates are refused",
213 text: "[31/02/2025, 10:00:00] Ada: Invalid\n",
214 want: "parse export local date",
217 name: "exports with no dated messages are refused",
219 want: "no dated messages",
223 for _, tc := range testCases {
224 t.Run(tc.name, func(t *testing.T) {
225 _, _, err := whatsappImportRead(whatsappImportZip(t, "Example", tc.text), "", time.UTC)
226 require.ErrorContains(t, err, tc.want)
231 func TestWhatsAppImportPlanCommandDoesNotChangeAgentOrCallModel(t *testing.T) {
232 agent := newRebuildTestAgent(t)
233 dir := filepath.Dir(agent.dir)
234 t.Setenv("ONECHAT_DIR", dir)
236 // A plan does not need model settings or credentials.
237 require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte("not model settings"), 0o600))
238 require.NoError(t, os.WriteFile(filepath.Join(dir, "auth.json"), []byte("not credentials"), 0o600))
240 rebuildTestLog(t, agent.dir, []DatedMessage{{
243 Date: "2026-11-01T00:00:00Z",
246 before := rebuildTestSnapshot(t, agent.dir)
248 var report strings.Builder
250 require.NoError(t, whatsappImport(t.Context(), []string{"-n", whatsappImportSample(t)}, &report))
251 require.Equal(t, before, rebuildTestSnapshot(t, agent.dir))
253 for _, want := range []string{"chats: 1", "Book Club", "batches: 5", "dates: ", "cutoff: none", "bytes: ", "messages: 6", "estimated calls: 8 (about 1.3 per message, limit 1500)", "owner: unknown"} {
254 require.Contains(t, report.String(), want)
257 require.NotContains(t, report.String(), "end-to-end encrypted")
258 require.NotContains(t, report.String(), "Sam Owner")
260 lock, err := lockTokens(dir)
261 require.NoError(t, err)
262 require.ErrorIs(t, whatsappImport(t.Context(), []string{"-n", whatsappImportSample(t)}, &strings.Builder{}), errTokensInUse)
263 require.NoError(t, lock.Close())
264 require.NoError(t, os.WriteFile(filepath.Join(agent.dir, "work.json"), []byte(`{"running":["BlueBird"]}`), 0o600))
266 require.ErrorContains(t, whatsappImport(t.Context(), []string{"-n", whatsappImportSample(t)}, &strings.Builder{}), "unfinished work")
269 func TestWhatsAppImportDoesNotRebuildWhenAllDaysAreHeld(t *testing.T) {
270 agent := newRebuildTestAgent(t)
271 dir := filepath.Dir(agent.dir)
272 t.Setenv("ONECHAT_DIR", dir)
274 path := whatsappImportSample(t)
275 plan, err := whatsappImportPlan(t.Context(), []string{path}, nil, nil, nil, "", time.UTC)
276 require.NoError(t, err)
278 rebuildTestLog(t, agent.dir, plan.added)
279 require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte("not model settings"), 0o600))
281 before := rebuildTestSnapshot(t, agent.dir)
283 var report strings.Builder
285 require.NoError(t, whatsappImport(t.Context(), []string{path}, &report))
286 require.Contains(t, report.String(), "batches: 0")
287 require.Contains(t, report.String(), "estimated calls: 0")
288 require.Equal(t, before, rebuildTestSnapshot(t, agent.dir))
291 func TestWhatsAppImportReadsOnlyLocalSessionNames(t *testing.T) {
293 whatsappTestSession(t, dir, true)
295 session := &WhatsApp{
296 dir: filepath.Join(dir, whatsappDirName),
299 err := session.openSession(t.Context())
300 require.NoError(t, err)
302 session.device.PushName = "Sam Owner"
303 session.device.Account.Details = []byte{}
304 require.NoError(t, session.device.Save(t.Context()))
305 require.NoError(t, session.device.Contacts.PutContactName(t.Context(), whatsappTestOwn, "", "Owner"))
306 require.NoError(t, session.device.Contacts.PutContactName(t.Context(), session.device.LID.ToNonAD(), "", "Owner LID"))
308 friend := types.NewJID("friend", types.HiddenUserServer)
309 require.NoError(t, session.device.Contacts.PutContactName(t.Context(), friend, "Ada", "Ada Example"))
310 require.NoError(t, session.device.Contacts.PutContactName(t.Context(), types.NewJID("other", types.HiddenUserServer), "Ada", ""))
311 require.NoError(t, session.container.Close())
313 names, owner, err := whatsappImportNames(t.Context(), dir)
314 require.NoError(t, err)
315 require.Equal(t, "Sam Owner", owner)
316 require.Equal(t, "friend@lid", names["Ada Example"].ID)
317 require.Empty(t, names["Ada"].ID)
318 require.NotContains(t, names, "Owner")
319 require.NotContains(t, names, "Owner LID")
322 func TestWhatsAppImportCutoffUsesEachChatAndBothLogs(t *testing.T) {
323 path := whatsappImportZip(t, "Plans", "[03/01/2025, 09:59:59] Ada: Before\n[03/01/2025, 10:00:00] Ada: Boundary\n[03/01/2025, 10:01:00] Ada: After\n")
324 target := []DatedMessage{{
326 Text: "[WhatsApp] Plans, group example@g.us, Friday 2025-01-03\n10:00 Ada: Target first\n10:30 Ada: Target last",
327 Date: "2025-01-03T10:30:00Z",
330 root := []DatedMessage{
333 Text: "[WhatsApp] Other, group other@g.us, Wednesday 2025-01-01\n08:00 Bea: Other chat",
334 Date: "2025-01-01T08:00:00Z",
338 Text: "[WhatsApp] Plans, group example@g.us, Saturday 2025-01-04\n10:00 Ada: Later batch",
339 Date: "2025-01-04T10:00:00Z",
343 testCases := []struct {
345 target []DatedMessage
349 name: "target holds the first live batch",
354 name: "root holds the first live batch",
360 for _, tc := range testCases {
361 t.Run(tc.name, func(t *testing.T) {
362 plan, err := whatsappImportPlan(t.Context(), []string{path}, tc.target, tc.root, nil, "", time.UTC)
363 require.NoError(t, err)
364 require.Equal(t, "2025-01-03T10:00:00Z", plan.cutoff.Format(time.RFC3339))
365 require.Len(t, plan.added, 1)
366 require.Contains(t, plan.added[0].Text, "Before")
367 require.NotContains(t, plan.added[0].Text, "Boundary")
368 require.NotContains(t, plan.added[0].Text, "After")
373 func TestWhatsAppImportSelectsOnlyAnExistingDirectChild(t *testing.T) {
374 root := newRebuildTestAgent(t)
375 dir := filepath.Dir(root.dir)
376 t.Setenv("ONECHAT_DIR", dir)
377 daemonChildConfig(t, dir, "House")
379 before := rebuildTestSnapshot(t, root.dir)
380 for _, name := range []string{"Missing", "../House", root.config.Name} {
381 err := whatsappImport(t.Context(), []string{"-n", "--agent", name, whatsappImportSample(t)}, &strings.Builder{})
382 require.ErrorContains(t, err, "does not exist")
383 require.Equal(t, before, rebuildTestSnapshot(t, root.dir))
387 func TestWhatsAppImportParsesGNUFlags(t *testing.T) {
388 testCases := []struct {
391 want WhatsAppImportOptions
394 name: "default target remains root",
395 args: []string{"One.zip", "Two.zip"},
396 want: WhatsAppImportOptions{paths: []string{"One.zip", "Two.zip"}},
399 name: "long flag takes a separate child name",
400 args: []string{"-n", "--agent", "House", "One.zip"},
401 want: WhatsAppImportOptions{
404 paths: []string{"One.zip"},
408 name: "flags can follow paths",
409 args: []string{"One.zip", "--agent=House", "-n"},
410 want: WhatsAppImportOptions{
413 paths: []string{"One.zip"},
417 name: "double dash ends flag parsing",
418 args: []string{"-n", "--", "-Example.zip"},
419 want: WhatsAppImportOptions{
421 paths: []string{"-Example.zip"},
426 for _, tc := range testCases {
427 t.Run(tc.name, func(t *testing.T) {
428 options, err := whatsappImportParse(tc.args)
429 require.NoError(t, err)
430 require.Equal(t, tc.want, options)
435 func TestWhatsAppImportRefusesInvalidFlags(t *testing.T) {
436 testCases := []struct {
442 name: "missing exports are refused",
443 args: []string{"-n", "--agent", "House"},
447 name: "missing child name is refused",
448 args: []string{"--agent"},
449 want: "needs a child name",
452 name: "empty child name is refused",
453 args: []string{"--agent=", "One.zip"},
454 want: "needs a child name",
457 name: "single dash long flag is refused",
458 args: []string{"-agent", "House", "One.zip"},
459 want: "unknown import flag",
463 for _, tc := range testCases {
464 t.Run(tc.name, func(t *testing.T) {
465 _, err := whatsappImportParse(tc.args)
466 require.ErrorContains(t, err, tc.want)
471 func TestWhatsAppImportChildPlanReadsRootCutoffButNotRootImportedDays(t *testing.T) {
472 root := newRebuildTestAgent(t)
473 dir := filepath.Dir(root.dir)
474 t.Setenv("ONECHAT_DIR", dir)
476 childDir := daemonChildConfig(t, dir, "House")
477 rebuildTestLog(t, root.dir, []DatedMessage{
480 Text: "[WhatsApp] Plans, Wednesday 2025-01-01 (imported)\n08:00 Ada: Root history",
481 Date: "2025-01-01T08:00:00Z",
485 Text: "[WhatsApp] Plans, group example@g.us, Friday 2025-01-03\n10:00 Ada: Root live\n10:20 Ada: Last",
486 Date: "2025-01-03T10:20:00Z",
489 rebuildTestLog(t, childDir, []DatedMessage{
492 Text: "[WhatsApp] Plans, Thursday 2025-01-02 (imported)\n08:00 Ada: Child history",
493 Date: "2025-01-02T08:00:00Z",
497 Text: "[WhatsApp] Plans, group example@g.us, Saturday 2025-01-04\n10:00 Ada: Child live",
498 Date: "2025-01-04T10:00:00Z",
502 path := whatsappImportZip(t, "Plans", "[01/01/2025, 08:00:00] Ada: Root history\n[02/01/2025, 08:00:00] Ada: Child history\n[03/01/2025, 09:59:59] Ada: Before\n[03/01/2025, 10:00:00] Ada: Boundary\n[03/01/2025, 10:01:00] Ada: After\n")
503 before := rebuildTestSnapshot(t, root.dir)
505 var report strings.Builder
507 require.NoError(t, whatsappImport(t.Context(), []string{"-n", "--agent", "House", path}, &report))
508 require.Contains(t, report.String(), "agent: House\n")
509 require.Contains(t, report.String(), "batches: 2 (1 chat-days skipped)")
510 require.Contains(t, report.String(), "2025-01-03T10:00:00")
511 require.Contains(t, report.String(), "messages: 4 (merged log)")
512 require.Equal(t, before, rebuildTestSnapshot(t, root.dir))
515 require.NoError(t, whatsappImport(t.Context(), []string{"-n", path}, &report))
516 require.Contains(t, report.String(), "agent: "+root.config.Name+"\n")
517 require.Contains(t, report.String(), "batches: 2 (1 chat-days skipped)")
518 require.Equal(t, before, rebuildTestSnapshot(t, root.dir))
521 func TestWhatsAppImportChildRebuildUsesOwnPromptAndCache(t *testing.T) {
522 root := newRebuildTestAgent(t)
523 dir := filepath.Dir(root.dir)
524 childDir := daemonChildConfig(t, dir, "House")
525 daemonChildConfig(t, dir, "Calendar")
526 require.NoError(t, os.WriteFile(filepath.Join(childDir, "instructions.md"), []byte("Keep room dimensions exact."), 0o600))
527 rebuildTestLog(t, root.dir, []DatedMessage{{
529 Text: "Keep the root history.",
530 Date: "2025-02-01T00:00:00Z",
532 rebuildTestLog(t, childDir, []DatedMessage{{
534 Text: "Keep the child history.",
535 Date: "2025-02-02T00:00:00Z",
538 child, err := whatsappImportAgent(root.dir, "House")
539 require.NoError(t, err)
541 old, err := rebuildReadLog(t.Context(), child.dir)
542 require.NoError(t, err)
544 rootLog, err := rebuildReadLog(t.Context(), root.dir)
545 require.NoError(t, err)
547 path := whatsappImportZip(t, "Plans", "[01/01/2025, 08:00:00] Ada: "+strings.Repeat("The room is ready. ", 80)+"\n")
548 plan, err := whatsappImportPlan(t.Context(), []string{path}, old, rootLog, nil, "", time.UTC)
549 require.NoError(t, err)
551 before := rebuildTestSnapshot(t, child.dir)
552 sibling := rebuildTestSnapshot(t, filepath.Join(root.dir, "children", "Calendar"))
553 server := newModelFake(t)
554 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
555 children, err := root.Children()
556 if err != nil || len(children) != 2 {
557 w.WriteHeader(http.StatusBadRequest)
562 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("The room is ready.")}, 100, 0, 5))
565 client := newModelClient(server.URL, ModelAPIKey(modelTestKey), child.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl"))
566 backup, err := rebuild(t.Context(), child, plan.added, client, newCompactor(client, child.Prompt, defaultConfig().Compactor, child.Tools(), child.Images()), &strings.Builder{})
567 require.NoError(t, err)
568 require.Equal(t, before, rebuildTestSnapshot(t, backup))
569 require.Equal(t, root.dir, filepath.Dir(backup))
570 require.Equal(t, sibling, rebuildTestSnapshot(t, filepath.Join(root.dir, "children", "Calendar")))
572 children, err := root.Children()
573 require.NoError(t, err)
574 require.Len(t, children, 2)
576 entries, err := rebuildReadLog(t.Context(), child.dir)
577 require.NoError(t, err)
578 require.Equal(t, append(plan.added, old...), entries)
580 unchanged, err := rebuildReadLog(t.Context(), root.dir)
581 require.NoError(t, err)
582 require.Equal(t, rootLog, unchanged)
584 prompt, err := child.Prompt()
585 require.NoError(t, err)
586 require.NotEqual(t, root.CacheKey(), child.CacheKey())
587 require.Positive(t, server.Calls())
589 for i := range server.Calls() {
590 call := server.Call(i)
591 require.Equal(t, child.CacheKey(), call.body["prompt_cache_key"])
592 require.Equal(t, prompt, call.body["instructions"])
593 require.Contains(t, prompt, "Keep room dimensions exact.")
594 require.NotContains(t, string(call.raw), `"name":"whatsapp_send"`)
598 func TestWhatsAppImportChildFailureKeepsConfiguredChildren(t *testing.T) {
599 root := newRebuildTestAgent(t)
600 daemonChildConfig(t, filepath.Dir(root.dir), "House")
602 child, err := whatsappImportAgent(root.dir, "House")
603 require.NoError(t, err)
605 before := rebuildTestSnapshot(t, root.dir)
606 server := newModelFake(t)
607 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
608 w.WriteHeader(http.StatusBadRequest)
611 client := newModelClient(server.URL, ModelAPIKey(modelTestKey), child.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl"))
612 _, err = rebuild(t.Context(), child, []DatedMessage{{
614 Text: strings.Repeat("The plan is ready. ", 80),
615 Date: "2025-01-01T08:00:00Z",
616 }}, client, newCompactor(client, child.Prompt, defaultConfig().Compactor, child.Tools(), child.Images()), &strings.Builder{})
617 require.ErrorContains(t, err, "rebuild")
618 require.Equal(t, before, rebuildTestSnapshot(t, root.dir))
620 children, err := root.Children()
621 require.NoError(t, err)
622 require.Len(t, children, 1)
625 func TestWhatsAppImportRefusesPendingDeliveriesAndKeepsRestartSafe(t *testing.T) {
626 testCases := []struct {
631 name: "pending child delivery keeps its log ID",
635 name: "pending root report keeps its log ID",
639 for _, tc := range testCases {
640 t.Run(tc.name, func(t *testing.T) {
642 t.Setenv("ONECHAT_DIR", dir)
644 server := newModelFake(t)
645 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
646 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
649 test := newDaemonMessageTest(t, dir, server)
651 args := []string{whatsappImportZip(t, "Plans", "[01/01/2025, 08:00:00] Ada: The room is ready.\n")}
654 args = append([]string{"--agent", "Garden"}, args...)
656 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Keep the plan."))
658 require.NoError(t, test.messages.Finish(test.loops[1], nil, "The plan is ready."))
661 queue := test.Queue(t)
662 require.Len(t, queue.Pending, 1)
663 require.NotNil(t, queue.Pending[0].LogID)
665 before, err := test.agents[index].memory.Message(*queue.Pending[0].LogID)
666 require.NoError(t, err)
669 root := filepath.Join(dir, "agent")
670 snapshot := rebuildTestSnapshot(t, root)
671 require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte("invalid model settings"), 0o600))
673 for _, planOnly := range []bool{true, false} {
674 input := append([]string(nil), args...)
676 input = append([]string{"-n"}, input...)
679 err = whatsappImport(t.Context(), input, &strings.Builder{})
680 require.ErrorContains(t, err, "has pending deliveries")
681 require.Equal(t, snapshot, rebuildTestSnapshot(t, root))
682 require.Zero(t, server.Calls())
685 restarted := newDaemonMessageTest(t, dir, server)
686 require.Equal(t, queue, restarted.Queue(t))
687 require.Equal(t, 1, restarted.agents[index].memory.Len())
689 after, err := restarted.agents[index].memory.Message(before.ID)
690 require.NoError(t, err)
691 require.Equal(t, before, after)
692 restarted.Start(t, index)
693 restarted.events[index].WaitFor(t, "idle", 1)
694 require.Empty(t, restarted.Queue(t).Pending)
695 require.Equal(t, 1, server.Calls())
698 again := newDaemonMessageTest(t, dir, server)
699 require.False(t, again.loops[index].waiting())
700 require.Equal(t, 1, server.Calls())
705 func TestWhatsAppImportPendingDeliveriesBlockOnlyTheirTarget(t *testing.T) {
707 t.Setenv("ONECHAT_DIR", dir)
709 server := newModelFake(t)
710 test := newDaemonMessageTest(t, dir, server)
711 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Keep the plan."))
713 queue := test.Queue(t)
716 path := whatsappImportZip(t, "Plans", "[01/01/2025, 08:00:00] Ada: The room is ready.\n")
717 for _, name := range []string{"", "Library"} {
718 args := []string{"-n", path}
720 args = append(args, "--agent", name)
723 require.NoError(t, whatsappImport(t.Context(), args, &strings.Builder{}))
726 queue.Pending[0].LogID = nil
727 data, err := json.Marshal(queue)
728 require.NoError(t, err)
729 require.NoError(t, os.WriteFile(filepath.Join(dir, "agent", "messages.json"), data, 0o600))
730 require.ErrorContains(t, whatsappImport(t.Context(), []string{"-n", "--agent", "Garden", path}, &strings.Builder{}), "has pending deliveries")
731 require.Zero(t, server.Calls())
734 func TestWhatsAppImportDefaultPlanDoesNotCreateAgent(t *testing.T) {
736 t.Setenv("ONECHAT_DIR", dir)
738 var report strings.Builder
740 require.NoError(t, whatsappImport(t.Context(), []string{"-n", whatsappImportSample(t)}, &report))
741 require.Contains(t, report.String(), "agent: OneChat\n")
743 _, err := os.Stat(filepath.Join(dir, "agent"))
744 require.ErrorIs(t, err, os.ErrNotExist)
747 func whatsappImportSample(t *testing.T) string {
750 data, err := os.ReadFile(filepath.Join("testdata", "whatsapp-export.zip"))
751 require.NoError(t, err)
753 path := filepath.Join(t.TempDir(), "WhatsApp Chat - Book Club.zip")
754 require.NoError(t, os.WriteFile(path, data, 0o600))
759 func whatsappImportZip(t *testing.T, name, text string) string {
762 path := filepath.Join(t.TempDir(), "WhatsApp Chat - "+name+".zip")
763 file, err := os.Create(path)
764 require.NoError(t, err)
766 archive := zip.NewWriter(file)
767 writer, err := archive.Create("_chat.txt")
768 require.NoError(t, err)
770 _, err = writer.Write([]byte(text))
771 require.NoError(t, err)
772 require.NoError(t, archive.Close())
773 require.NoError(t, file.Close())