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

147 lines
5.5 KiB
Go

package agent
import (
"context"
"strings"
"testing"
)
// TestSupportsSessionResume_PerAdapter pins which adapters advertise durable
// session resume. Claude and codex have native resume (claude --resume,
// codex exec resume); every other adapter must run cold so the pipeline's
// fallback path records the cold invocation instead of assuming reuse.
func TestSupportsSessionResume_PerAdapter(t *testing.T) {
cases := []struct {
name string
agent Agent
want bool
}{
{"claude", &claudeAgent{bin: "claude"}, true},
{"codex", &codexAgent{bin: "codex"}, true},
{"rovodev", &rovodevAgent{bin: "acli"}, false},
{"opencode", &opencodeAgent{bin: "opencode"}, false},
{"pi", &piAgent{bin: "pi"}, false},
{"copilot", &copilotAgent{bin: "copilot"}, false},
{"acpx", &acpxAgent{bin: "acpx", target: "gemini"}, false},
{"noop", NewNoop(), false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := SupportsSessionResume(tc.agent); got != tc.want {
t.Fatalf("SupportsSessionResume(%s) = %v, want %v", tc.name, got, tc.want)
}
})
}
}
func TestClaudeAgent_BuildArgs_Resume(t *testing.T) {
ca := &claudeAgent{bin: "claude"}
args := ca.buildArgs("re-review the branch", nil, "sess-1234")
joined := strings.Join(args, " ")
if !strings.Contains(joined, "--resume sess-1234") {
t.Fatalf("resume args missing --resume <id>: %v", args)
}
if !strings.Contains(joined, "-p re-review the branch") {
t.Fatalf("resume args must keep print-mode prompt: %v", args)
}
if strings.Contains(joined, "--fork-session") {
t.Fatalf("resume must continue the same session, not fork: %v", args)
}
}
func TestClaudeAgent_BuildArgs_NoResumeByDefault(t *testing.T) {
ca := &claudeAgent{bin: "claude"}
args := ca.buildArgs("review", nil, "")
for _, a := range args {
if a == "--resume" {
t.Fatalf("cold invocation must not pass --resume: %v", args)
}
}
}
func TestParseClaudeEvents_CapturesSessionID(t *testing.T) {
events := `{"type":"system","subtype":"init","session_id":"0dc121a5-fbc0-4e5a-ae81-5851cc03ca1b"}
{"type":"assistant","session_id":"0dc121a5-fbc0-4e5a-ae81-5851cc03ca1b","message":{"model":"claude-opus-4-8","usage":{"input_tokens":1,"output_tokens":1},"content":[{"type":"text","text":"ok"}]}}
{"type":"result","subtype":"success","session_id":"0dc121a5-fbc0-4e5a-ae81-5851cc03ca1b","usage":{"input_tokens":1,"output_tokens":1}}
`
var usage TokenUsage
var result *claudeResult
if err := parseClaudeEvents(context.Background(), strings.NewReader(events), nil, &usage, &result); err != nil {
t.Fatalf("parse: %v", err)
}
if result == nil {
t.Fatal("expected result event")
}
if result.sessionID != "0dc121a5-fbc0-4e5a-ae81-5851cc03ca1b" {
t.Fatalf("sessionID = %q, want the stream session id", result.sessionID)
}
if result.model != "claude-opus-4-8" {
t.Fatalf("model = %q, want claude-opus-4-8", result.model)
}
}
func TestParseClaudeEvents_SessionIDFallsBackToLastSeen(t *testing.T) {
// Some result events may omit session_id; the last-seen stream value wins.
events := `{"type":"assistant","session_id":"sess-a","message":{"usage":{"input_tokens":1,"output_tokens":1},"content":[{"type":"text","text":"ok"}]}}
{"type":"result","subtype":"success","usage":{"input_tokens":1,"output_tokens":1}}
`
var usage TokenUsage
var result *claudeResult
if err := parseClaudeEvents(context.Background(), strings.NewReader(events), nil, &usage, &result); err != nil {
t.Fatalf("parse: %v", err)
}
if result == nil || result.sessionID != "sess-a" {
t.Fatalf("expected fallback session id sess-a, got %+v", result)
}
}
func TestCodexAgent_BuildArgs_Resume(t *testing.T) {
ca := &codexAgent{bin: "codex"}
args := ca.buildArgs("re-review the branch", "/tmp/schema.json", "thread-99")
joined := strings.Join(args, " ")
if !strings.HasPrefix(joined, "exec resume thread-99 ") {
t.Fatalf("resume args must start with exec resume <id>: %v", args)
}
if !strings.Contains(joined, "--json") {
t.Fatalf("resume args must keep --json: %v", args)
}
if !strings.Contains(joined, "--output-schema /tmp/schema.json") {
t.Fatalf("resume args must keep --output-schema: %v", args)
}
if !strings.Contains(joined, "--dangerously-bypass-approvals-and-sandbox") {
t.Fatalf("resume args must keep the sandbox bypass: %v", args)
}
// codex exec resume (0.144) does not accept --color; passing it fails the
// whole invocation.
if strings.Contains(joined, "--color") {
t.Fatalf("resume args must not include --color: %v", args)
}
}
func TestCodexAgent_BuildArgs_ResumeKeepsExtraArgs(t *testing.T) {
ca := &codexAgent{bin: "codex", extraArgs: []string{"-m", "gpt-5.2-codex"}}
args := ca.buildArgs("prompt", "", "thread-1")
joined := strings.Join(args, " ")
if !strings.HasPrefix(joined, "exec resume -m gpt-5.2-codex thread-1 prompt") {
t.Fatalf("resume args must interleave user extraArgs before the session id: %v", args)
}
}
func TestParseCodexEvents_CapturesThreadID(t *testing.T) {
events := `{"type":"thread.started","thread_id":"019f4d4d-5dc0-75c1-8efe-adf4531bd733"}
{"type":"item.completed","item":{"type":"agent_message","text":"done"}}
{"type":"turn.completed","usage":{"input_tokens":1,"output_tokens":1}}
`
var usage TokenUsage
var lastMessage, codexErr, threadID string
if err := parseCodexEvents(context.Background(), strings.NewReader(events), nil, &usage, &lastMessage, &codexErr, &threadID, nil); err != nil {
t.Fatalf("parse: %v", err)
}
if threadID != "019f4d4d-5dc0-75c1-8efe-adf4531bd733" {
t.Fatalf("threadID = %q, want the thread.started id", threadID)
}
}