added C-c handling
This commit is contained in:
+118
-31
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
@@ -58,7 +59,7 @@ func TestParseStreamReasoningNoDuplication(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -90,7 +91,7 @@ func TestParseStreamReasoningAlias(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -108,7 +109,7 @@ func TestParseStreamContentOnly(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -129,7 +130,7 @@ func TestParseStreamReasoningOnly(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -143,7 +144,7 @@ func TestParseStreamReasoningOnly(t *testing.T) {
|
||||
|
||||
func TestParseStreamEmpty(t *testing.T) {
|
||||
for _, in := range []string{"", "\n\n", "event: message\n\n"} {
|
||||
msg, err := parseStream(strings.NewReader(in))
|
||||
msg, err := parseStream(context.Background(), strings.NewReader(in))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error for input %q: %v", in, err)
|
||||
}
|
||||
@@ -161,7 +162,7 @@ func TestParseStreamToolCallSplitAcrossChunks(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -187,7 +188,7 @@ func TestParseStreamMultipleToolCallsKeepFirstAppearanceOrder(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -209,7 +210,7 @@ func TestParseStreamJunkAndNoChoicesIgnored(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -225,7 +226,7 @@ func TestParseStreamReasoningAfterContent(t *testing.T) {
|
||||
`data: [DONE]`,
|
||||
}, "\n")
|
||||
|
||||
msg, err := parseStream(bytes.NewBufferString(sseData))
|
||||
msg, err := parseStream(context.Background(), bytes.NewBufferString(sseData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parseStream error: %v", err)
|
||||
}
|
||||
@@ -475,33 +476,45 @@ func TestIsInvalidAssistantErr(t *testing.T) {
|
||||
// ---------- shell ----------
|
||||
|
||||
func TestShellBasic(t *testing.T) {
|
||||
res := shell("echo hi", 10)
|
||||
res := shell(context.Background(), "echo hi", 10)
|
||||
if !strings.Contains(res, "hi") || !strings.HasSuffix(res, "exit: 0") {
|
||||
t.Errorf("shell(echo hi) = %q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellExitCodeAndStderr(t *testing.T) {
|
||||
res := shell("echo out; echo err >&2; exit 7", 10)
|
||||
res := shell(context.Background(), "echo out; echo err >&2; exit 7", 10)
|
||||
if !strings.Contains(res, "out") || !strings.Contains(res, "err") || !strings.HasSuffix(res, "exit: 7") {
|
||||
t.Errorf("shell multi = %q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellUnknownCommand(t *testing.T) {
|
||||
res := shell("definitely_not_a_command_xyz", 10)
|
||||
res := shell(context.Background(), "definitely_not_a_command_xyz", 10)
|
||||
if !strings.Contains(res, "exit: 127") {
|
||||
t.Errorf("expected exit 127, got %q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellTimeout(t *testing.T) {
|
||||
res := shell("sleep 5", 1)
|
||||
res := shell(context.Background(), "sleep 5", 1)
|
||||
if !strings.Contains(res, "[shell timeout after 1s]") || !strings.HasSuffix(res, "exit: -1") {
|
||||
t.Errorf("expected timeout marker and exit -1, got %q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellContextCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
res := shell(ctx, "sleep 5", 10)
|
||||
if !strings.Contains(res, "[interrupted]") || !strings.HasSuffix(res, "exit: -1") {
|
||||
t.Errorf("expected interrupted result on cancellation, got %q", res)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- last / summary ----------
|
||||
|
||||
func TestLast(t *testing.T) {
|
||||
@@ -694,23 +707,23 @@ func TestAutosave(t *testing.T) {
|
||||
// ---------- summarize / compact (error paths only, no network) ----------
|
||||
|
||||
func TestSummarizeEmpty(t *testing.T) {
|
||||
if _, err := summarize(&Cfg{}, nil); err == nil || !strings.Contains(err.Error(), "no conversation") {
|
||||
if _, err := summarize(context.Background(), &Cfg{}, nil); err == nil || !strings.Contains(err.Error(), "no conversation") {
|
||||
t.Errorf("expected no-conversation error, got %v", err)
|
||||
}
|
||||
if _, err := summarize(&Cfg{}, []Message{{Role: "system", Content: strp("sys")}}); err == nil {
|
||||
if _, err := summarize(context.Background(), &Cfg{}, []Message{{Role: "system", Content: strp("sys")}}); err == nil {
|
||||
t.Errorf("expected error for system-only conversation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactNoSystem(t *testing.T) {
|
||||
msgs, _, err := compact(&Cfg{}, nil)
|
||||
msgs, _, err := compact(context.Background(), &Cfg{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "no system message") {
|
||||
t.Errorf("expected no-system error, got %v", err)
|
||||
}
|
||||
if msgs != nil {
|
||||
t.Errorf("expected original messages on error")
|
||||
}
|
||||
msgs2, _, err2 := compact(&Cfg{}, []Message{{Role: "user", Content: strp("x")}})
|
||||
msgs2, _, err2 := compact(context.Background(), &Cfg{}, []Message{{Role: "user", Content: strp("x")}})
|
||||
if err2 == nil {
|
||||
t.Errorf("expected error when first message is not system")
|
||||
}
|
||||
@@ -930,7 +943,7 @@ func TestLLMNonStreamingAndHeaders(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "secret"
|
||||
m, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, TOOLS)
|
||||
m, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, TOOLS)
|
||||
if err != nil {
|
||||
t.Fatalf("llm: %v", err)
|
||||
}
|
||||
@@ -960,7 +973,7 @@ func TestLLMNoAuthHeaderWhenNoKey(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
if _, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err != nil {
|
||||
if _, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err != nil {
|
||||
t.Fatalf("llm: %v", err)
|
||||
}
|
||||
if gotAuth != "" {
|
||||
@@ -981,7 +994,7 @@ func TestLLMStreaming(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = true
|
||||
cfg.APIKey = "-"
|
||||
m, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
m, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("llm: %v", err)
|
||||
}
|
||||
@@ -1005,7 +1018,7 @@ func TestLLM4xxReturnsImmediately(t *testing.T) {
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
start := time.Now()
|
||||
_, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
_, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "Invalid assistant message") {
|
||||
t.Fatalf("expected 400 error, got %v", err)
|
||||
}
|
||||
@@ -1031,7 +1044,7 @@ func TestLLMRetriesOn5xxThenSucceeds(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
m, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
m, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("llm after retries: %v", err)
|
||||
}
|
||||
@@ -1053,7 +1066,7 @@ func TestLLMEmptyChoices(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
if _, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err == nil || !strings.Contains(err.Error(), "empty choices") {
|
||||
if _, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil); err == nil || !strings.Contains(err.Error(), "empty choices") {
|
||||
t.Errorf("expected empty choices error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1080,7 +1093,7 @@ func TestALToolLoop(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
msgs, err := AL(&cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("run")}}, "sys", 0)
|
||||
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("run")}}, "sys", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("AL: %v", err)
|
||||
}
|
||||
@@ -1142,7 +1155,7 @@ func TestALRunSubagent(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
msgs, err := AL(&cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("parent task")}}, "sys", 0)
|
||||
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("parent task")}}, "sys", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("AL: %v", err)
|
||||
}
|
||||
@@ -1185,7 +1198,7 @@ func TestALSubagentDepthLimit(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
msgs, err := AL(&cfg, []Message{{Role: "user", Content: strp("go")}}, "sys", MAX_DEPTH)
|
||||
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "user", Content: strp("go")}}, "sys", MAX_DEPTH)
|
||||
if err != nil {
|
||||
t.Fatalf("AL: %v", err)
|
||||
}
|
||||
@@ -1232,7 +1245,7 @@ func TestALStripsInvalidAssistantAndRetries(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
msgs, err := AL(&cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("go")}}, "sys", 0)
|
||||
msgs, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("go")}}, "sys", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("AL: %v", err)
|
||||
}
|
||||
@@ -1256,7 +1269,7 @@ func TestSummarizeHappyPath(t *testing.T) {
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
s, err := summarize(&cfg, []Message{{Role: "user", Content: strp("hello world")}})
|
||||
s, err := summarize(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hello world")}})
|
||||
if err != nil {
|
||||
t.Fatalf("summarize: %v", err)
|
||||
}
|
||||
@@ -1276,7 +1289,7 @@ func TestCompactHappyPath(t *testing.T) {
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
orig := []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("hello world")}}
|
||||
msgs, sm, err := compact(&cfg, orig)
|
||||
msgs, sm, err := compact(context.Background(), &cfg, orig)
|
||||
if err != nil {
|
||||
t.Fatalf("compact: %v", err)
|
||||
}
|
||||
@@ -1339,7 +1352,7 @@ func TestLLMForwardsRelevantParameters(t *testing.T) {
|
||||
}, "\n"))
|
||||
|
||||
cfg := getCfg(cfgFile)
|
||||
_, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
_, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("llm: %v", err)
|
||||
}
|
||||
@@ -1591,7 +1604,7 @@ func TestRenderMDNoColorFallback(t *testing.T) {
|
||||
|
||||
func TestDirectShellExecution(t *testing.T) {
|
||||
cmd := "echo direct_exec_test"
|
||||
res := shell(cmd, 10)
|
||||
res := shell(context.Background(), cmd, 10)
|
||||
if !strings.HasPrefix(res, "direct_exec_test") || !strings.Contains(res, "exit: 0") {
|
||||
t.Errorf("direct shell exec failed, got %q", res)
|
||||
}
|
||||
@@ -1604,6 +1617,80 @@ func TestDirectShellExecution(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestALContextCancellation(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
case <-done:
|
||||
}
|
||||
}))
|
||||
defer func() {
|
||||
close(done)
|
||||
srv.CloseClientConnections()
|
||||
srv.Close()
|
||||
}()
|
||||
|
||||
cfg := defCfg
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
origMsgs := []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("hello")}}
|
||||
inputMsgs := append([]Message{}, origMsgs...)
|
||||
msgs, err := AL(ctx, &cfg, inputMsgs, "sys", 0)
|
||||
if err == nil {
|
||||
t.Fatalf("expected context cancellation error, got nil")
|
||||
}
|
||||
if !errors.Is(err, context.Canceled) && ctx.Err() == nil {
|
||||
t.Errorf("expected context.Canceled, got %v", err)
|
||||
}
|
||||
// Verify that input messages slice was not mutated with partial assistant messages
|
||||
if len(msgs) != len(origMsgs) {
|
||||
t.Errorf("expected %d messages after cancellation, got %d: %+v", len(origMsgs), len(msgs), msgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMContextCancellation(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
case <-done:
|
||||
}
|
||||
}))
|
||||
defer func() {
|
||||
close(done)
|
||||
srv.CloseClientConnections()
|
||||
srv.Close()
|
||||
}()
|
||||
|
||||
cfg := defCfg
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
_, err := llm(ctx, &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("expected context cancellation error, got nil")
|
||||
}
|
||||
if !errors.Is(err, context.Canceled) && ctx.Err() == nil {
|
||||
t.Errorf("expected context.Canceled, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user