Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

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")
}