mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
Fixes from the code review on the critical/high audit branch: - tools/apply_patch: parseHunkCounts now reads only the range section between the opening and closing "@@". A crafted section heading like "@@ -1,1 +1,1 @@ +1,999999" could otherwise overwrite the line count, wedge the parser in hunk mode, and swallow later file headers so they escaped validatePatchPaths / recheckPatchWriteTargets — a workspace-confinement bypass. - providerhealth: the SSRF-hardened dial now tries every validated address in order instead of pinning addrs[0], so a dual-stack/multi-record provider isn't failed by a single dead record. Extracted dialValidatedAddrs for coverage. - hooks/dispatch: a beforeTool hook killed by its deadline now fails CLOSED (vetoes the tool) instead of fail-open. A launch failure (missing binary) still fails open so a misconfigured hook can't wedge every tool call. - cron: Store.Get returns a distinct ErrJobNotFound sentinel; cron_run only treats that as "removed during run" and still records/persists on a transient read error instead of silently dropping the run. - providers/anthropic: closeOpen finalizes thinking buffers still open at stream end (SSE ended after thinking_delta/signature_delta but before content_block_stop), so they survive in the done event's ReasoningBlocks for replay. Finalized in index order. - providers/gemini: corrected the stale thinkingConfig comment (no IncludeThoughts field; thought summaries are suppressed by skipping part.Thought parts). Tests: regression coverage for each fix, plus the requested nitpicks — EOF-terminated MCP line, recipe-prompt propagation assertion, checked file.Close() in session store tests, and a shortened bash process-group timeout test via the bashWaitDelay hook. Note: declined the suggestion to replay a "signature" on redacted_thinking blocks — Anthropic's redacted_thinking carries only an opaque "data" field; the signature is for regular thinking blocks, and the stream never populates one for redacted blocks.
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
package mcp
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestMessageWriterEmitsNewlineDelimitedJSON(t *testing.T) {
|
|
var out bytes.Buffer
|
|
if err := newMessageWriter(&out).write(rpcMessage{Method: "ping"}); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
got := out.String()
|
|
if strings.Contains(got, "Content-Length") {
|
|
t.Fatalf("writer must use newline-delimited framing, got %q", got)
|
|
}
|
|
if !strings.HasSuffix(got, "\n") || strings.Count(got, "\n") != 1 {
|
|
t.Fatalf("expected exactly one trailing newline, got %q", got)
|
|
}
|
|
if !strings.HasPrefix(strings.TrimSpace(got), "{") {
|
|
t.Fatalf("expected a single JSON object line, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestMessageReaderAcceptsNewlineDelimited(t *testing.T) {
|
|
in := strings.NewReader(`{"jsonrpc":"2.0","method":"tools/list"}` + "\n")
|
|
msg, err := newMessageReader(in).read()
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if msg.Method != "tools/list" {
|
|
t.Fatalf("method=%q want tools/list", msg.Method)
|
|
}
|
|
}
|
|
|
|
func TestMessageReaderAcceptsFinalLineWithoutTrailingNewline(t *testing.T) {
|
|
// A final message that ends at EOF without a trailing newline must still be
|
|
// decoded (the read() EOF-tail branch), so a peer that doesn't terminate its
|
|
// last line isn't dropped.
|
|
in := strings.NewReader(`{"jsonrpc":"2.0","method":"tools/list"}`)
|
|
msg, err := newMessageReader(in).read()
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if msg.Method != "tools/list" {
|
|
t.Fatalf("method=%q want tools/list", msg.Method)
|
|
}
|
|
}
|
|
|
|
func TestMessageReaderAcceptsContentLengthFraming(t *testing.T) {
|
|
body := `{"jsonrpc":"2.0","method":"initialize"}`
|
|
framed := fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(body), body)
|
|
msg, err := newMessageReader(strings.NewReader(framed)).read()
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if msg.Method != "initialize" {
|
|
t.Fatalf("method=%q want initialize", msg.Method)
|
|
}
|
|
}
|
|
|
|
func TestMessageReaderRejectsOversizedContentLength(t *testing.T) {
|
|
// A hostile Content-Length must be rejected before any allocation, not used
|
|
// to size a make([]byte, n).
|
|
framed := fmt.Sprintf("Content-Length: %d\r\n\r\n", maxMessageBytes+1)
|
|
_, err := newMessageReader(strings.NewReader(framed)).read()
|
|
if err == nil {
|
|
t.Fatal("expected oversized Content-Length to be rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), "limit") {
|
|
t.Fatalf("expected limit error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMessageReaderWriterRoundTrip(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
w := newMessageWriter(&buf)
|
|
for _, method := range []string{"a", "b", "c"} {
|
|
if err := w.write(rpcMessage{Method: method}); err != nil {
|
|
t.Fatalf("write %s: %v", method, err)
|
|
}
|
|
}
|
|
r := newMessageReader(&buf)
|
|
for _, want := range []string{"a", "b", "c"} {
|
|
msg, err := r.read()
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if msg.Method != want {
|
|
t.Fatalf("method=%q want %q", msg.Method, want)
|
|
}
|
|
}
|
|
}
|