Files
tutti/services/tuttid/data/workspace/agent_session_fixture_test.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

307 lines
8.9 KiB
Go

package workspace
import (
"bytes"
"context"
"encoding/json"
"errors"
"testing"
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
agenthost "github.com/tutti-os/tutti/packages/agent/host"
agentactivitybiz "github.com/tutti-os/tutti/packages/agent/store-sqlite"
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
)
func TestCaptureReplayStateExcludesUnrelatedSentinelSession(t *testing.T) {
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "workspace-fixture"
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Fixture"}); err != nil {
t.Fatal(err)
}
for _, report := range []agentactivitybiz.SessionStateReport{
{
WorkspaceID: workspaceID,
AgentSessionID: "root-session",
Kind: "root",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
AgentTargetID: "local:codex",
Provider: "codex",
ProviderSessionID: "provider-root",
Status: "ready",
Title: workspaceID,
RuntimeContext: map[string]any{
canonical.ProviderResumeCheckpointRuntimeContextKey: map[string]any{
"defaultModel": "gpt-5.5",
"defaultModeMask": map[string]any{
"mode": "default",
},
},
},
OccurredAtUnixMS: 100,
},
{
WorkspaceID: workspaceID,
AgentSessionID: "child-session",
Kind: "child",
RootAgentSessionID: "root-session",
RootTurnID: "root-turn",
ParentAgentSessionID: "root-session",
ParentTurnID: "root-turn",
ParentToolCallID: "tool-call",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
AgentTargetID: "local:codex",
Provider: "codex",
ProviderSessionID: "provider-child",
Status: "ready",
OccurredAtUnixMS: 110,
},
{
WorkspaceID: workspaceID,
AgentSessionID: "unrelated-sentinel-session",
Kind: "root",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
AgentTargetID: "local:codex",
Provider: "codex",
ProviderSessionID: "provider-unrelated",
Status: "ready",
Title: "UNRELATED_SENTINEL_MUST_NOT_EXPORT",
OccurredAtUnixMS: 120,
},
} {
if report.AgentSessionID == "child-session" {
seedTestAgentTurn(
t,
store,
ctx,
workspaceID,
"root-session",
"root-turn",
"codex",
105,
)
}
if _, err := store.ReportSessionState(ctx, report); err != nil {
t.Fatal(err)
}
}
if _, accepted, err := store.agentStore().RecordTurnTransition(
ctx,
agentactivitybiz.TurnTransition{
WorkspaceID: workspaceID, AgentSessionID: "root-session",
TurnID: "root-turn", Phase: agentactivitybiz.TurnPhaseSettled,
Outcome: agentactivitybiz.TurnOutcomeCompleted,
Origin: agentactivitybiz.TurnOriginLegacyUnknown,
OccurredAtUnixMS: 130,
},
); err != nil || !accepted {
t.Fatalf("settle root Turn accepted=%v error=%v", accepted, err)
}
rootID, err := store.ResolveRootAgentSession(ctx, workspaceID, "child-session")
if err != nil {
t.Fatal(err)
}
if rootID != "root-session" {
t.Fatalf("root id = %q", rootID)
}
raw, err := store.CaptureReplayState(ctx, workspaceID, rootID)
if err != nil {
t.Fatal(err)
}
var state TuttiReplayState
if err := json.Unmarshal(raw, &state); err != nil {
t.Fatal(err)
}
if state.SchemaVersion != tuttiReplayStateSchemaVersion {
t.Fatalf("semantic state schema version = %d", state.SchemaVersion)
}
var sessionIDs []string
foundSourceIDInUserData := false
for _, session := range state.Agent.Sessions {
sessionIDs = append(sessionIDs, session.ID)
if session.Title == workspaceID {
foundSourceIDInUserData = true
}
}
if len(sessionIDs) != 2 || sessionIDs[0] != "root-session" || sessionIDs[1] != "child-session" {
t.Fatalf("exported sessions = %#v", sessionIDs)
}
if !foundSourceIDInUserData {
t.Fatal("fixture export rewrote user data equal to the source Workspace id")
}
if checkpoint := state.Agent.Sessions[0].ProviderResumeCheckpoint; checkpoint == nil ||
checkpoint["defaultModel"] != "gpt-5.5" {
t.Fatalf("provider resume checkpoint = %#v", checkpoint)
}
if string(raw) == "" ||
jsonContainsKey(t, raw, "workspaceId") ||
jsonContainsKey(t, raw, "table") ||
jsonContainsKey(t, raw, "values") {
t.Fatalf("semantic state leaks persistence vocabulary: %s", raw)
}
const replayWorkspaceID = "workspace-fixture-replay"
if err := store.Create(ctx, workspacebiz.Summary{
ID: replayWorkspaceID, Name: "Replay",
}); err != nil {
t.Fatal(err)
}
if err := store.AgentCanonicalStore().RestoreHistoricalSessionGraph(
ctx,
agenthost.HistoricalSessionGraphRestoreInput{
WorkspaceID: replayWorkspaceID,
UserID: "user-replay",
Graph: state.Agent,
},
); err != nil {
t.Fatal(err)
}
restored, err := store.AgentCanonicalStore().CaptureHistoricalSessionGraph(
ctx,
replayWorkspaceID,
state.Agent.RootSessionID,
)
if err != nil {
t.Fatal(err)
}
restoredRaw, err := json.Marshal(restored)
if err != nil {
t.Fatal(err)
}
expectedRaw, err := json.Marshal(state.Agent)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(restoredRaw, expectedRaw) {
t.Fatalf(
"restored graph differs:\nrestored=%s\nexpected=%s",
restoredRaw,
expectedRaw,
)
}
restoredSession, ok, err := store.AgentCanonicalStore().GetSession(
ctx,
replayWorkspaceID,
"root-session",
)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("restored root Session is missing")
}
if checkpoint := restoredSession.InternalRuntimeContext[canonical.ProviderResumeCheckpointRuntimeContextKey]; !bytes.Equal(mustJSON(t, checkpoint), mustJSON(
t,
state.Agent.Sessions[0].ProviderResumeCheckpoint,
)) {
t.Fatalf("restored provider resume checkpoint = %#v", checkpoint)
}
if err := store.AgentCanonicalStore().RestoreHistoricalSessionGraph(
ctx,
agenthost.HistoricalSessionGraphRestoreInput{
WorkspaceID: replayWorkspaceID,
UserID: "user-replay",
Graph: state.Agent,
},
); err != nil {
t.Fatalf("idempotent restore error = %v", err)
}
conflicting := state.Agent
conflicting.Sessions = append([]agenthost.HistoricalSession(nil), state.Agent.Sessions...)
conflicting.Sessions[0].Title = "conflicting title"
if err := store.AgentCanonicalStore().RestoreHistoricalSessionGraph(
ctx,
agenthost.HistoricalSessionGraphRestoreInput{
WorkspaceID: replayWorkspaceID,
UserID: "user-replay",
Graph: conflicting,
},
); !errors.Is(err, agenthost.ErrHistoricalStateConflict) {
t.Fatalf("conflicting restore error = %v", err)
}
}
func mustJSON(t *testing.T, value any) []byte {
t.Helper()
raw, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
return raw
}
func TestCaptureReplayStateIgnoresUnrelatedSQLiteColumns(t *testing.T) {
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "workspace-portability"
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Portable"}); err != nil {
t.Fatal(err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: workspaceID, AgentSessionID: "session-1", Kind: "root",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
AgentTargetID: "local:codex", Provider: "codex",
ProviderSessionID: "provider-session-1",
Status: "ready", OccurredAtUnixMS: 100,
}); err != nil {
t.Fatal(err)
}
if _, err := store.writeDB.ExecContext(ctx, `
UPDATE workspace_agent_session_goals
SET desired_json = NULL, observed_json = NULL, last_evidence_json = NULL
WHERE workspace_id = ? AND agent_session_id = ?
`, workspaceID, "session-1"); err != nil {
t.Fatal(err)
}
beforeRaw, err := store.CaptureReplayState(ctx, workspaceID, "session-1")
if err != nil {
t.Fatal(err)
}
if _, err := store.writeDB.ExecContext(ctx, `
ALTER TABLE tutti_mode_turn_snapshots
ADD COLUMN unrelated_future_column TEXT NOT NULL DEFAULT 'speed-mismatch-regression'
`); err != nil {
t.Fatal(err)
}
afterRaw, err := store.CaptureReplayState(ctx, workspaceID, "session-1")
if err != nil {
t.Fatal(err)
}
if string(beforeRaw) != string(afterRaw) {
t.Fatalf("unrelated SQLite column changed semantic output\nbefore=%s\nafter=%s", beforeRaw, afterRaw)
}
}
func jsonContainsKey(t *testing.T, raw []byte, key string) bool {
t.Helper()
var visit func(any) bool
visit = func(value any) bool {
switch value := value.(type) {
case map[string]any:
if _, ok := value[key]; ok {
return true
}
for _, child := range value {
if visit(child) {
return true
}
}
case []any:
for _, child := range value {
if visit(child) {
return true
}
}
}
return false
}
var value any
if err := json.Unmarshal(raw, &value); err != nil {
t.Fatal(err)
}
return visit(value)
}