556 lines
19 KiB
Go
556 lines
19 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/apps/node/internal/adapters/cli/status"
|
|
"iop/apps/node/internal/runtime"
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
// --- driveJSONLines tests ---
|
|
|
|
type testSink struct {
|
|
events []runtime.RuntimeEvent
|
|
}
|
|
|
|
func (s *testSink) Emit(_ context.Context, e runtime.RuntimeEvent) error {
|
|
s.events = append(s.events, e)
|
|
return nil
|
|
}
|
|
|
|
type mockLineEmitter struct {
|
|
name string
|
|
emitFn func(line string) ([]runtime.RuntimeEvent, error)
|
|
}
|
|
|
|
func (m *mockLineEmitter) Name() string { return m.name }
|
|
func (m *mockLineEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) {
|
|
if m.emitFn != nil {
|
|
return m.emitFn(line)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func TestDriveJSONLines_DispatchesEmitterEvents(t *testing.T) {
|
|
input := `{"type":"message","role":"assistant","content":"hello"}
|
|
not-json
|
|
{"type":"message","role":"assistant","content":"world"}`
|
|
outBuf := &strings.Builder{}
|
|
sink := &testSink{}
|
|
outReader := strings.NewReader(input + "\n")
|
|
|
|
mockEmitter := &mockLineEmitter{
|
|
name: "mock",
|
|
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
|
return []runtime.RuntimeEvent{
|
|
{Type: runtime.EventTypeDelta, Delta: "a:" + line},
|
|
{Type: runtime.EventTypeDelta, Delta: "b:" + line},
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-1", outBuf, mockEmitter, 4*1024*1024)
|
|
if err != nil {
|
|
t.Fatalf("driveJSONLines: %v", err)
|
|
}
|
|
if got := len(sink.events); got != 4 {
|
|
t.Fatalf("expected 4 events, got %d", got)
|
|
}
|
|
for i, ev := range sink.events {
|
|
if ev.RunID != "run-1" {
|
|
t.Errorf("event %d: RunID = %q, want %q", i, ev.RunID, "run-1")
|
|
}
|
|
if ev.Timestamp.IsZero() {
|
|
t.Errorf("event %d: Timestamp is zero", i)
|
|
}
|
|
if ev.Type == runtime.EventTypeDelta {
|
|
outputTokens += len(strings.Fields(ev.Delta))
|
|
}
|
|
}
|
|
raw := outBuf.String()
|
|
if !strings.Contains(raw, `{"type":"message"`) {
|
|
t.Fatalf("outBuf missing JSON line: %q", raw)
|
|
}
|
|
}
|
|
|
|
func TestDriveJSONLines_StopsOnEmitterError(t *testing.T) {
|
|
input := `{"type":"text"}
|
|
{"type":"error"}`
|
|
outBuf := &strings.Builder{}
|
|
sink := &testSink{}
|
|
outReader := strings.NewReader(input + "\n")
|
|
|
|
callCount := 0
|
|
mockEmitter := &mockLineEmitter{
|
|
name: "mock",
|
|
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
|
callCount++
|
|
if callCount == 2 {
|
|
return nil, errors.New("emitter failure")
|
|
}
|
|
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: "ok"}}, nil
|
|
},
|
|
}
|
|
|
|
_, err := driveJSONLines(context.Background(), outReader, sink, "run-2", outBuf, mockEmitter, 4*1024*1024)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if err.Error() != "emitter failure" {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if callCount != 2 {
|
|
t.Fatalf("expected emitter called 2 times, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestDriveJSONLines_AccumulatesRawOutput(t *testing.T) {
|
|
lines := `{"type":"text","part":{"type":"text","text":"a"}}
|
|
{"type":"text","part":{"type":"text","text":"b"}}`
|
|
outBuf := &strings.Builder{}
|
|
sink := &testSink{}
|
|
outReader := strings.NewReader(lines + "\n")
|
|
|
|
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-int", outBuf, opencodeJSONEmitter{}, 4*1024*1024)
|
|
if err != nil {
|
|
t.Fatalf("driveJSONLines: %v", err)
|
|
}
|
|
raw := outBuf.String()
|
|
if !strings.Contains(raw, `{"type":"text"`) {
|
|
t.Fatalf("outBuf missing expected line: %q", raw)
|
|
}
|
|
if newlineCount := strings.Count(raw, "\n"); newlineCount != 2 {
|
|
t.Fatalf("expected 2 newlines in outBuf, got %d", newlineCount)
|
|
}
|
|
if outputTokens != 2 {
|
|
t.Fatalf("expected 2 outputTokens, got %d", outputTokens)
|
|
}
|
|
}
|
|
|
|
func TestDriveJSONLines_ScannerBufferMax(t *testing.T) {
|
|
longLine := strings.Repeat("x", 100) + `{"type":"text"}`
|
|
outBuf := &strings.Builder{}
|
|
sink := &testSink{}
|
|
outReader := strings.NewReader(longLine + "\n")
|
|
|
|
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-buf", outBuf, &mockLineEmitter{name: "buf"}, 50)
|
|
if err != nil {
|
|
t.Fatalf("driveJSONLines: %v", err)
|
|
}
|
|
if outputTokens != 0 {
|
|
t.Fatalf("expected 0 outputTokens for line too long for buffer, got %d", outputTokens)
|
|
}
|
|
}
|
|
|
|
func TestDriveJSONLines_SkipsEmptyAndNonJSONLines(t *testing.T) {
|
|
input := "\n\nnot json at all\n \n{\"type\":\"text\"}\n"
|
|
outBuf := &strings.Builder{}
|
|
sink := &testSink{}
|
|
outReader := strings.NewReader(input)
|
|
|
|
callCount := 0
|
|
mockEmitter := &mockLineEmitter{
|
|
name: "mock",
|
|
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
|
callCount++
|
|
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: line}}, nil
|
|
},
|
|
}
|
|
|
|
_, err := driveJSONLines(context.Background(), outReader, sink, "run-skip", outBuf, mockEmitter, 4*1024*1024)
|
|
if err != nil {
|
|
t.Fatalf("driveJSONLines: %v", err)
|
|
}
|
|
if callCount != 1 {
|
|
t.Fatalf("expected emitter called 1 time, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestDriveJSONLines_OutputTokensCountedForDeltaOnly(t *testing.T) {
|
|
outBuf := &strings.Builder{}
|
|
sink := &testSink{}
|
|
outReader := strings.NewReader(`{"type":"error"}
|
|
{"type":"delta"}` + "\n")
|
|
|
|
mockEmitter := &mockLineEmitter{
|
|
name: "mock",
|
|
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
|
if strings.Contains(line, "error") {
|
|
return []runtime.RuntimeEvent{{Type: runtime.EventTypeError, Error: "bad"}}, nil
|
|
}
|
|
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: "one two three"}}, nil
|
|
},
|
|
}
|
|
|
|
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-tokens", outBuf, mockEmitter, 4*1024*1024)
|
|
if err != nil {
|
|
t.Fatalf("driveJSONLines: %v", err)
|
|
}
|
|
if outputTokens != 3 {
|
|
t.Fatalf("expected 3 outputTokens, got %d", outputTokens)
|
|
}
|
|
}
|
|
|
|
// --- emitter edge case tests (not covered by blackbox tests) ---
|
|
|
|
func TestStreamJSONEmitter_SkipsNonAssistantRoles(t *testing.T) {
|
|
e := streamJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"message","role":"user","content":"hi"}`)
|
|
if len(events) != 0 {
|
|
t.Fatalf("expected 0 events for user role, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestStreamJSONEmitter_ErrorEvent(t *testing.T) {
|
|
e := streamJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"error","error":"something broke"}`)
|
|
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "something broke" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestStreamJSONEmitter_ToolUseBecomesVisibleDelta(t *testing.T) {
|
|
e := streamJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"tool_use","tool_name":"run_shell_command","tool_id":"tool-1","parameters":{"command":"go test ./..."}}`)
|
|
if len(events) != 1 || events[0].Type != runtime.EventTypeDelta {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
if events[0].Delta != "[tool] run_shell_command: go test ./...\n" {
|
|
t.Fatalf("unexpected delta: %q", events[0].Delta)
|
|
}
|
|
if events[0].Metadata["kind"] != "tool_use" || events[0].Metadata["tool_name"] != "run_shell_command" {
|
|
t.Fatalf("unexpected metadata: %+v", events[0].Metadata)
|
|
}
|
|
}
|
|
|
|
func TestStreamJSONEmitter_ToolResultBecomesVisibleDelta(t *testing.T) {
|
|
e := streamJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"tool_result","tool_id":"tool-1","status":"success","output":"ok\n"}`)
|
|
if len(events) != 1 || events[0].Type != runtime.EventTypeDelta {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
if events[0].Delta != "[tool-result] success: ok\n" {
|
|
t.Fatalf("unexpected delta: %q", events[0].Delta)
|
|
}
|
|
if events[0].Metadata["kind"] != "tool_result" {
|
|
t.Fatalf("unexpected metadata: %+v", events[0].Metadata)
|
|
}
|
|
}
|
|
|
|
func TestStreamJSONEmitter_ResultError(t *testing.T) {
|
|
e := streamJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"result","status":"error","error":{"message":"quota exceeded"}}`)
|
|
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "quota exceeded" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestStreamJSONEmitter_EmptyContentSkipped(t *testing.T) {
|
|
e := streamJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"message","role":"assistant","content":""}`)
|
|
if len(events) != 0 {
|
|
t.Fatalf("expected 0 events for empty content, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestClaudeJSONEmitter_ErrorResult(t *testing.T) {
|
|
e := claudeJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"result","is_error":true,"result":"API timeout"}`)
|
|
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "API timeout" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestClaudeJSONEmitter_NonTextDeltaSkipped(t *testing.T) {
|
|
e := claudeJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"image_delta","data":"base64"}}}`)
|
|
if len(events) != 0 {
|
|
t.Fatalf("expected 0 events for non-text delta, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestCodexJSONEmitter_ItemDeltaBecomesDelta(t *testing.T) {
|
|
e := codexJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"item.delta","item":{"type":"agent_message","delta":"Doing."}}`)
|
|
if len(events) != 1 || events[0].Delta != "Doing." {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestCodexJSONEmitter_OutputTextDeltaBecomesDelta(t *testing.T) {
|
|
e := codexJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"response.output_text.delta","delta":"chunk"}`)
|
|
if len(events) != 1 || events[0].Delta != "chunk" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestCodexJSONEmitter_ContentOutputTextFallsBackToDelta(t *testing.T) {
|
|
e := codexJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"item.updated","item":{"type":"agent_message","message":{"content":[{"type":"output_text","text":"partial text"}]}}}`)
|
|
if len(events) != 1 || events[0].Delta != "partial text" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestCodexJSONEmitter_TurnFailedBecomesError(t *testing.T) {
|
|
e := codexJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"turn.failed","error":{"message":"quota exceeded"}}`)
|
|
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "quota exceeded" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestCodexJSONEmitter_StandardErrorEvent(t *testing.T) {
|
|
e := codexJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"error","message":"network timeout"}`)
|
|
if len(events) != 1 || events[0].Error != "network timeout" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestCodexJSONEmitter_NonAgentMessageSkipped(t *testing.T) {
|
|
e := codexJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"item.completed","item":{"type":"tool_call","text":"ls -la"}}`)
|
|
if len(events) != 0 {
|
|
t.Fatalf("expected 0 events for non-agent_message item, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestClineJSONEmitter_CompletionError(t *testing.T) {
|
|
e := clineJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"completion","status":"error","error":"task failed"}`)
|
|
if len(events) != 1 || events[0].Error != "task failed" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestClineJSONEmitter_SayErrorWithFallback(t *testing.T) {
|
|
e := clineJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"say","say":"error","message":"fallback msg"}`)
|
|
if len(events) != 1 || events[0].Error != "fallback msg" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestClineJSONEmitter_CompletionErrorWithoutErrorField(t *testing.T) {
|
|
e := clineJSONEmitter{}
|
|
events, _ := e.Emit(`{"type":"completion","status":"error"}`)
|
|
if len(events) != 1 || events[0].Error != "cline task failed" {
|
|
t.Fatalf("unexpected events: %+v", events)
|
|
}
|
|
}
|
|
|
|
// --- emitter registry tests ---
|
|
|
|
func TestEmitters_HaveDistinctNames(t *testing.T) {
|
|
testCases := []struct {
|
|
emitter lineEmitter
|
|
want string
|
|
}{
|
|
{streamJSONEmitter{}, "stream-json"},
|
|
{claudeJSONEmitter{}, "claude-json"},
|
|
{codexJSONEmitter{}, "codex-json"},
|
|
{opencodeJSONEmitter{}, "opencode-json"},
|
|
{clineJSONEmitter{}, "cline-json"},
|
|
}
|
|
for _, tc := range testCases {
|
|
if got := tc.emitter.Name(); got != tc.want {
|
|
t.Errorf("%T.Name() = %q, want %q", tc.emitter, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleSessionList_PopulatedSnapshot(t *testing.T) {
|
|
c := &CLI{
|
|
sessions: make(map[sessionKey]*profileSession),
|
|
codexSessions: make(map[sessionKey]*codexExecSession),
|
|
agySessions: make(map[sessionKey]*antigravitySession),
|
|
opencodeSessions: make(map[sessionKey]*opencodeSSESession),
|
|
}
|
|
c.sessions[sessionKey{target: "claude", sessionID: "default"}] = &profileSession{}
|
|
c.sessions[sessionKey{target: "claude", sessionID: "alt"}] = &profileSession{}
|
|
c.codexSessions[sessionKey{target: "codex", sessionID: "default"}] = &codexExecSession{}
|
|
c.agySessions[sessionKey{target: "antigravity", sessionID: "main"}] = &antigravitySession{}
|
|
c.opencodeSessions[sessionKey{target: "opencode", sessionID: "main"}] = &opencodeSSESession{}
|
|
|
|
resp := c.handleSessionList(runtime.CommandRequest{
|
|
RequestID: "req-list",
|
|
Type: runtime.CommandTypeSessionList,
|
|
Adapter: "cli",
|
|
})
|
|
|
|
if resp.Result["count"] != "5" {
|
|
t.Fatalf("count: got %q want %q", resp.Result["count"], "5")
|
|
}
|
|
want := "antigravity-print:antigravity/main,codex-exec:codex/default,opencode-sse:opencode/main,persistent:claude/alt,persistent:claude/default"
|
|
if got := resp.Result["sessions"]; got != want {
|
|
t.Fatalf("sessions: got %q want %q", got, want)
|
|
}
|
|
if resp.RequestID != "req-list" {
|
|
t.Fatalf("request id not echoed: %+v", resp)
|
|
}
|
|
|
|
// Sorted order: antigravity-print:antigravity/main, codex-exec:codex/default,
|
|
// opencode-sse:opencode/main, persistent:claude/alt, persistent:claude/default
|
|
wantSessions := []struct{ mode, target, sessionID, label string }{
|
|
{"antigravity-print", "antigravity", "main", "antigravity-print:antigravity/main"},
|
|
{"codex-exec", "codex", "default", "codex-exec:codex/default"},
|
|
{"opencode-sse", "opencode", "main", "opencode-sse:opencode/main"},
|
|
{"persistent", "claude", "alt", "persistent:claude/alt"},
|
|
{"persistent", "claude", "default", "persistent:claude/default"},
|
|
}
|
|
for i, ws := range wantSessions {
|
|
prefix := fmt.Sprintf("session.%d.", i)
|
|
if got := resp.Result[prefix+"label"]; got != ws.label {
|
|
t.Fatalf("%slabel: got %q want %q", prefix, got, ws.label)
|
|
}
|
|
if got := resp.Result[prefix+"mode"]; got != ws.mode {
|
|
t.Fatalf("%smode: got %q want %q", prefix, got, ws.mode)
|
|
}
|
|
if got := resp.Result[prefix+"target"]; got != ws.target {
|
|
t.Fatalf("%starget: got %q want %q", prefix, got, ws.target)
|
|
}
|
|
if got := resp.Result[prefix+"session_id"]; got != ws.sessionID {
|
|
t.Fatalf("%ssession_id: got %q want %q", prefix, got, ws.sessionID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleSessionList_SlashInSessionID(t *testing.T) {
|
|
c := &CLI{
|
|
sessions: make(map[sessionKey]*profileSession),
|
|
codexSessions: make(map[sessionKey]*codexExecSession),
|
|
agySessions: make(map[sessionKey]*antigravitySession),
|
|
opencodeSessions: make(map[sessionKey]*opencodeSSESession),
|
|
}
|
|
c.sessions[sessionKey{target: "claude", sessionID: "team/a/b"}] = &profileSession{}
|
|
|
|
resp := c.handleSessionList(runtime.CommandRequest{
|
|
RequestID: "req-slash",
|
|
Type: runtime.CommandTypeSessionList,
|
|
})
|
|
|
|
if resp.Result["count"] != "1" {
|
|
t.Fatalf("count: got %q want %q", resp.Result["count"], "1")
|
|
}
|
|
if got := resp.Result["session.0.label"]; got != "persistent:claude/team/a/b" {
|
|
t.Fatalf("session.0.label: got %q want %q", got, "persistent:claude/team/a/b")
|
|
}
|
|
if got := resp.Result["session.0.mode"]; got != "persistent" {
|
|
t.Fatalf("session.0.mode: got %q want %q", got, "persistent")
|
|
}
|
|
if got := resp.Result["session.0.target"]; got != "claude" {
|
|
t.Fatalf("session.0.target: got %q want %q", got, "claude")
|
|
}
|
|
if got := resp.Result["session.0.session_id"]; got != "team/a/b" {
|
|
t.Fatalf("session.0.session_id: got %q want %q", got, "team/a/b")
|
|
}
|
|
}
|
|
|
|
func TestJsonEmitters_RegistryMatchesImpls(t *testing.T) {
|
|
expectedKeys := []string{"stream-json", "claude-json", "codex-json", "opencode-json", "cline-json"}
|
|
for _, key := range expectedKeys {
|
|
reg, ok := jsonEmitters[key]
|
|
if !ok {
|
|
t.Fatalf("jsonEmitters[%q] not found in registry", key)
|
|
}
|
|
if reg.emitter.Name() != key {
|
|
t.Errorf("jsonEmitters[%q].emitter.Name() = %q, want %q", key, reg.emitter.Name(), key)
|
|
}
|
|
if reg.scanBufMax < 1024 {
|
|
t.Errorf("jsonEmitters[%q].scanBufMax = %d, expected at least 1024", key, reg.scanBufMax)
|
|
}
|
|
}
|
|
if len(jsonEmitters) != len(expectedKeys) {
|
|
t.Errorf("expected %d registered emitters, got %d", len(expectedKeys), len(jsonEmitters))
|
|
}
|
|
}
|
|
|
|
func TestHandleUsageStatus_EnvelopeAndParseMetadata(t *testing.T) {
|
|
t.Run("raw-only parse_status", func(t *testing.T) {
|
|
c := &CLI{
|
|
profiles: map[string]config.CLIProfileConf{
|
|
"claude": {},
|
|
},
|
|
sessions: make(map[sessionKey]*profileSession),
|
|
codexSessions: make(map[sessionKey]*codexExecSession),
|
|
agySessions: make(map[sessionKey]*antigravitySession),
|
|
opencodeSessions: make(map[sessionKey]*opencodeSSESession),
|
|
}
|
|
c.StatusChecker = func(_ context.Context, _ string, _ config.CLIProfileConf) (*status.UsageStatus, error) {
|
|
return &status.UsageStatus{RawOutput: "some raw text"}, nil
|
|
}
|
|
|
|
resp, err := c.HandleCommand(context.Background(), runtime.CommandRequest{
|
|
RequestID: "req-usage-1",
|
|
Type: runtime.CommandTypeUsageStatus,
|
|
Adapter: "cli",
|
|
Target: "claude",
|
|
SessionID: "sess-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCommand: %v", err)
|
|
}
|
|
if resp.RequestID != "req-usage-1" {
|
|
t.Fatalf("RequestID: got %q want %q", resp.RequestID, "req-usage-1")
|
|
}
|
|
if resp.Type != runtime.CommandTypeUsageStatus {
|
|
t.Fatalf("Type: got %q want %q", resp.Type, runtime.CommandTypeUsageStatus)
|
|
}
|
|
if resp.Adapter != "cli" {
|
|
t.Fatalf("Adapter: got %q want %q", resp.Adapter, "cli")
|
|
}
|
|
if resp.Target != "claude" {
|
|
t.Fatalf("Target: got %q want %q", resp.Target, "claude")
|
|
}
|
|
if resp.SessionID != "sess-1" {
|
|
t.Fatalf("SessionID: got %q want %q", resp.SessionID, "sess-1")
|
|
}
|
|
if resp.UsageStatus == nil {
|
|
t.Fatal("UsageStatus is nil")
|
|
}
|
|
if got := resp.UsageStatus.Metadata["parse_status"]; got != "raw_only" {
|
|
t.Fatalf("parse_status: got %q want %q", got, "raw_only")
|
|
}
|
|
})
|
|
|
|
t.Run("metadata-only no synthetic parse_status", func(t *testing.T) {
|
|
c := &CLI{
|
|
profiles: map[string]config.CLIProfileConf{
|
|
"claude": {},
|
|
},
|
|
sessions: make(map[sessionKey]*profileSession),
|
|
codexSessions: make(map[sessionKey]*codexExecSession),
|
|
agySessions: make(map[sessionKey]*antigravitySession),
|
|
opencodeSessions: make(map[sessionKey]*opencodeSSESession),
|
|
}
|
|
c.StatusChecker = func(_ context.Context, _ string, _ config.CLIProfileConf) (*status.UsageStatus, error) {
|
|
return &status.UsageStatus{
|
|
Metadata: map[string]string{"source": "cli"},
|
|
}, nil
|
|
}
|
|
|
|
resp, err := c.HandleCommand(context.Background(), runtime.CommandRequest{
|
|
RequestID: "req-usage-2",
|
|
Type: runtime.CommandTypeUsageStatus,
|
|
Adapter: "cli",
|
|
Target: "claude",
|
|
SessionID: "sess-2",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCommand: %v", err)
|
|
}
|
|
if resp.UsageStatus == nil {
|
|
t.Fatal("UsageStatus is nil")
|
|
}
|
|
if _, ok := resp.UsageStatus.Metadata["parse_status"]; ok {
|
|
t.Fatalf("parse_status should not be set for metadata-only result, got %q", resp.UsageStatus.Metadata["parse_status"])
|
|
}
|
|
})
|
|
}
|