Files
tutti/services/tuttid/data/workspace/migrations_workbench_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

295 lines
11 KiB
Go

package workspace
import (
"context"
"encoding/json"
"testing"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
)
func TestSQLiteStoreMigrationUnifiesAgentGUIDockIdentity(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "ws-agent-gui-dock-migration"
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Agent GUI Dock Migration"}); err != nil {
t.Fatalf("Create() error = %v", err)
}
const snapshotJSON = `{
"schemaVersion": 1,
"nodes": [
{"id":"agent-legacy","kind":"agent-gui","data":{"dockEntryId":"agent-gui","instanceId":"agent-gui:codex:panel:1","typeId":"agent-gui"}},
{"id":"agent-provider","kind":"agent-gui","data":{"dockEntryId":"agent-gui:codex","instanceId":"agent-gui:codex:panel:2","typeId":"agent-gui"}},
{"id":"agent-null","kind":"agent-gui","data":{"dockEntryId":null,"instanceId":"agent-gui:claude-code:panel:3","typeId":"agent-gui"}},
{"id":"agent-missing","kind":"agent-gui","data":{"instanceId":"agent-gui:codex:panel:4","typeId":"agent-gui"}},
{"id":"agent-unified","kind":"agent-gui","data":{"dockEntryId":"agent-gui:unified","instanceId":"agent-gui:codex:panel:5","typeId":"agent-gui"}},
{"id":"app","kind":"workspace-app-webview","data":{"dockEntryId":"workspace-app:calendar","instanceId":"app:calendar","typeId":"workspace-app-webview"}}
],
"nodeStack": ["agent-legacy", "app"],
"metadata": {
"workbenchHostClosedDockWindowFrames": {
"version": 1,
"entries": [
{"dockEntryId":"agent-gui","typeId":"agent-gui","frame":{"x":1,"y":1,"width":111,"height":111}},
{"dockEntryId":"workspace-app:calendar","typeId":"workspace-app-webview","frame":{"x":2,"y":2,"width":222,"height":222}},
{"dockEntryId":"agent-gui:unified","typeId":"agent-gui","frame":{"x":3,"y":3,"width":333,"height":333}},
{"dockEntryId":"agent-gui:codex","typeId":"agent-gui","frame":{"x":4,"y":4,"width":444,"height":444}}
]
}
}
}`
if err := store.PutWorkbenchSnapshot(ctx, workspacebiz.WorkbenchSnapshot{
JSON: []byte(snapshotJSON),
SchemaVersion: 1,
WorkspaceID: workspaceID,
}); err != nil {
t.Fatalf("PutWorkbenchSnapshot() error = %v", err)
}
var originalUpdatedAt int64
if err := store.writeDB.QueryRowContext(ctx, `
SELECT updated_at_unix_ms
FROM workspace_workbench_snapshots
WHERE workspace_id = ?
`, workspaceID).Scan(&originalUpdatedAt); err != nil {
t.Fatalf("read original snapshot timestamp: %v", err)
}
if _, err := store.writeDB.ExecContext(ctx, `
DELETE FROM tuttid_schema_migrations
WHERE id = ?
`, schemaMigrationWorkspaceWorkbenchAgentGUIUnifiedDockV1); err != nil {
t.Fatalf("delete migration marker: %v", err)
}
if err := store.Migrate(ctx); err != nil {
t.Fatalf("Migrate() error = %v", err)
}
migrated := readTestWorkbenchSnapshotMigration(t, store, workspaceID)
for _, node := range migrated.Nodes {
if node.TypeID == agentGUIWorkbenchTypeID && node.DockEntryID != agentGUIWorkbenchUnifiedDockEntryID {
t.Fatalf("AgentGUI node %q dockEntryId = %q, want %q", node.ID, node.DockEntryID, agentGUIWorkbenchUnifiedDockEntryID)
}
if node.ID == "app" && node.DockEntryID != "workspace-app:calendar" {
t.Fatalf("workspace app dockEntryId = %q, want workspace-app:calendar", node.DockEntryID)
}
}
if len(migrated.ClosedFrames) != 2 {
t.Fatalf("closed frame entries len = %d, want 2", len(migrated.ClosedFrames))
}
if migrated.ClosedFrames[0].DockEntryID != "workspace-app:calendar" {
t.Fatalf("workspace app closed frame dockEntryId = %q", migrated.ClosedFrames[0].DockEntryID)
}
if migrated.ClosedFrames[1].DockEntryID != agentGUIWorkbenchUnifiedDockEntryID || migrated.ClosedFrames[1].Frame.Width != 444 {
t.Fatalf("AgentGUI closed frame = %+v, want final legacy entry normalized", migrated.ClosedFrames[1])
}
var migratedUpdatedAt int64
if err := store.writeDB.QueryRowContext(ctx, `
SELECT updated_at_unix_ms
FROM workspace_workbench_snapshots
WHERE workspace_id = ?
`, workspaceID).Scan(&migratedUpdatedAt); err != nil {
t.Fatalf("read migrated snapshot timestamp: %v", err)
}
if migratedUpdatedAt != originalUpdatedAt {
t.Fatalf("snapshot updated_at = %d, want unchanged %d", migratedUpdatedAt, originalUpdatedAt)
}
firstJSON := readTestWorkbenchSnapshotJSON(t, store, workspaceID)
if err := store.Migrate(ctx); err != nil {
t.Fatalf("second Migrate() error = %v", err)
}
if secondJSON := readTestWorkbenchSnapshotJSON(t, store, workspaceID); secondJSON != firstJSON {
t.Fatal("second migration changed the normalized snapshot")
}
}
func TestSQLiteStoreAgentGUIDockMigrationRollsBackInvalidSnapshotJSON(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "ws-invalid-agent-gui-dock-migration"
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Invalid Agent GUI Dock Migration"}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if err := store.PutWorkbenchSnapshot(ctx, workspacebiz.WorkbenchSnapshot{
JSON: []byte(`{"schemaVersion":1`),
SchemaVersion: 1,
WorkspaceID: workspaceID,
}); err != nil {
t.Fatalf("PutWorkbenchSnapshot() error = %v", err)
}
if _, err := store.writeDB.ExecContext(ctx, `
DELETE FROM tuttid_schema_migrations
WHERE id = ?
`, schemaMigrationWorkspaceWorkbenchAgentGUIUnifiedDockV1); err != nil {
t.Fatalf("delete migration marker: %v", err)
}
if err := store.Migrate(ctx); err == nil {
t.Fatal("Migrate() error = nil, want invalid snapshot error")
}
applied, err := store.hasMigration(ctx, schemaMigrationWorkspaceWorkbenchAgentGUIUnifiedDockV1)
if err != nil {
t.Fatalf("hasMigration() error = %v", err)
}
if applied {
t.Fatal("migration marker was recorded after rollback")
}
if storedJSON := readTestWorkbenchSnapshotJSON(t, store, workspaceID); storedJSON != `{"schemaVersion":1` {
t.Fatalf("invalid snapshot changed to %q", storedJSON)
}
}
func TestSQLiteStoreMigrationFiltersAgentGUINodesWithoutValidTargetIdentity(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "ws-agent-gui-target-migration"
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Agent GUI Target Migration"}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.writeDB.ExecContext(ctx, `
INSERT OR IGNORE INTO agent_targets (
id, provider, launch_ref_json, name, icon_key, enabled, source,
sort_order, created_at_ms, updated_at_ms
) VALUES (
'local:codex', 'codex', '{"type":"local_cli","provider":"codex"}',
'Codex', 'codex', 1, 'system', 10, 1, 1
)
`); err != nil {
t.Fatalf("insert target fixture: %v", err)
}
const snapshotJSON = `{
"schemaVersion": 1,
"nodes": [
{"id":"agent-valid","kind":"agent-gui","data":{"typeId":"agent-gui","snapshotNodeState":{"agentTargetId":"local:codex"}}},
{"id":"agent-missing","kind":"agent-gui","data":{"typeId":"agent-gui","snapshotNodeState":{}}},
{"id":"agent-unknown","kind":"agent-gui","data":{"typeId":"agent-gui","snapshotNodeState":{"agentTargetId":"missing-target"}}},
{"id":"app","kind":"workspace-app-webview","data":{"typeId":"workspace-app-webview"}}
],
"nodeStack": ["agent-valid", "agent-missing", "agent-unknown", "app"],
"activeNodeId": "agent-unknown",
"spaces": [{"id":"space-1","name":"One","nodeIds":["agent-valid","agent-missing","agent-unknown","app"]}]
}`
if err := store.PutWorkbenchSnapshot(ctx, workspacebiz.WorkbenchSnapshot{
JSON: []byte(snapshotJSON), SchemaVersion: 1, WorkspaceID: workspaceID,
}); err != nil {
t.Fatalf("PutWorkbenchSnapshot() error = %v", err)
}
if _, err := store.writeDB.ExecContext(ctx, `
DELETE FROM tuttid_schema_migrations WHERE id = ?
`, schemaMigrationWorkspaceWorkbenchAgentTargetIdentityV1); err != nil {
t.Fatalf("delete migration marker: %v", err)
}
if err := store.Migrate(ctx); err != nil {
t.Fatalf("Migrate() error = %v", err)
}
var migrated struct {
Nodes []struct {
ID string `json:"id"`
} `json:"nodes"`
NodeStack []string `json:"nodeStack"`
ActiveNodeID *string `json:"activeNodeId"`
Spaces []struct {
NodeIDs []string `json:"nodeIds"`
} `json:"spaces"`
}
if err := json.Unmarshal([]byte(readTestWorkbenchSnapshotJSON(t, store, workspaceID)), &migrated); err != nil {
t.Fatalf("decode migrated snapshot: %v", err)
}
if len(migrated.Nodes) != 2 || migrated.Nodes[0].ID != "agent-valid" || migrated.Nodes[1].ID != "app" {
t.Fatalf("nodes = %+v, want valid AgentGUI node and app", migrated.Nodes)
}
if got := migrated.NodeStack; len(got) != 2 || got[0] != "agent-valid" || got[1] != "app" {
t.Fatalf("nodeStack = %v, want [agent-valid app]", got)
}
if migrated.ActiveNodeID != nil {
t.Fatalf("activeNodeId = %v, want null", migrated.ActiveNodeID)
}
if len(migrated.Spaces) != 1 || len(migrated.Spaces[0].NodeIDs) != 2 {
t.Fatalf("spaces = %+v, want only retained node ids", migrated.Spaces)
}
}
type testWorkbenchSnapshotMigration struct {
Nodes []testWorkbenchSnapshotMigrationNode
ClosedFrames []testWorkbenchSnapshotMigrationClosedFrame
}
type testWorkbenchSnapshotMigrationNode struct {
ID string
TypeID string
DockEntryID string
}
type testWorkbenchSnapshotMigrationClosedFrame struct {
DockEntryID string
Frame struct {
Width float64 `json:"width"`
}
TypeID string
}
type testWorkbenchSnapshotMigrationRawClosedFrame struct {
DockEntryID string `json:"dockEntryId"`
Frame struct {
Width float64 `json:"width"`
} `json:"frame"`
TypeID string `json:"typeId"`
}
func readTestWorkbenchSnapshotMigration(t *testing.T, store *SQLiteStore, workspaceID string) testWorkbenchSnapshotMigration {
t.Helper()
storedJSON := readTestWorkbenchSnapshotJSON(t, store, workspaceID)
var raw struct {
Nodes []struct {
ID string `json:"id"`
Data struct {
DockEntryID string `json:"dockEntryId"`
TypeID string `json:"typeId"`
} `json:"data"`
} `json:"nodes"`
Metadata struct {
ClosedFrames struct {
Entries []testWorkbenchSnapshotMigrationRawClosedFrame `json:"entries"`
} `json:"workbenchHostClosedDockWindowFrames"`
} `json:"metadata"`
}
if err := json.Unmarshal([]byte(storedJSON), &raw); err != nil {
t.Fatalf("decode migrated snapshot: %v", err)
}
result := testWorkbenchSnapshotMigration{}
for _, entry := range raw.Metadata.ClosedFrames.Entries {
result.ClosedFrames = append(result.ClosedFrames, testWorkbenchSnapshotMigrationClosedFrame(entry))
}
for _, node := range raw.Nodes {
result.Nodes = append(result.Nodes, testWorkbenchSnapshotMigrationNode{
ID: node.ID, TypeID: node.Data.TypeID, DockEntryID: node.Data.DockEntryID,
})
}
return result
}
func readTestWorkbenchSnapshotJSON(t *testing.T, store *SQLiteStore, workspaceID string) string {
t.Helper()
var snapshotJSON string
if err := store.writeDB.QueryRowContext(context.Background(), `
SELECT snapshot_json
FROM workspace_workbench_snapshots
WHERE workspace_id = ?
`, workspaceID).Scan(&snapshotJSON); err != nil {
t.Fatalf("read workbench snapshot json: %v", err)
}
return snapshotJSON
}