Files
weichen 43297795f5
ci / vet-and-test (pull_request) Successful in 48s
Keep original materials and cleared results reachable across turns
Why: Real-run before/after evidence (phase01 card) showed later turns never
re-read the uploaded source (0 original reads in plan-followup) because the
attachment only existed in the current turn's injection and the context window
hard default was 32000 chars - a 25k-char transcript tripped the clear-on-first
request, replacing old tool results with a pointer the model has no tool to
follow. MiniMax is an interleaved-thinking model whose docs require <think>
blocks in assistant history; dropping them degrades later turns.

Evidence: SELF - new tests TestThinkReplayWrapsAssistantHistory,
TestContextWindowScalesFromTokens, TestThinkReplayEnabled,
TestPersistInputTextsAndListWorkspaceInputs; go test ./internal/... passes;
live before/after comparison on the golden task set recorded on the phase02
task card per the new policy clause.

Impact: models.chat.contextWindow/outputReserve/contextBuffer (tokens,
defaults 128k/16k/4k) drive the in-run clear threshold, converted to chars;
cleared tool results spill to <sandbox>/.agent/results with readable paths;
attachment parse text persists as inputs/<name>.md and the env section lists
all workspace inputs every turn; context.thinkReplay (auto|on|off, auto =
MiniMax) replays thinking blocks into request-side assistant history only.
2026-10-09 14:28:39 +00:00

66 lines
2.7 KiB
Go

package runmgr
import (
"os"
"path/filepath"
"strings"
"testing"
core "agent-system-core"
)
// 附件解析文本要落 inputs/<原名>.md 且跨轮可见:后续轮上下文里原件只剩占位时,
// 模型仍能读到原文(实测「根据纪要制定方案」轮 0 次读原件,只能编细节)。
func TestPersistInputTextsAndListWorkspaceInputs(t *testing.T) {
root := t.TempDir()
if _, err := StageWorkspaceInputs(root, "alice", "conv1", []WorkspaceFile{
{Name: "会议.docx", Content: []byte("bin")},
}, "", nil); err != nil {
t.Fatal(err)
}
long := strings.Repeat("会议结论先入库。", 40) // >200 字
docs := []core.ParsedDocument{{
Meta: core.DocMeta{Filename: "会议.docx"},
Blocks: []core.DocBlock{{Type: core.BlockPara, Text: long}},
}}
PersistInputTexts(root, "alice", "conv1", docs)
extracted := filepath.Join(root, "alice", "conv1", "inputs", "会议.md")
b, err := os.ReadFile(extracted)
if err != nil {
t.Fatalf("extracted text not written: %v", err)
}
if !strings.Contains(string(b), "会议结论先入库") {
t.Fatalf("extracted text wrong: %q", string(b)[:50])
}
// 短文本(解析失败/过短)与已是文本类的原件不写。
short := []core.ParsedDocument{{Meta: core.DocMeta{Filename: "短.docx"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "太短"}}}}
PersistInputTexts(root, "alice", "conv1", short)
textKind := []core.ParsedDocument{{Meta: core.DocMeta{Filename: "笔记.md"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: long}}}}
PersistInputTexts(root, "alice", "conv1", textKind)
if _, err := os.Stat(filepath.Join(root, "alice", "conv1", "inputs", "短.md")); err == nil {
t.Fatal("short parse should not be persisted")
}
if _, err := os.Stat(filepath.Join(root, "alice", "conv1", "inputs", "笔记.md.md")); err == nil {
t.Fatal("text-kind originals should not get a duplicate extract")
}
// 跨轮列出:包含原件 + 解析文本,而不是只有本轮 staging。
got := ListWorkspaceInputs(root, "alice", "conv1", []string{"inputs/会议.docx"})
joined := strings.Join(got, ",")
if !strings.Contains(joined, "inputs/会议.docx") || !strings.Contains(joined, "inputs/会议.md") {
t.Fatalf("listing should include original + extracted: %v", got)
}
// 目录缺失时回落 staging 列表。
fallback := ListWorkspaceInputs(root, "alice", "nope", []string{"inputs/only.txt"})
if len(fallback) != 1 || fallback[0] != "inputs/only.txt" {
t.Fatalf("fallback listing broken: %v", fallback)
}
// root 未配置:全部 no-op。
PersistInputTexts("", "alice", "conv1", docs)
if got := ListWorkspaceInputs("", "alice", "conv1", []string{"inputs/a.txt"}); got[0] != "inputs/a.txt" {
t.Fatalf("unconfigured root should pass through staged: %v", got)
}
}