172 lines
6.4 KiB
Go
172 lines
6.4 KiB
Go
package taskrun_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/ctxmgr"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/stream"
|
|
"chat-agent-system/internal/taskrun"
|
|
)
|
|
|
|
func TestCommands_DuplicateFinalizeMemory(t *testing.T) {
|
|
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.TimeDecayConfig{})
|
|
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-dup", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{"task_status": "running"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmds := &taskrun.Commands{Ctx: ctxMgr, Env: "test", StorageIdentity: "env:test"}
|
|
rec := &taskrun.RunRecord{RunID: "run_dup_mem", SessionID: "sess-dup", TaskKey: "intake", AssistantMsgID: asstID}
|
|
res := &bizdev.TaskRunResult{
|
|
Success: true, TaskKey: "intake", ArtifactTitle: "用户初始需求收集表", MarkdownBody: "# body\n",
|
|
DocRecord: &docgov.DocumentRecord{DocID: "doc_dup", ContentSHA256: strings.Repeat("a", 64), Version: 1},
|
|
}
|
|
st1, err := cmds.Finalize(context.Background(), rec, res)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msg, err := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Content == "任务执行中…" {
|
|
t.Fatal("placeholder not replaced")
|
|
}
|
|
st2, err := cmds.Finalize(context.Background(), rec, res)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st1 != "completed" || st2 != "completed" {
|
|
t.Fatalf("status %s %s", st1, st2)
|
|
}
|
|
msgAgain, _ := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if msgAgain.ID != msg.ID || msgAgain.Content != msg.Content {
|
|
t.Fatal("duplicate finalize mutated the assistant turn")
|
|
}
|
|
}
|
|
|
|
func TestCommands_FinalizeWithEventsPersistsPhaseSnapshot(t *testing.T) {
|
|
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.TimeDecayConfig{})
|
|
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-snap", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{"task_status": "running"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmds := &taskrun.Commands{Ctx: ctxMgr, Env: "test", StorageIdentity: "env:test"}
|
|
rec := &taskrun.RunRecord{RunID: "run_snap_mem", SessionID: "sess-snap", TaskKey: "intake", AssistantMsgID: asstID}
|
|
res := &bizdev.TaskRunResult{
|
|
Success: true, TaskKey: "intake", ArtifactTitle: "用户初始需求收集表", MarkdownBody: "# body\n",
|
|
DocRecord: &docgov.DocumentRecord{DocID: "doc_snap", ContentSHA256: strings.Repeat("b", 64), Version: 1},
|
|
}
|
|
events := []taskrun.StoredEvent{
|
|
{Seq: 1, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "squad_assembly", Message: "组建小队"}}},
|
|
{Seq: 2, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "quality_gate", Message: "门禁", Progress: 95}}},
|
|
}
|
|
if _, err := cmds.FinalizeWithEvents(context.Background(), rec, res, events, 3500); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msg, err := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, ok := msg.Metadata["task_phase_snapshot"]
|
|
if !ok || raw == nil {
|
|
t.Fatal("expected task_phase_snapshot in metadata")
|
|
}
|
|
snap, ok := raw.(*taskrun.PhaseSnapshot)
|
|
if !ok {
|
|
t.Fatalf("unexpected snapshot type %T", raw)
|
|
}
|
|
if snap.Title != "用户初始需求收集表" || snap.DurationMs != 3500 || len(snap.Steps) == 0 {
|
|
t.Fatalf("snapshot %+v", snap)
|
|
}
|
|
if snap.Steps[len(snap.Steps)-1].Status != "completed" {
|
|
t.Fatalf("terminal step should be completed: %+v", snap.Steps)
|
|
}
|
|
}
|
|
|
|
func TestCommands_CancelWinsOverFinalizeMemory(t *testing.T) {
|
|
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.TimeDecayConfig{})
|
|
asstID, _ := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-race", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil)
|
|
cmds := &taskrun.Commands{Ctx: ctxMgr, Env: "test", StorageIdentity: "env:test"}
|
|
rec := &taskrun.RunRecord{RunID: "run_race_mem", SessionID: "sess-race", AssistantMsgID: asstID}
|
|
st, err := cmds.Cancel(context.Background(), rec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st != taskrun.StatusCancelled {
|
|
t.Fatalf("status=%s", st)
|
|
}
|
|
res := &bizdev.TaskRunResult{Success: true, MarkdownBody: "# later\n"}
|
|
st2, err := cmds.Finalize(context.Background(), rec, res)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st2 != taskrun.StatusCancelled {
|
|
t.Fatalf("finalize must adopt cancelled, got %s", st2)
|
|
}
|
|
}
|
|
|
|
func TestCommands_ConcurrentMemorySingleTerminal(t *testing.T) {
|
|
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.TimeDecayConfig{})
|
|
asstID, _ := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-conc", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil)
|
|
cmds := &taskrun.Commands{Ctx: ctxMgr, Env: "test", StorageIdentity: "env:test"}
|
|
rec := &taskrun.RunRecord{RunID: "run_conc_mem", SessionID: "sess-conc", AssistantMsgID: asstID}
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = cmds.Finalize(context.Background(), rec, &bizdev.TaskRunResult{Success: true, MarkdownBody: "# ok\n"})
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = cmds.Cancel(context.Background(), rec)
|
|
}()
|
|
wg.Wait()
|
|
msg, err := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.ID != asstID {
|
|
t.Fatalf("assistant id changed %d", msg.ID)
|
|
}
|
|
}
|
|
|
|
func TestRunManifest_CleanupGate(t *testing.T) {
|
|
man := &taskrun.RunManifest{
|
|
RunID: "run_clean", WorkspaceID: "ws", StorageIdentity: "env:dev",
|
|
Members: []taskrun.ManifestMember{{LogicalID: "doc_a", Digest: "abc", Kind: taskrun.MemberKindDocument}},
|
|
}
|
|
if err := man.Freeze(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := man.AssertCleanupAllowed(nil); err == nil {
|
|
t.Fatal("cleanup without receipts must fail")
|
|
}
|
|
receipts := []taskrun.VerifiedReceipt{{
|
|
LogicalID: "doc_a", Digest: "abc", StorageIdentity: "env:dev", Verified: true,
|
|
}}
|
|
if err := man.AssertCleanupAllowed(receipts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bad := []taskrun.VerifiedReceipt{{
|
|
LogicalID: "doc_a", Digest: "zzz", StorageIdentity: "env:dev", Verified: true,
|
|
}}
|
|
if err := man.AssertCleanupAllowed(bad); err == nil {
|
|
t.Fatal("digest mismatch must fail")
|
|
}
|
|
}
|
|
|
|
func TestCommands_RejectsSensitiveMarkdown(t *testing.T) {
|
|
cmds := &taskrun.Commands{Env: "test", StorageIdentity: "env:test"}
|
|
_, err := cmds.Finalize(context.Background(), &taskrun.RunRecord{RunID: "run_secret"}, &bizdev.TaskRunResult{
|
|
Success: true, MarkdownBody: "api_key=super-secret",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected sensitive reject")
|
|
}
|
|
}
|