kunchenguid--no-mistakes
26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
304 行
11 KiB
Go
304 行
11 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPiAgent_BuildArgs(t *testing.T) {
|
|
pa := &piAgent{bin: "pi"}
|
|
args := pa.buildArgs()
|
|
|
|
expected := []string{"--mode", "json", "--no-session"}
|
|
|
|
if len(args) != len(expected) {
|
|
t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
|
|
}
|
|
for i, want := range expected {
|
|
if args[i] != want {
|
|
t.Errorf("arg[%d]: expected %q, got %q", i, want, args[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_BuildArgs_PrependsExtraArgs(t *testing.T) {
|
|
pa := &piAgent{bin: "pi", extraArgs: []string{"--provider", "google"}}
|
|
args := pa.buildArgs()
|
|
|
|
expected := []string{"--provider", "google", "--mode", "json", "--no-session"}
|
|
|
|
if len(args) != len(expected) {
|
|
t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
|
|
}
|
|
for i, want := range expected {
|
|
if args[i] != want {
|
|
t.Errorf("arg[%d]: expected %q, got %q", i, want, args[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_BuildPromptIncludesSchema(t *testing.T) {
|
|
schema := json.RawMessage(`{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}`)
|
|
prompt := buildPiPrompt("do a thing", schema)
|
|
if !strings.Contains(prompt, "do a thing") {
|
|
t.Errorf("prompt missing user prompt: %s", prompt)
|
|
}
|
|
if !strings.Contains(prompt, "no-mistakes final output contract") {
|
|
t.Errorf("prompt missing contract header: %s", prompt)
|
|
}
|
|
if !strings.Contains(prompt, "summary") {
|
|
t.Errorf("prompt missing schema property: %s", prompt)
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_BuildPromptOmitsContractWhenSchemaEmpty(t *testing.T) {
|
|
prompt := buildPiPrompt("do a thing", nil)
|
|
if prompt != "do a thing" {
|
|
t.Errorf("expected raw prompt when no schema, got: %q", prompt)
|
|
}
|
|
}
|
|
|
|
func writeFakePi(t *testing.T, dir, posixScript, windowsScript string) string {
|
|
t.Helper()
|
|
|
|
name := "pi"
|
|
script := posixScript
|
|
if runtime.GOOS == "windows" {
|
|
name = "pi.cmd"
|
|
script = windowsScript
|
|
}
|
|
|
|
bin := filepath.Join(dir, name)
|
|
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
|
|
t.Fatalf("write fake pi: %v", err)
|
|
}
|
|
return bin
|
|
}
|
|
|
|
func TestPiAgent_RunParsesAssistantContentAndUsage(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Fake pi that emits a streaming text_delta plus a final message_end with
|
|
// content blocks and a usage record. Mirrors the live JSONL shape.
|
|
bin := writeFakePi(t, dir, `#!/bin/sh
|
|
cat > /dev/null
|
|
printf '%s\n' '{"type":"message_update","message":{"role":"assistant","responseId":"r1"},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"{\"ok"}}'
|
|
printf '%s\n' '{"type":"message_update","message":{"role":"assistant","responseId":"r1"},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"\":true}"}}'
|
|
printf '%s\n' '{"type":"message_end","message":{"role":"assistant","responseId":"r1","content":[{"type":"text","text":"{\"ok\":true}"}],"usage":{"input":11,"output":7,"cacheRead":3,"cacheWrite":1}}}'
|
|
printf '%s\n' '{"type":"agent_end","messages":[]}'
|
|
`, strings.Join([]string{
|
|
"@echo off",
|
|
"more > nul",
|
|
"echo {\"type\":\"message_update\",\"message\":{\"role\":\"assistant\",\"responseId\":\"r1\"},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"{\\\"ok\"}}",
|
|
"echo {\"type\":\"message_update\",\"message\":{\"role\":\"assistant\",\"responseId\":\"r1\"},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"\\\":true}\"}}",
|
|
"echo {\"type\":\"message_end\",\"message\":{\"role\":\"assistant\",\"responseId\":\"r1\",\"content\":[{\"type\":\"text\",\"text\":\"{\\\"ok\\\":true}\"}],\"usage\":{\"input\":11,\"output\":7,\"cacheRead\":3,\"cacheWrite\":1}}}",
|
|
"echo {\"type\":\"agent_end\",\"messages\":[]}",
|
|
}, "\r\n"))
|
|
|
|
schema := json.RawMessage(`{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}`)
|
|
pa := &piAgent{bin: bin}
|
|
|
|
var chunks []string
|
|
result, err := pa.Run(context.Background(), RunOpts{
|
|
Prompt: "review",
|
|
CWD: t.TempDir(),
|
|
JSONSchema: schema,
|
|
OnChunk: func(s string) { chunks = append(chunks, s) },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(result.Output) != `{"ok":true}` {
|
|
t.Fatalf("unexpected output: %s", string(result.Output))
|
|
}
|
|
if result.Usage.InputTokens != 11 || result.Usage.OutputTokens != 7 ||
|
|
result.Usage.CacheReadTokens != 3 || result.Usage.CacheCreationTokens != 1 {
|
|
t.Fatalf("unexpected usage: %+v", result.Usage)
|
|
}
|
|
if len(chunks) == 0 {
|
|
t.Fatal("expected onChunk to receive streaming text")
|
|
}
|
|
// OnChunk must receive the incremental deltas, not cumulative state.
|
|
// Otherwise the TUI log buffer (which appends each chunk) duplicates
|
|
// the running prefix.
|
|
wantChunks := []string{`{"ok`, `":true}`}
|
|
if len(chunks) != len(wantChunks) {
|
|
t.Fatalf("expected %d delta chunks, got %d: %v", len(wantChunks), len(chunks), chunks)
|
|
}
|
|
for i, want := range wantChunks {
|
|
if chunks[i] != want {
|
|
t.Errorf("chunk[%d] = %q, want %q", i, chunks[i], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_RunFallsBackToAgentEndMessages(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bin := writeFakePi(t, dir, `#!/bin/sh
|
|
cat > /dev/null
|
|
printf '%s\n' '{"type":"agent_end","messages":[{"role":"user","content":"prompt"},{"role":"assistant","content":"{\"ok\":true}"}]}'
|
|
`, strings.Join([]string{
|
|
"@echo off",
|
|
"more > nul",
|
|
"echo {\"type\":\"agent_end\",\"messages\":[{\"role\":\"user\",\"content\":\"prompt\"},{\"role\":\"assistant\",\"content\":\"{\\\"ok\\\":true}\"}]}",
|
|
}, "\r\n"))
|
|
|
|
schema := json.RawMessage(`{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}`)
|
|
pa := &piAgent{bin: bin}
|
|
result, err := pa.Run(context.Background(), RunOpts{
|
|
Prompt: "review",
|
|
CWD: t.TempDir(),
|
|
JSONSchema: schema,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(result.Output) != `{"ok":true}` {
|
|
t.Fatalf("unexpected output: %s", string(result.Output))
|
|
}
|
|
}
|
|
|
|
func TestPiParser_ClearsPriorAssistantErrorAfterSuccessfulRetry(t *testing.T) {
|
|
stream := strings.Join([]string{
|
|
`{"type":"message_end","message":{"role":"assistant","responseId":"r1","stopReason":"error","errorMessage":"transient failure"}}`,
|
|
`{"type":"message_end","message":{"role":"assistant","responseId":"r2","stopReason":"stop","content":[{"type":"text","text":"success"}]}}`,
|
|
`{"type":"agent_end","messages":[{"role":"assistant","responseId":"r1","stopReason":"error","errorMessage":"transient failure"},{"role":"assistant","responseId":"r2","stopReason":"stop","content":[{"type":"text","text":"success"}]}]}`,
|
|
}, "\n")
|
|
|
|
pp := &piParser{}
|
|
if err := pp.parse(context.Background(), strings.NewReader(stream)); err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if pp.assistantError != "" {
|
|
t.Fatalf("expected successful retry to clear assistant error, got %q", pp.assistantError)
|
|
}
|
|
if got := pp.finalText(); got != "success" {
|
|
t.Fatalf("expected final retry text, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestPiParser_SumsUniqueAssistantUsageAcrossTurns(t *testing.T) {
|
|
stream := strings.Join([]string{
|
|
`{"type":"message_end","message":{"role":"assistant","responseId":"r1","stopReason":"toolUse","content":[{"type":"toolCall","name":"bash"}],"usage":{"input":10,"output":2,"cacheRead":3,"cacheWrite":4}}}`,
|
|
`{"type":"turn_end","message":{"role":"assistant","responseId":"r1","stopReason":"toolUse","content":[{"type":"toolCall","name":"bash"}],"usage":{"input":10,"output":2,"cacheRead":3,"cacheWrite":4}}}`,
|
|
`{"type":"message_end","message":{"role":"assistant","responseId":"r2","stopReason":"stop","content":[{"type":"text","text":"done"}],"usage":{"input":1,"output":5,"cacheRead":6,"cacheWrite":7}}}`,
|
|
`{"type":"turn_end","message":{"role":"assistant","responseId":"r2","stopReason":"stop","content":[{"type":"text","text":"done"}],"usage":{"input":1,"output":5,"cacheRead":6,"cacheWrite":7}}}`,
|
|
`{"type":"agent_end","messages":[{"role":"assistant","responseId":"r1","stopReason":"toolUse","content":[{"type":"toolCall","name":"bash"}],"usage":{"input":10,"output":2,"cacheRead":3,"cacheWrite":4}},{"role":"toolResult","content":[{"type":"text","text":"ok"}]},{"role":"assistant","responseId":"r2","stopReason":"stop","content":[{"type":"text","text":"done"}],"usage":{"input":1,"output":5,"cacheRead":6,"cacheWrite":7}}]}`,
|
|
}, "\n")
|
|
|
|
pp := &piParser{}
|
|
if err := pp.parse(context.Background(), strings.NewReader(stream)); err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
want := TokenUsage{InputTokens: 11, OutputTokens: 7, CacheReadTokens: 9, CacheCreationTokens: 11, Reported: true, CacheCreationReported: true}
|
|
if pp.usage != want {
|
|
t.Fatalf("usage = %+v, want %+v", pp.usage, want)
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_RunRejectsAssistantError(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bin := writeFakePi(t, dir, `#!/bin/sh
|
|
cat > /dev/null
|
|
printf '%s\n' '{"type":"message_end","message":{"role":"assistant","stopReason":"error","errorMessage":"auth failed","content":[{"type":"text","text":"{\"ok\":true}"}]}}'
|
|
`, strings.Join([]string{
|
|
"@echo off",
|
|
"more > nul",
|
|
"echo {\"type\":\"message_end\",\"message\":{\"role\":\"assistant\",\"stopReason\":\"error\",\"errorMessage\":\"auth failed\",\"content\":[{\"type\":\"text\",\"text\":\"{\\\"ok\\\":true}\"}]}}",
|
|
}, "\r\n"))
|
|
|
|
pa := &piAgent{bin: bin}
|
|
_, err := pa.Run(context.Background(), RunOpts{
|
|
Prompt: "review",
|
|
CWD: t.TempDir(),
|
|
JSONSchema: json.RawMessage(`{"type":"object"}`),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), "auth failed") {
|
|
t.Errorf("expected error to mention 'auth failed', got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_RunRejectsEmptyOutput(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bin := writeFakePi(t, dir, `#!/bin/sh
|
|
cat > /dev/null
|
|
printf '%s\n' '{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":" "}]}}'
|
|
`, strings.Join([]string{
|
|
"@echo off",
|
|
"more > nul",
|
|
"echo {\"type\":\"message_end\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\" \"}]}}",
|
|
}, "\r\n"))
|
|
|
|
pa := &piAgent{bin: bin}
|
|
_, err := pa.Run(context.Background(), RunOpts{
|
|
Prompt: "review",
|
|
CWD: t.TempDir(),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), "no text output") {
|
|
t.Errorf("expected 'no text output', got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_RunSurfacesNonZeroExit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bin := writeFakePi(t, dir, `#!/bin/sh
|
|
cat > /dev/null
|
|
echo "boom" >&2
|
|
exit 2
|
|
`, strings.Join([]string{
|
|
"@echo off",
|
|
"more > nul",
|
|
"echo boom 1>&2",
|
|
"exit /b 2",
|
|
}, "\r\n"))
|
|
|
|
pa := &piAgent{bin: bin}
|
|
_, err := pa.Run(context.Background(), RunOpts{
|
|
Prompt: "review",
|
|
CWD: t.TempDir(),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected non-zero exit error")
|
|
}
|
|
if !strings.Contains(err.Error(), "boom") {
|
|
t.Errorf("expected stderr in error message, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPiAgent_RunCancelledByContext(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bin := writeFakePi(t, dir, `#!/bin/sh
|
|
cat > /dev/null
|
|
sleep 30
|
|
`, strings.Join([]string{
|
|
"@echo off",
|
|
"more > nul",
|
|
"timeout /t 30 /nobreak > nul",
|
|
}, "\r\n"))
|
|
|
|
pa := &piAgent{bin: bin}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
_, err := pa.Run(ctx, RunOpts{
|
|
Prompt: "review",
|
|
CWD: t.TempDir(),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error from cancelled context")
|
|
}
|
|
if !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "context canceled") {
|
|
t.Logf("got error: %v", err)
|
|
}
|
|
}
|