Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
91 lines
3.3 KiB
Go
91 lines
3.3 KiB
Go
package compaction
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Tencent/WeKnora/internal/models/chat"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func toolCallMsg(name, arguments string) chat.Message {
|
|
return chat.Message{
|
|
Role: "assistant",
|
|
ToolCalls: []chat.ToolCall{{
|
|
ID: "call-1",
|
|
Type: "function",
|
|
Function: chat.FunctionCall{Name: name, Arguments: arguments},
|
|
}},
|
|
}
|
|
}
|
|
|
|
func TestResolveSplitsReadsFromWrites(t *testing.T) {
|
|
read, modified := extractFileOps("", []chat.Message{
|
|
toolCallMsg("write_sandbox_file", `{"path":"/workspace/output/deck.html","content":"<html>"}`),
|
|
toolCallMsg("edit_sandbox_file", `{"path":"/workspace/output/deck.html","old_string":"a"}`),
|
|
toolCallMsg("read_sandbox_file", `{"path":"/workspace/input/notes.txt"}`),
|
|
toolCallMsg("shell_exec", `{"command":"ls"}`),
|
|
}).resolve()
|
|
|
|
// Written then edited is one entry, not two.
|
|
assert.Equal(t, []string{"/workspace/output/deck.html"}, modified)
|
|
assert.Equal(t, []string{"/workspace/input/notes.txt"}, read)
|
|
}
|
|
|
|
// A file the agent read and then rewrote is only interesting as something it
|
|
// modified; listing it twice invites the model to re-read its own output.
|
|
func TestResolveDropsReadsThatWereAlsoWritten(t *testing.T) {
|
|
read, modified := extractFileOps("", []chat.Message{
|
|
toolCallMsg("read_sandbox_file", `{"path":"/workspace/output/deck.html"}`),
|
|
toolCallMsg("write_sandbox_file", `{"path":"/workspace/output/deck.html","content":"x"}`),
|
|
}).resolve()
|
|
|
|
assert.Equal(t, []string{"/workspace/output/deck.html"}, modified)
|
|
assert.Empty(t, read)
|
|
}
|
|
|
|
// Truncated arguments are the normal case for the calls this feature exists
|
|
// to survive, so they must be skipped rather than poison the list.
|
|
func TestExtractFileOpsIgnoresUnparseableArguments(t *testing.T) {
|
|
read, modified := extractFileOps("", []chat.Message{
|
|
toolCallMsg("write_sandbox_file", `{"path":"/workspace/output/a.html","content":"<htm`),
|
|
toolCallMsg("write_sandbox_file", `{"content":"no path here"}`),
|
|
}).resolve()
|
|
|
|
assert.Empty(t, modified)
|
|
assert.Empty(t, read)
|
|
}
|
|
|
|
// Without inheritance the second compaction forgets what the first one
|
|
// recorded, which is the whole failure mode this guards against. The prior
|
|
// block now arrives via the previous summary rather than as a message, because
|
|
// summaries no longer re-enter their successor's input.
|
|
func TestExtractFileOpsInheritsFromPreviousSummary(t *testing.T) {
|
|
earlier := fileOps{
|
|
written: []string{"/workspace/output/deck.html"},
|
|
read: []string{"/workspace/input/notes.txt"},
|
|
}
|
|
|
|
read, modified := extractFileOps("prose"+earlier.format(), []chat.Message{
|
|
toolCallMsg("write_sandbox_file", `{"path":"/workspace/output/report.md","content":"x"}`),
|
|
}).resolve()
|
|
|
|
assert.Equal(t, []string{"/workspace/output/deck.html", "/workspace/output/report.md"}, modified)
|
|
assert.Equal(t, []string{"/workspace/input/notes.txt"}, read)
|
|
}
|
|
|
|
func TestFileOpsFormatIsEmptyWhenNothingTouched(t *testing.T) {
|
|
assert.Empty(t, fileOps{}.format())
|
|
}
|
|
|
|
func TestFileOpsCapsTrackedPaths(t *testing.T) {
|
|
msgs := make([]chat.Message, 0, maxTrackedFilePaths+10)
|
|
for i := 0; i < maxTrackedFilePaths+10; i++ {
|
|
msgs = append(msgs, toolCallMsg("write_sandbox_file",
|
|
`{"path":"/workspace/output/`+strings.Repeat("a", i+1)+`.txt"}`))
|
|
}
|
|
|
|
_, modified := extractFileOps("", msgs).resolve()
|
|
assert.Len(t, modified, maxTrackedFilePaths)
|
|
}
|