package agent import ( "io" "log/slog" "os" "path/filepath" "slices" "strings" "testing" "time" ) func quietAntigravityLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } func TestBuildAntigravityArgsBasic(t *testing.T) { t.Parallel() args := buildAntigravityArgs( "hello", "/tmp/agy.log", 20*time.Minute, ExecOptions{Cwd: "/work"}, quietAntigravityLogger(), ) want := []string{ "-p", "hello", "--dangerously-skip-permissions", "--print-timeout", "20m0s", "--log-file", "/tmp/agy.log", "--add-dir", "/work", } if !slices.Equal(args, want) { t.Fatalf("buildAntigravityArgs basic mismatch\n got: %v\nwant: %v", args, want) } } func TestBuildAntigravityArgsResume(t *testing.T) { t.Parallel() args := buildAntigravityArgs( "continue", "/tmp/agy.log", 20*time.Minute, ExecOptions{ResumeSessionID: "b8b263a4-4b2f-4339-acc9-78b248e2b606"}, quietAntigravityLogger(), ) joined := strings.Join(args, " ") if !strings.Contains(joined, "--conversation b8b263a4-4b2f-4339-acc9-78b248e2b606") { t.Fatalf("expected --conversation flag with id; got %v", args) } } func TestBuildAntigravityArgsFiltersBlockedCustomArgs(t *testing.T) { t.Parallel() args := buildAntigravityArgs( "go", "/tmp/agy.log", time.Minute, ExecOptions{ // Each blocked flag below must be stripped silently — the daemon // owns these because they're required for non-interactive, // resume-aware operation. CustomArgs: []string{ "-p", "hijacked-prompt", "--continue", "-c", "--conversation", "bad-id", "--dangerously-skip-permissions", "--print-timeout", "1h", "--log-file", "/elsewhere.log", "--add-dir", "/extra", // user-added workspace dir should survive }, }, quietAntigravityLogger(), ) joined := strings.Join(args, " ") // Prompt argument should appear exactly once — the daemon's, not the // user's hijacked copy. pCount := 0 for _, a := range args { if a == "-p" { pCount++ } } if pCount != 1 { t.Errorf("expected exactly one -p flag, got args=%v", args) } if strings.Contains(joined, "hijacked-prompt") { t.Errorf("custom -p value leaked through filter: %v", args) } if strings.Contains(joined, "bad-id") { t.Errorf("custom --conversation value leaked through filter: %v", args) } if strings.Contains(joined, "/elsewhere.log") { t.Errorf("custom --log-file value leaked through filter: %v", args) } if !strings.Contains(joined, "--add-dir /extra") { t.Errorf("non-blocked --add-dir flag should pass through: %v", args) } } func TestAntigravityFormatTimeoutClampsSubSecond(t *testing.T) { t.Parallel() if got := antigravityFormatTimeout(0); got != "1s" { t.Errorf("antigravityFormatTimeout(0) = %q, want 1s", got) } if got := antigravityFormatTimeout(20 * time.Minute); got != "20m0s" { t.Errorf("antigravityFormatTimeout(20m) = %q, want 20m0s", got) } } func TestReadAntigravityConversationID(t *testing.T) { t.Parallel() dir := t.TempDir() logPath := filepath.Join(dir, "agy.log") // Sample log content modelled on real agy glog output: the // conversation= line is what printmode.go writes once per dispatch. logBody := strings.Join([]string{ `I0528 13:36:19.959748 73304 printmode.go:71] Print mode: starting (promptLength=18, model="", conversationID="")`, `I0528 13:36:23.318877 73304 printmode.go:130] Print mode: conversation=b8b263a4-4b2f-4339-acc9-78b248e2b606, sending message`, `I0528 13:36:23.318892 73304 server.go:1083] Sending user message to conversation b8b263a4-4b2f-4339-acc9-78b248e2b606 (items=1, media=0)`, }, "\n") if err := os.WriteFile(logPath, []byte(logBody), 0o644); err != nil { t.Fatal(err) } got := readAntigravityConversationID(logPath) want := "b8b263a4-4b2f-4339-acc9-78b248e2b606" if got != want { t.Fatalf("readAntigravityConversationID = %q, want %q", got, want) } } func TestReadAntigravityConversationIDMissingFile(t *testing.T) { t.Parallel() if got := readAntigravityConversationID("/nonexistent/path"); got != "" { t.Errorf("expected empty string for missing file, got %q", got) } if got := readAntigravityConversationID(""); got != "" { t.Errorf("expected empty string for empty path, got %q", got) } }