Files
Agent-System-Backend/internal/server/rewind_http_test.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:47:16 +00:00

155 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server
import (
"net/http"
"os"
"path/filepath"
"testing"
"time"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/bus"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/cmdbus"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/plugins"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/profiles"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/runmgr"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/runtime"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/settings"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/store"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/tools"
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
)
// ---- N3 WP3:rewind HTTP 层(happy + busy 409)----
// happy:经 API 回滚被追踪写入,restored/skipped 上抛,文件内容退回。
func TestRewindFilesHTTPHappy(t *testing.T) {
dir := t.TempDir()
st, err := store.OpenSQLite(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
_ = st.MergeSettingsScope(store.OwnerDefault, settings.ScopeUser, map[string]any{"runtime": map[string]any{"mock": true}})
reg := tools.NewRegistry()
reg.RegisterBuiltin()
setSvc := settings.NewService(st)
profSvc := profiles.New(st)
profSvc.EnsureDefault()
mgr := runmgr.New(st, bus.New(), reg, profSvc, setSvc, &runtime.MockRunner{}, &runtime.MockSubagent{}, &runtime.MockRunner{}, &runtime.MockSubagent{})
ws := t.TempDir()
mgr.SetWorkspaceRoot(ws)
ph := plugins.New(st, reg, setSvc, filepath.Join(dir, "plugins"))
srv := New(st, mgr, reg, profSvc, setSvc, ph)
srv.Bus = cmdbus.New(st, nil, nil)
h := srv.Handler()
_, convID := setupConversation(t, h)
runID := postMessage(t, h, convID, "写个文件")
// 等 run 终态(mock run 是异步的;不回等会撞到会话占用 409)。
deadline := time.Now().Add(2 * time.Second)
for {
run, err := st.GetRunByID(runID)
if err == nil && (run.Status == core.RunCompleted || run.Status == core.RunFailed || run.Status == core.RunCancelled) {
break
}
if time.Now().After(deadline) {
t.Fatalf("run 未终态:%+v", run)
}
time.Sleep(10 * time.Millisecond)
}
// 找到用户消息(anchor)并造一条被追踪写入。
var userMsgID string
msgs, err := st.ListMessages(convID)
if err != nil {
t.Fatal(err)
}
for _, m := range msgs {
if m.Role == core.RoleUser {
userMsgID = m.ID
}
}
if userMsgID == "" {
t.Fatal("no user message")
}
sandbox := filepath.Join(ws, store.OwnerDefault, convID)
if err := os.MkdirAll(sandbox, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sandbox, "tracked.txt"), []byte("写入后"), 0o644); err != nil {
t.Fatal(err)
}
cpLog, err := mgr.OpenCheckpoints(store.OwnerDefault, convID)
if err != nil {
t.Fatal(err)
}
if err := cpLog.Record(userMsgID, sandbox, "tracked.txt", []byte("写入前"), true, []byte("写入后")); err != nil {
t.Fatal(err)
}
// run 行已终态与释放会话占用之间有个 settle 窗口(CI 慢机上会撞 409):
// 有界重试直到放行。
var code int
var out map[string]any
deadline2 := time.Now().Add(3 * time.Second)
for {
code, out = doJSON(t, h, "POST", "/api/conversations/"+convID+"/rewind", map[string]any{"messageId": userMsgID})
if code != http.StatusConflict {
break
}
if time.Now().After(deadline2) {
break
}
time.Sleep(15 * time.Millisecond)
}
if code != http.StatusOK {
t.Fatalf("rewind status %d: %v", code, out)
}
restored, _ := out["restored"].([]any)
if len(restored) != 1 || restored[0] != "tracked.txt" {
t.Fatalf("restored: %v", out)
}
body, err := os.ReadFile(filepath.Join(sandbox, "tracked.txt"))
if err != nil || string(body) != "写入前" {
t.Fatalf("文件未退回:%v %q", err, body)
}
}
// busy:会话被占时回滚返回 409。
func TestRewindFilesHTTPBusy409(t *testing.T) {
dir := t.TempDir()
st, err := store.OpenSQLite(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
_ = st.MergeSettingsScope(store.OwnerDefault, settings.ScopeUser, map[string]any{"runtime": map[string]any{"mock": false}})
reg := tools.NewRegistry()
reg.RegisterBuiltin()
setSvc := settings.NewService(st)
profSvc := profiles.New(st)
profSvc.EnsureDefault()
release := make(chan struct{})
blocking := blockingRunner{block: make(chan struct{}, 1), release: release}
defer close(release)
mgr := runmgr.New(st, bus.New(), reg, profSvc, setSvc, blocking, blocking, &runtime.MockRunner{}, &runtime.MockSubagent{})
t.Cleanup(mgr.Shutdown)
ph := plugins.New(st, reg, setSvc, filepath.Join(dir, "plugins"))
srv := New(st, mgr, reg, profSvc, setSvc, ph)
srv.Bus = cmdbus.New(st, nil, nil)
h := srv.Handler()
_, convID := setupConversation(t, h)
postMessage(t, h, convID, "先占住")
var userMsgID string
msgs, _ := st.ListMessages(convID)
for _, m := range msgs {
if m.Role == core.RoleUser {
userMsgID = m.ID
}
}
code, _ := doJSON(t, h, "POST", "/api/conversations/"+convID+"/rewind", map[string]any{"messageId": userMsgID})
if code != http.StatusConflict {
t.Fatalf("busy rewind 应 409,得到 %d", code)
}
}