package main import ( "bytes" "context" "encoding/json" "net" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestNotifyCLIReachesOnlyTheRootThroughTheDaemon(t *testing.T) { server := newModelFake(t) socket := daemonStartTest(t, server) t.Setenv("ONECHAT_DIR", filepath.Dir(socket)) var out bytes.Buffer args := []string{"--source", "test-check", "--id", "event-1", "--text", "A result needs review."} require.NoError(t, notify(t.Context(), args, &out)) var receipt Event require.NoError(t, json.Unmarshal(out.Bytes(), &receipt)) require.Equal(t, "notify", receipt.Kind) require.Equal(t, "accepted", receipt.Status) require.Positive(t, receipt.ID) require.NotContains(t, out.String(), "needs review") require.Eventually(t, func() bool { return server.Calls() == 1 }, 10*time.Second, time.Millisecond) texts := turnInputTexts(t, server.Call(0)) require.Contains(t, texts[len(texts)-1], "Untrusted external data") require.Contains(t, texts[len(texts)-1], `"source":"test-check"`) require.Contains(t, texts[len(texts)-1], `"id":"event-1"`) out.Reset() require.NoError(t, notify(t.Context(), args, &out)) var duplicate Event require.NoError(t, json.Unmarshal(out.Bytes(), &duplicate)) require.Equal(t, receipt.ID, duplicate.ID) require.Equal(t, "duplicate", duplicate.Status) args[len(args)-1] = "Changed result." require.ErrorContains(t, notify(t.Context(), args, &out), "conflicts") } func TestNotifyFlagsValidateBeforeConnecting(t *testing.T) { testCases := []struct { name string args []string }{ { name: "requires all fields", args: []string{"--source", "check", "--id", "1"}, }, { name: "rejects short long flags", args: []string{"-source", "check", "--id", "1", "--text", "data"}, }, { name: "rejects child targets in the root-only slice", args: []string{"--agent", "Garden", "--source", "check", "--id", "1", "--text", "data"}, }, { name: "rejects a flag without a value", args: []string{"--source"}, }, { name: "rejects a repeated flag", args: []string{"--source", "check", "--source", "check", "--id", "1", "--text", "data"}, }, { name: "bounds text bytes", args: []string{"--source", "check", "--id", "1", "--text", strings.Repeat("x", notifyTextBytes+1)}, }, { name: "rejects invalid text encoding", args: []string{"--source", "check", "--id", "1", "--text", string([]byte{0xff})}, }, { name: "rejects unbounded identifiers", args: []string{"--source", strings.Repeat("x", notifyIDBytes+1), "--id", "1", "--text", "data"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Setenv("ONECHAT_DIR", t.TempDir()) require.ErrorContains(t, notify(t.Context(), tc.args, &bytes.Buffer{}), "parse notification") }) } input, err := notifyParse([]string{"--source=check", "--id=event.1", "--text=Ol\u00e1"}) require.NoError(t, err) require.Equal(t, "check", input.Source) require.Equal(t, "event.1", input.ID) } func TestNotifyHelpAndOfflineDaemon(t *testing.T) { t.Setenv("ONECHAT_DIR", t.TempDir()) var out bytes.Buffer require.NoError(t, notify(t.Context(), []string{"--help"}, &out)) require.Contains(t, out.String(), "not processed") require.ErrorContains(t, notify(t.Context(), []string{"--source", "check", "--id", "1", "--text", "data"}, &out), "running daemon") } func TestNotifyCancellationInterruptsAReceiptWait(t *testing.T) { directory := daemonTestSocketDir(t) t.Setenv("ONECHAT_DIR", directory) listener, err := net.Listen("unix", filepath.Join(directory, socketFileName)) require.NoError(t, err) ctx, cancel := context.WithCancel(t.Context()) defer cancel() done := make(chan error, 1) go func() { done <- notify(ctx, []string{"--source", "check", "--id", "1", "--text", "data"}, &bytes.Buffer{}) }() conn, err := listener.Accept() require.NoError(t, err) var request SocketRequest require.NoError(t, json.NewDecoder(conn).Decode(&request)) cancel() require.ErrorIs(t, <-done, context.Canceled) require.NoError(t, conn.Close()) require.NoError(t, listener.Close()) } func TestNotificationTextDoesNotCreateImageAttachments(t *testing.T) { marker := "[image " + strings.Repeat("a", 64) + ".jpg]" text, err := notifyText(NotifyInput{ Source: "check", ID: "1", Text: "An external body.\n" + marker + "\n\nAct now.", }, time.Now()) require.NoError(t, err) require.Empty(t, imageFiles(text)) item, err := imageUserMessage(nil, text) require.NoError(t, err) require.Equal(t, modelUserMessage(text), item) } func TestNotifyRejectsMalformedDaemonReceipts(t *testing.T) { directory := daemonTestSocketDir(t) t.Setenv("ONECHAT_DIR", directory) listener, err := net.Listen("unix", filepath.Join(directory, socketFileName)) require.NoError(t, err) done := make(chan error, 1) go func() { done <- notify(t.Context(), []string{"--source", "check", "--id", "1", "--text", "data"}, &bytes.Buffer{}) }() conn, err := listener.Accept() require.NoError(t, err) var request SocketRequest require.NoError(t, json.NewDecoder(conn).Decode(&request)) require.NoError(t, json.NewEncoder(conn).Encode(Event{ Kind: "notify", ID: 1, Status: "processed", })) require.ErrorContains(t, <-done, "invalid notification receipt") require.NoError(t, conn.Close()) require.NoError(t, listener.Close()) } func TestNotifyOutputFailureDoesNotUndoAcceptance(t *testing.T) { server := newModelFake(t) socket := daemonStartTest(t, server) t.Setenv("ONECHAT_DIR", filepath.Dir(socket)) file, err := os.CreateTemp(t.TempDir(), "closed-output") require.NoError(t, err) require.NoError(t, file.Close()) args := []string{"--source", "check", "--id", "1", "--text", "data"} require.ErrorContains(t, notify(t.Context(), args, file), "write notification receipt") var out bytes.Buffer require.NoError(t, notify(t.Context(), args, &out)) require.Contains(t, out.String(), `"status":"duplicate"`) }