f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
683 lines
18 KiB
Go
683 lines
18 KiB
Go
package insight
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestParseCodexStream(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "agent messages",
|
|
input: `{"type":"thread.started","thread_id":"abc"}
|
|
{"type":"turn.started"}
|
|
{"type":"item.started","item":{"id":"m1","type":"agent_message"}}
|
|
{"type":"item.updated","item":{"id":"m1","type":"agent_message","text":"partial"}}
|
|
{"type":"item.completed","item":{"id":"m1","type":"agent_message","text":"# Summary\nDone."}}
|
|
{"type":"turn.completed"}
|
|
`,
|
|
want: `# Summary
|
|
Done.`,
|
|
},
|
|
{
|
|
name: "multiple messages",
|
|
input: `{"type":"item.completed","item":{"id":"m1","type":"agent_message","text":"First"}}
|
|
{"type":"item.completed","item":{"id":"m2","type":"agent_message","text":"Second"}}
|
|
`,
|
|
want: `First
|
|
Second`,
|
|
},
|
|
{
|
|
name: "turn failed",
|
|
input: `{"type":"turn.started"}
|
|
{"type":"turn.failed","error":{"message":"rate limit"}}
|
|
`,
|
|
wantError: "rate limit",
|
|
},
|
|
{
|
|
name: "deduplicates by id",
|
|
input: `{"type":"item.updated","item":{"id":"m1","type":"agent_message","text":"v1"}}
|
|
{"type":"item.updated","item":{"id":"m1","type":"agent_message","text":"v2"}}
|
|
{"type":"item.completed","item":{"id":"m1","type":"agent_message","text":"v3"}}
|
|
`,
|
|
want: "v3",
|
|
},
|
|
{
|
|
name: "skips malformed json",
|
|
input: `not valid json
|
|
{"type":"item.completed","item":{"id":"m1","type":"agent_message","text":"OK"}}
|
|
`,
|
|
want: "OK",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := parseCodexStream(strings.NewReader(tt.input), nil)
|
|
if tt.wantError != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantError)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseStreamJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "result event",
|
|
input: `{"type":"system","subtype":"init"}
|
|
{"type":"assistant","message":{"content":"Working..."}}
|
|
{"type":"result","result":"# Final Summary"}
|
|
`,
|
|
want: "# Final Summary",
|
|
},
|
|
{
|
|
name: "falls back to assistant messages",
|
|
input: `{"type":"assistant","message":{"content":"Part 1"}}
|
|
{"type":"assistant","message":{"content":"Part 2"}}
|
|
`,
|
|
want: `Part 1
|
|
Part 2`,
|
|
},
|
|
{
|
|
name: "gemini format",
|
|
input: `{"type":"system","subtype":"init"}
|
|
{"type":"message","role":"assistant","content":"Analysis done.","delta":true}
|
|
{"type":"result","result":"# Full Result"}
|
|
`,
|
|
want: "# Full Result",
|
|
},
|
|
{
|
|
name: "error event",
|
|
input: `{"type":"system","subtype":"init"}
|
|
{"type":"error","error":{"message":"rate limited"}}
|
|
`,
|
|
wantError: "rate limited",
|
|
},
|
|
{
|
|
name: "empty",
|
|
input: "",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := parseStreamJSON(strings.NewReader(tt.input), nil)
|
|
if tt.wantError != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantError)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCollectStreamLines_LargeLine(t *testing.T) {
|
|
longLine := strings.Repeat("x", 3*1024*1024)
|
|
input := longLine + "\nsmall-line\n"
|
|
var got []LogEvent
|
|
|
|
done := collectStreamLines(
|
|
strings.NewReader(input), "stderr",
|
|
func(ev LogEvent) {
|
|
got = append(got, ev)
|
|
},
|
|
)
|
|
text := <-done
|
|
|
|
require.Len(t, got, 2)
|
|
assert.Equal(t, "stderr", got[0].Stream)
|
|
assert.Len(t, got[0].Line, len(longLine))
|
|
assert.Equal(t, "small-line", got[1].Line)
|
|
assert.Contains(t, text, "small-line")
|
|
}
|
|
|
|
func TestAgentEnv(t *testing.T) {
|
|
t.Setenv("ANTHROPIC_API_KEY", "sk-secret")
|
|
t.Setenv("CUSTOM_VAR", "custom-val")
|
|
|
|
env := agentEnv()
|
|
envMap := make(map[string]string, len(env))
|
|
for _, e := range env {
|
|
k, v, _ := strings.Cut(e, "=")
|
|
envMap[strings.ToUpper(k)] = v
|
|
}
|
|
|
|
// Full env is passed through — no filtering.
|
|
assert.Equal(t, "sk-secret", envMap["ANTHROPIC_API_KEY"], "ANTHROPIC_API_KEY should be preserved")
|
|
assert.Equal(t, "custom-val", envMap["CUSTOM_VAR"], "CUSTOM_VAR should be preserved")
|
|
assert.Equal(t, "1", envMap["CLAUDE_NO_SOUND"])
|
|
}
|
|
|
|
func TestValidAgents(t *testing.T) {
|
|
for _, agent := range []string{
|
|
"claude", "codex", "copilot", "gemini", "kiro",
|
|
} {
|
|
assert.True(t, ValidAgents[agent], "%s should be valid", agent)
|
|
}
|
|
assert.False(t, ValidAgents["gpt"], "gpt should not be valid")
|
|
}
|
|
|
|
func createMockBinary(
|
|
t *testing.T, stdout string, exitCode int, writeArgs bool, name string,
|
|
) (bin, argsFile string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
dataFile := filepath.Join(dir, "stdout.txt")
|
|
require.NoError(t, os.WriteFile(dataFile, []byte(stdout), 0o644))
|
|
|
|
if writeArgs {
|
|
argsFile = filepath.Join(dir, "args.txt")
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
bin = filepath.Join(dir, name+".cmd")
|
|
var script string
|
|
if writeArgs {
|
|
script = fmt.Sprintf(
|
|
"@echo off\r\n@break > %q\r\n@:write_args\r\n@if \"%%~1\"==\"\" goto done_args\r\n@>> %q echo %%~1\r\n@shift\r\n@goto write_args\r\n@:done_args\r\n@type %q\r\n@exit /b %d\r\n",
|
|
argsFile,
|
|
argsFile,
|
|
dataFile,
|
|
exitCode,
|
|
)
|
|
} else {
|
|
script = fmt.Sprintf("@type %q\r\n@exit /b %d\r\n", dataFile, exitCode)
|
|
}
|
|
require.NoError(t, os.WriteFile(bin, []byte(script), 0o755))
|
|
return bin, argsFile
|
|
}
|
|
|
|
bin = filepath.Join(dir, name)
|
|
var script string
|
|
if writeArgs {
|
|
script = fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' \"$@\" > %s\nprintf '%%s' %s\nexit %d\n", shellQuote(argsFile), shellQuote(stdout), exitCode)
|
|
} else {
|
|
script = fmt.Sprintf("#!/bin/sh\nprintf '%%s' %s\nexit %d\n", shellQuote(stdout), exitCode)
|
|
}
|
|
require.NoError(t, os.WriteFile(bin, []byte(script), 0o755))
|
|
return bin, argsFile
|
|
}
|
|
|
|
// fakeClaudeBin writes a script that prints the given stdout
|
|
// and exits with the given code, ignoring all flags. Uses a
|
|
// .cmd batch file on Windows and a shell script elsewhere.
|
|
func fakeClaudeBin(
|
|
t *testing.T, stdout string, exitCode int,
|
|
) string {
|
|
bin, _ := createMockBinary(t, stdout, exitCode, false, "claude")
|
|
return bin
|
|
}
|
|
|
|
func shellQuote(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
|
|
}
|
|
|
|
func readArgLines(t *testing.T, argsFile string) []string {
|
|
t.Helper()
|
|
argsData, err := os.ReadFile(argsFile)
|
|
require.NoError(t, err, "reading args")
|
|
lines := strings.Split(strings.TrimSpace(string(argsData)), "\n")
|
|
args := make([]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
args = append(args, line)
|
|
}
|
|
}
|
|
return args
|
|
}
|
|
|
|
func TestGenerateStreamWithOptions_UsesConfiguredBinary(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("shell script test not supported on windows")
|
|
}
|
|
|
|
stdout := `[{"type":"result","result":"OK","modelUsage":{"m1":{}}}]`
|
|
bin, _ := createMockBinary(t, stdout, 0, false, "custom-claude")
|
|
t.Setenv("PATH", "/bin:/usr/bin")
|
|
|
|
result, err := GenerateStreamWithOptions(
|
|
context.Background(), "claude", "test prompt", nil,
|
|
GenerateOptions{
|
|
Agents: map[string]AgentConfig{
|
|
"claude": {Binary: bin},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "OK", result.Content)
|
|
}
|
|
|
|
func TestGenerateClaude_CLIFlags(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("shell script test not supported on windows")
|
|
}
|
|
|
|
stdout := `[{"type":"result","result":"OK","modelUsage":{"m1":{}}}]`
|
|
bin, argsFile := createMockBinary(
|
|
t, stdout, 0, true, "claude",
|
|
)
|
|
|
|
result, err := generateClaude(
|
|
context.Background(), bin, "test prompt", nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "OK", result.Content)
|
|
|
|
argsData, err := os.ReadFile(argsFile)
|
|
require.NoError(t, err, "reading args")
|
|
args := strings.Split(
|
|
strings.TrimSpace(string(argsData)), "\n",
|
|
)
|
|
|
|
// The empty string value for --tools is lost by the
|
|
// shell printf, so verify args as a joined string.
|
|
joined := strings.Join(args, " ")
|
|
for _, want := range []string{
|
|
"-p",
|
|
"--output-format json",
|
|
"--no-session-persistence",
|
|
"--tools",
|
|
} {
|
|
assert.Contains(t, joined, want)
|
|
}
|
|
}
|
|
|
|
func TestGenerateCodex_CLIFlags(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("shell script test not supported on windows")
|
|
}
|
|
|
|
stdout := `{"type":"item.completed","item":{"id":"m1","type":"agent_message","text":"OK"}}
|
|
`
|
|
bin, argsFile := createMockBinary(
|
|
t, stdout, 0, true, "codex",
|
|
)
|
|
|
|
result, err := generateCodex(
|
|
context.Background(), bin, "test prompt", nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "OK", result.Content)
|
|
|
|
argsData, err := os.ReadFile(argsFile)
|
|
require.NoError(t, err, "reading args")
|
|
args := strings.Split(
|
|
strings.TrimSpace(string(argsData)), "\n",
|
|
)
|
|
|
|
wantArgs := []string{
|
|
"exec", "--json",
|
|
"--sandbox", "read-only",
|
|
"--skip-git-repo-check",
|
|
"--ephemeral",
|
|
"-",
|
|
}
|
|
assert.Equal(t, wantArgs, args)
|
|
}
|
|
|
|
func TestGenerateCopilot_CLIFlags(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("shell script test not supported on windows")
|
|
}
|
|
|
|
bin, argsFile := createMockBinary(
|
|
t, "Hello from copilot", 0, true, "copilot",
|
|
)
|
|
|
|
result, err := generateCopilot(
|
|
context.Background(), bin, "test prompt", nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Hello from copilot", result.Content)
|
|
assert.Equal(t, "copilot", result.Agent)
|
|
|
|
argsData, err := os.ReadFile(argsFile)
|
|
require.NoError(t, err, "reading args")
|
|
args := strings.Split(
|
|
strings.TrimSpace(string(argsData)), "\n",
|
|
)
|
|
|
|
wantArgs := []string{
|
|
"-p", "test prompt",
|
|
"--silent",
|
|
"--no-custom-instructions",
|
|
"--no-ask-user",
|
|
"--disable-builtin-mcps",
|
|
}
|
|
assert.Equal(t, wantArgs, args)
|
|
}
|
|
|
|
func TestGenerateCopilot_EmptyResult(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("shell script test not supported on windows")
|
|
}
|
|
|
|
bin, _ := createMockBinary(
|
|
t, "", 0, false, "copilot",
|
|
)
|
|
|
|
_, err := generateCopilot(
|
|
context.Background(), bin, "test", nil,
|
|
)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "empty result")
|
|
}
|
|
|
|
func TestGenerateCopilot_PreservesBlankLines(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("shell script test not supported on windows")
|
|
}
|
|
|
|
multiParagraph := "# Summary\n\nParagraph one.\n\nParagraph two.\n"
|
|
bin, _ := createMockBinary(
|
|
t, multiParagraph, 0, false, "copilot",
|
|
)
|
|
|
|
result, err := generateCopilot(
|
|
context.Background(), bin, "test", nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, result.Content, "\n\n", "blank lines lost")
|
|
}
|
|
|
|
func TestGenerateClaude_SalvageOnNonZeroExit(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stdout string
|
|
exitCode int
|
|
wantResult string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "non-zero exit with valid result",
|
|
stdout: `[{"type":"result","result":"# Analysis\nDone.","modelUsage":{"m1":{}}}]`,
|
|
exitCode: 1,
|
|
wantResult: "# Analysis\nDone.",
|
|
},
|
|
{
|
|
name: "non-zero exit with empty result",
|
|
stdout: `[{"type":"result","result":"","modelUsage":{"m1":{}}}]`,
|
|
exitCode: 1,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "non-zero exit with invalid JSON",
|
|
stdout: `not json`,
|
|
exitCode: 1,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "non-zero exit with no stdout",
|
|
stdout: "",
|
|
exitCode: 1,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "zero exit with valid result",
|
|
stdout: `[{"type":"result","result":"OK","modelUsage":{"m2":{}}}]`,
|
|
exitCode: 0,
|
|
wantResult: "OK",
|
|
},
|
|
{
|
|
name: "zero exit with empty result",
|
|
stdout: `[{"type":"result","result":"","modelUsage":{"m2":{}}}]`,
|
|
exitCode: 0,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bin := fakeClaudeBin(
|
|
t, tt.stdout, tt.exitCode,
|
|
)
|
|
result, err := generateClaude(
|
|
context.Background(), bin, "test", nil,
|
|
)
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantResult, result.Content)
|
|
assert.Equal(t, "claude", result.Agent)
|
|
})
|
|
}
|
|
}
|
|
|
|
// fakeGeminiBin writes a script that records its argv to an
|
|
// args file, then prints stream-json output. This lets tests
|
|
// verify both the CLI flags and the parsed result.
|
|
func fakeGeminiBin(
|
|
t *testing.T, stdout string, exitCode int,
|
|
) (bin, argsFile string) {
|
|
return createMockBinary(t, stdout, exitCode, true, "gemini")
|
|
}
|
|
|
|
func fakeGeminiBinWithEnvCapture(
|
|
t *testing.T, stdout string, exitCode int,
|
|
) (bin, argsFile, envFile string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
dataFile := filepath.Join(dir, "stdout.txt")
|
|
require.NoError(t, os.WriteFile(dataFile, []byte(stdout), 0o644))
|
|
argsFile = filepath.Join(dir, "args.txt")
|
|
envFile = filepath.Join(dir, "env.txt")
|
|
|
|
if runtime.GOOS == "windows" {
|
|
bin = filepath.Join(dir, "gemini.cmd")
|
|
script := fmt.Sprintf(
|
|
"@echo off\r\n@break > %q\r\n@:write_args\r\n@if \"%%~1\"==\"\" goto done_args\r\n@>> %q echo %%~1\r\n@shift\r\n@goto write_args\r\n@:done_args\r\n@echo %%GEMINI_SANDBOX%%> %q\r\n@type %q\r\n@exit /b %d\r\n",
|
|
argsFile,
|
|
argsFile,
|
|
envFile,
|
|
dataFile,
|
|
exitCode,
|
|
)
|
|
require.NoError(t, os.WriteFile(bin, []byte(script), 0o755))
|
|
return bin, argsFile, envFile
|
|
}
|
|
|
|
bin = filepath.Join(dir, "gemini")
|
|
script := fmt.Sprintf(
|
|
"#!/bin/sh\nprintf '%%s\\n' \"$@\" > %s\nprintf '%%s' \"$GEMINI_SANDBOX\" > %s\ncat %s\nexit %d\n",
|
|
shellQuote(argsFile),
|
|
shellQuote(envFile),
|
|
shellQuote(dataFile),
|
|
exitCode,
|
|
)
|
|
require.NoError(t, os.WriteFile(bin, []byte(script), 0o755))
|
|
return bin, argsFile, envFile
|
|
}
|
|
|
|
func TestGenerateGemini_ModelFlag(t *testing.T) {
|
|
streamJSON := `{"type":"message","role":"assistant","content":"Hello"}
|
|
{"type":"result","result":"# Analysis"}
|
|
`
|
|
|
|
bin, argsFile := fakeGeminiBin(t, streamJSON, 0)
|
|
|
|
result, err := generateGemini(
|
|
context.Background(), bin, "test prompt", nil,
|
|
AgentConfig{AllowUnsafe: true},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "# Analysis", result.Content)
|
|
assert.Equal(t, "gemini", result.Agent)
|
|
assert.Equal(t, geminiInsightModel, result.Model)
|
|
|
|
// Verify the CLI was invoked with --model flag.
|
|
args := readArgLines(t, argsFile)
|
|
|
|
wantArgs := []string{
|
|
"--model", geminiInsightModel,
|
|
"--output-format", "stream-json",
|
|
}
|
|
assert.Equal(t, wantArgs, args)
|
|
}
|
|
|
|
func TestGenerateGemini_RequiresSandboxOrUnsafeOptIn(t *testing.T) {
|
|
bin, _ := fakeGeminiBin(t, "", 0)
|
|
|
|
_, err := generateGemini(
|
|
context.Background(), bin, "test prompt", nil,
|
|
AgentConfig{},
|
|
)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "explicit sandbox or unsafe opt-in")
|
|
}
|
|
|
|
func TestGenerateGemini_SetsSandboxEnv(t *testing.T) {
|
|
streamJSON := `{"type":"result","result":"# Analysis"}`
|
|
bin, argsFile, envFile := fakeGeminiBinWithEnvCapture(
|
|
t, streamJSON, 0,
|
|
)
|
|
|
|
result, err := generateGemini(
|
|
context.Background(), bin, "test prompt", nil,
|
|
AgentConfig{Sandbox: "sandbox-exec"},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "# Analysis", result.Content)
|
|
assert.Equal(t, []string{
|
|
"--model", geminiInsightModel,
|
|
"--output-format", "stream-json",
|
|
}, readArgLines(t, argsFile))
|
|
envData, err := os.ReadFile(envFile)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "sandbox-exec", strings.TrimSpace(string(envData)))
|
|
}
|
|
|
|
func TestGenerateClaude_CancelledContext(t *testing.T) {
|
|
// Pre-cancelled context: cmd.Run fails (runErr != nil)
|
|
// and ctx.Err() != nil → cancellation error.
|
|
bin := fakeClaudeBin(
|
|
t, `[{"type":"result","result":"OK","modelUsage":{"m1":{}}}]`, 0,
|
|
)
|
|
ctx, cancel := context.WithCancel(
|
|
context.Background(),
|
|
)
|
|
cancel()
|
|
|
|
_, err := generateClaude(ctx, bin, "test", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "cancel")
|
|
}
|
|
|
|
func TestGenerateClaude_SuccessNotDiscarded(t *testing.T) {
|
|
// Successful cmd.Run should return the result even if
|
|
// the context is not fresh (regression test for gating
|
|
// ctx.Err() on runErr != nil).
|
|
bin := fakeClaudeBin(
|
|
t, `[{"type":"result","result":"OK","modelUsage":{"m1":{}}}]`, 0,
|
|
)
|
|
result, err := generateClaude(
|
|
context.Background(), bin, "test", nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "OK", result.Content)
|
|
}
|
|
|
|
func TestParseCLIResult(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantResult string
|
|
wantModel string
|
|
}{
|
|
{
|
|
name: "array format with result event",
|
|
input: `[{"type":"result","result":"# Summary","modelUsage":{"claude-3":{}}}]`,
|
|
wantResult: "# Summary",
|
|
wantModel: "claude-3",
|
|
},
|
|
{
|
|
name: "legacy single-object format",
|
|
input: `{"result":"legacy result","model":"old-model"}`,
|
|
wantResult: "legacy result",
|
|
wantModel: "old-model",
|
|
},
|
|
{
|
|
name: "array with no result event",
|
|
input: `[{"type":"system_prompt","content":"hello"},{"type":"turn","result":""}]`,
|
|
wantResult: "",
|
|
wantModel: "",
|
|
},
|
|
{
|
|
name: "empty input",
|
|
input: ``,
|
|
wantResult: "",
|
|
wantModel: "",
|
|
},
|
|
{
|
|
name: "garbage input",
|
|
input: `not json at all`,
|
|
wantResult: "",
|
|
wantModel: "",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, model := parseCLIResult([]byte(tc.input))
|
|
assert.Equal(t, tc.wantResult, result)
|
|
assert.Equal(t, tc.wantModel, model)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateClaude_TruncatesLargeStdoutLogEvent(t *testing.T) {
|
|
largeResult := strings.Repeat("x", claudeStdoutLogMaxBytes*2)
|
|
stdout := fmt.Sprintf(`[{"type":"result","result":%q,"modelUsage":{"m1":{}}}]`, largeResult)
|
|
bin := fakeClaudeBin(t, stdout, 0)
|
|
|
|
var logs []LogEvent
|
|
result, err := generateClaude(
|
|
context.Background(),
|
|
bin,
|
|
"test",
|
|
func(ev LogEvent) { logs = append(logs, ev) },
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, largeResult, result.Content, "result content was truncated unexpectedly")
|
|
|
|
var stdoutLog string
|
|
for _, ev := range logs {
|
|
if ev.Stream == "stdout" {
|
|
stdoutLog = ev.Line
|
|
break
|
|
}
|
|
}
|
|
require.NotEmpty(t, stdoutLog, "expected stdout log event")
|
|
assert.Contains(t, stdoutLog, "[truncated ", "expected truncation marker in stdout log")
|
|
assert.Less(t, len(stdoutLog), len(stdout), "expected truncated stdout log to be smaller than raw payload")
|
|
}
|