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
728 lines
27 KiB
Go
728 lines
27 KiB
Go
package storesqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func mkdirAll(path string) error {
|
|
return os.MkdirAll(path, 0o755)
|
|
}
|
|
|
|
func TestAgentTargetsV5ReplacesSidebarIconWithoutCompatibility(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
db := openTestDB(t)
|
|
store := New(db, Options{})
|
|
if _, err := db.ExecContext(ctx, `
|
|
CREATE TABLE agent_store_schema_migrations (
|
|
id TEXT PRIMARY KEY,
|
|
applied_at_unix_ms INTEGER NOT NULL
|
|
);
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, migrate := range []func(context.Context) error{
|
|
store.applyAgentTargetsV1,
|
|
store.applyAgentTargetsV2,
|
|
store.applyAgentTargetsV3,
|
|
store.applyAgentTargetsV4,
|
|
} {
|
|
if err := migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := db.ExecContext(ctx, `
|
|
INSERT INTO agent_targets (
|
|
id, provider, launch_ref_json, name, icon_url, sidebar_icon_url,
|
|
enabled, source, sort_order, created_at_ms, updated_at_ms
|
|
) VALUES (
|
|
'extension:example', 'acp:example', '{"type":"agent_extension","extensionInstallationId":"example@1.0.0"}',
|
|
'Example', 'data:image/svg+xml;base64,old-mask', 'data:image/svg+xml;base64,old-primary',
|
|
1, 'system', 700, 1, 1
|
|
);
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := store.applyAgentTargetsV5(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.GetAgentTarget(ctx, "extension:example"); !errors.Is(err, ErrAgentTargetNotFound) {
|
|
t.Fatalf("old extension target error = %v, want not found", err)
|
|
}
|
|
rows, err := db.QueryContext(ctx, `PRAGMA table_info(agent_targets)`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
columns := map[string]bool{}
|
|
for rows.Next() {
|
|
var cid, notNull, primaryKey int
|
|
var name, columnType string
|
|
var defaultValue any
|
|
if err := rows.Scan(&cid, &name, &columnType, ¬Null, &defaultValue, &primaryKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
columns[name] = true
|
|
}
|
|
if !columns["mask_icon_url"] || columns["sidebar_icon_url"] {
|
|
t.Fatalf("agent target columns = %#v", columns)
|
|
}
|
|
}
|
|
|
|
// createLegacyTuttidDatabase replays the schema a fully migrated tuttid
|
|
// database had before the store was extracted: shared tuttid ledger, host
|
|
// workspaces table, agent tables with the workspaces foreign key, and
|
|
// applied records for every agent migration.
|
|
func createLegacyTuttidDatabase(t *testing.T, db *sql.DB) {
|
|
t.Helper()
|
|
if _, err := db.Exec(`
|
|
CREATE TABLE tuttid_schema_migrations (
|
|
id TEXT PRIMARY KEY,
|
|
applied_at_unix_ms INTEGER NOT NULL
|
|
);
|
|
INSERT INTO tuttid_schema_migrations (id, applied_at_unix_ms) VALUES
|
|
('workspaces_v1', 11),
|
|
('workspaces_v2', 12),
|
|
('workspace_agent_activity_v1', 21),
|
|
('workspace_agent_activity_v2', 22),
|
|
('workspace_agent_activity_v3', 23),
|
|
('workspace_agent_activity_v4', 24),
|
|
('workspace_agent_activity_v5', 25),
|
|
('agent_targets_v1', 26),
|
|
('workspace_agent_activity_rail_v1', 27);
|
|
|
|
CREATE TABLE workspaces (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL
|
|
);
|
|
INSERT INTO workspaces (id, name, created_at_unix_ms, updated_at_unix_ms)
|
|
VALUES ('ws-legacy', 'Legacy Workspace', 1, 1);
|
|
|
|
CREATE TABLE workspace_agent_sessions (
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
origin TEXT NOT NULL DEFAULT '',
|
|
agent_target_id TEXT,
|
|
provider TEXT NOT NULL DEFAULT '',
|
|
provider_session_id TEXT NOT NULL DEFAULT '',
|
|
model TEXT NOT NULL DEFAULT '',
|
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
|
runtime_context_json TEXT NOT NULL DEFAULT '{}',
|
|
cwd TEXT NOT NULL DEFAULT '',
|
|
rail_section_kind TEXT NOT NULL DEFAULT 'conversations',
|
|
rail_project_path TEXT NOT NULL DEFAULT '',
|
|
rail_section_key TEXT NOT NULL DEFAULT 'conversations',
|
|
title TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
current_phase TEXT NOT NULL DEFAULT '',
|
|
last_error TEXT NOT NULL DEFAULT '',
|
|
message_version INTEGER NOT NULL DEFAULT 0,
|
|
last_event_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
ended_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
pinned_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
PRIMARY KEY (workspace_id, agent_session_id),
|
|
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
|
|
);
|
|
INSERT INTO workspace_agent_sessions (
|
|
workspace_id, agent_session_id, origin, agent_target_id, provider, title, status, runtime_context_json,
|
|
created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES
|
|
('ws-legacy', 'session-with-target', 'runtime', 'local:codex', 'codex', 'Old Session', 'completed',
|
|
'{"visible":false,"imported":true,"capabilities":["planMode","planMode","unknown","modelImageInputRequired"],"goal":{"objective":"ship","status":"usageLimited"},"providerConfig":{"threadId":"thread-1"}}', 1, 2),
|
|
('ws-legacy', 'session-untargeted', 'runtime', '', 'codex', 'Untargeted Session', 'completed', '{}', 1, 1);
|
|
|
|
CREATE TABLE workspace_agent_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
message_id TEXT NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
turn_id TEXT NOT NULL DEFAULT '',
|
|
role TEXT NOT NULL DEFAULT '',
|
|
kind TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
occurred_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
completed_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE (workspace_id, agent_session_id, message_id),
|
|
FOREIGN KEY (workspace_id, agent_session_id)
|
|
REFERENCES workspace_agent_sessions(workspace_id, agent_session_id)
|
|
ON DELETE CASCADE
|
|
);
|
|
INSERT INTO workspace_agent_messages (
|
|
workspace_id, agent_session_id, message_id, version, role, kind, status,
|
|
payload_json, created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES ('ws-legacy', 'session-with-target', 'message-1', 1, 'assistant', 'text', 'completed', '{"text":"legacy"}', 1, 1);
|
|
|
|
CREATE TABLE agent_targets (
|
|
id TEXT PRIMARY KEY,
|
|
provider TEXT NOT NULL,
|
|
launch_ref_json TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
icon_key TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
source TEXT NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at_ms INTEGER NOT NULL,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
INSERT 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("create legacy tuttid database: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreMigrateClaimsLegacyTuttidMigrationsWithoutReplay(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := openTestDB(t)
|
|
createLegacyTuttidDatabase(t, db)
|
|
store := New(db, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
var metadataJSON, internalJSON string
|
|
if err := db.QueryRowContext(ctx, `
|
|
SELECT session_metadata_json, internal_runtime_context_json
|
|
FROM workspace_agent_sessions
|
|
WHERE workspace_id = 'ws-legacy' AND agent_session_id = 'session-with-target'
|
|
`).Scan(&metadataJSON, &internalJSON); err != nil {
|
|
t.Fatalf("read migrated session metadata: %v", err)
|
|
}
|
|
metadata, err := unmarshalJSONMap(metadataJSON)
|
|
if err != nil || metadata["visible"] != false || metadata["imported"] != true {
|
|
t.Fatalf("metadata=%#v err=%v", metadata, err)
|
|
}
|
|
capabilities, _ := metadata["capabilities"].([]any)
|
|
goal, _ := metadata["goal"].(map[string]any)
|
|
if len(capabilities) != 2 || goal["status"] != "usageLimited" {
|
|
t.Fatalf("normalized metadata=%#v", metadata)
|
|
}
|
|
internal, err := unmarshalJSONMap(internalJSON)
|
|
providerConfig, _ := internal["providerConfig"].(map[string]any)
|
|
if err != nil || providerConfig["threadId"] != "thread-1" || internal["visible"] != nil ||
|
|
internal["goal"] != nil {
|
|
t.Fatalf("internal context=%#v err=%v", internal, err)
|
|
}
|
|
for _, removedColumn := range []string{"status", "current_phase", "last_error", "runtime_context_json"} {
|
|
hasColumn, err := store.hasColumn(ctx, "workspace_agent_sessions", removedColumn)
|
|
if err != nil || hasColumn {
|
|
t.Fatalf("removed column %s present=%v err=%v", removedColumn, hasColumn, err)
|
|
}
|
|
}
|
|
|
|
// Every legacy record is claimed into the package ledger with its
|
|
// original timestamp, plus the claim marker itself.
|
|
for migrationID, wantAppliedAt := range map[string]int64{
|
|
"workspace_agent_activity_v1": 21,
|
|
"workspace_agent_activity_v2": 22,
|
|
"workspace_agent_activity_v3": 23,
|
|
"workspace_agent_activity_v4": 24,
|
|
"workspace_agent_activity_v5": 25,
|
|
"agent_targets_v1": 26,
|
|
"workspace_agent_activity_rail_v1": 27,
|
|
} {
|
|
var appliedAt int64
|
|
if err := db.QueryRowContext(ctx, `
|
|
SELECT applied_at_unix_ms FROM agent_store_schema_migrations WHERE id = ?
|
|
`, migrationID).Scan(&appliedAt); err != nil {
|
|
t.Fatalf("claimed migration %s missing: %v", migrationID, err)
|
|
}
|
|
if appliedAt != wantAppliedAt {
|
|
t.Fatalf("claimed migration %s applied_at = %d, want %d (copied from legacy ledger)", migrationID, appliedAt, wantAppliedAt)
|
|
}
|
|
}
|
|
claimed, err := store.hasMigration(ctx, schemaMigrationLegacyClaimV1)
|
|
if err != nil || !claimed {
|
|
t.Fatalf("claim marker present = %v error = %v, want recorded", claimed, err)
|
|
}
|
|
|
|
// Host-only ledger entries are not claimed; the legacy ledger is left
|
|
// untouched.
|
|
if got, err := store.hasMigration(ctx, "workspaces_v1"); err != nil || got {
|
|
t.Fatalf("workspaces_v1 claimed = %v error = %v, want host record left out", got, err)
|
|
}
|
|
var legacyCount int
|
|
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM tuttid_schema_migrations`).Scan(&legacyCount); err != nil {
|
|
t.Fatalf("count legacy ledger: %v", err)
|
|
}
|
|
if legacyCount != 9 {
|
|
t.Fatalf("legacy ledger rows = %d, want 9 untouched rows", legacyCount)
|
|
}
|
|
|
|
// The v5 backfill was claimed, not replayed: the untargeted codex
|
|
// session keeps its empty agent_target_id.
|
|
var untargeted sql.NullString
|
|
if err := db.QueryRowContext(ctx, `
|
|
SELECT agent_target_id FROM workspace_agent_sessions
|
|
WHERE workspace_id = 'ws-legacy' AND agent_session_id = 'session-untargeted'
|
|
`).Scan(&untargeted); err != nil {
|
|
t.Fatalf("read untargeted session: %v", err)
|
|
}
|
|
if untargeted.String != "" {
|
|
t.Fatalf("untargeted session agent_target_id = %q, want empty (v5 must not replay)", untargeted.String)
|
|
}
|
|
|
|
// Legacy rows stay readable through the store.
|
|
sessions, ok, err := store.ListSessions(ctx, "ws-legacy")
|
|
if err != nil || !ok {
|
|
t.Fatalf("ListSessions() ok=%v error=%v", ok, err)
|
|
}
|
|
if len(sessions) != 2 {
|
|
t.Fatalf("ListSessions() len = %d, want 2 legacy sessions", len(sessions))
|
|
}
|
|
page, ok, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{
|
|
WorkspaceID: "ws-legacy",
|
|
AgentSessionID: "session-with-target",
|
|
Limit: 10,
|
|
})
|
|
if err != nil || !ok {
|
|
t.Fatalf("ListSessionMessages() ok=%v error=%v", ok, err)
|
|
}
|
|
if len(page.Messages) != 1 || page.Messages[0].Payload["text"] != "legacy" {
|
|
t.Fatalf("legacy message page = %#v", page)
|
|
}
|
|
|
|
// The final entity migration deliberately normalizes legacy sessions onto
|
|
// the package-owned schema: host workspaces are not an agent-store parent,
|
|
// while active_turn_id is an exact nullable turn reference.
|
|
rows, err := db.QueryContext(ctx, `PRAGMA foreign_key_list(workspace_agent_sessions)`)
|
|
if err != nil {
|
|
t.Fatalf("foreign_key_list error = %v", err)
|
|
}
|
|
defer rows.Close()
|
|
hasWorkspacesFK := false
|
|
hasTurnsFK := false
|
|
for rows.Next() {
|
|
var (
|
|
id, seq int
|
|
table, from, to, onUpdate, onDelete, match string
|
|
)
|
|
if err := rows.Scan(&id, &seq, &table, &from, &to, &onUpdate, &onDelete, &match); err != nil {
|
|
t.Fatalf("scan foreign_key_list: %v", err)
|
|
}
|
|
if table == "workspaces" {
|
|
hasWorkspacesFK = true
|
|
}
|
|
if table == "workspace_agent_turns" {
|
|
hasTurnsFK = true
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatalf("iterate foreign_key_list: %v", err)
|
|
}
|
|
if hasWorkspacesFK || !hasTurnsFK {
|
|
t.Fatalf("legacy session foreign keys workspaces=%v turns=%v, want package-owned turn reference only", hasWorkspacesFK, hasTurnsFK)
|
|
}
|
|
|
|
// A second Migrate is a no-op for claimed and applied migrations.
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() second run error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionMetadataMigrationRollsBackDDLAndBackfillAtomically(t *testing.T) {
|
|
db := openTestDB(t)
|
|
createLegacyTuttidDatabase(t, db)
|
|
store := New(db, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
if _, err := db.Exec(`
|
|
CREATE TABLE agent_store_schema_migrations (id TEXT PRIMARY KEY, applied_at_unix_ms INTEGER NOT NULL);
|
|
CREATE TRIGGER fail_session_metadata_backfill BEFORE UPDATE ON workspace_agent_sessions
|
|
BEGIN SELECT RAISE(ABORT, 'injected metadata backfill failure'); END;
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.applyWorkspaceAgentSessionMetadataV1(ctx); err == nil {
|
|
t.Fatal("metadata migration error=nil")
|
|
}
|
|
for _, column := range []string{"session_metadata_json", "internal_runtime_context_json"} {
|
|
present, err := store.hasColumn(ctx, "workspace_agent_sessions", column)
|
|
if err != nil || present {
|
|
t.Fatalf("rolled back column %s present=%v err=%v", column, present, err)
|
|
}
|
|
}
|
|
if _, err := db.Exec(`DROP TRIGGER fail_session_metadata_backfill`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.applyWorkspaceAgentSessionMetadataV1(ctx); err != nil {
|
|
t.Fatalf("metadata migration retry: %v", err)
|
|
}
|
|
for _, column := range []string{"session_metadata_json", "internal_runtime_context_json"} {
|
|
present, err := store.hasColumn(ctx, "workspace_agent_sessions", column)
|
|
if err != nil || !present {
|
|
t.Fatalf("retried column %s present=%v err=%v", column, present, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionMetadataDropMigrationRollsBackAllColumnsAtomically(t *testing.T) {
|
|
db := openTestDB(t)
|
|
createLegacyTuttidDatabase(t, db)
|
|
store := New(db, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
if _, err := db.Exec(`CREATE TABLE agent_store_schema_migrations (id TEXT PRIMARY KEY, applied_at_unix_ms INTEGER NOT NULL)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.applyWorkspaceAgentSessionMetadataV1(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec(`CREATE INDEX block_runtime_context_drop ON workspace_agent_sessions(runtime_context_json)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.applyWorkspaceAgentSessionMetadataV2(ctx); err == nil {
|
|
t.Fatal("drop migration error=nil")
|
|
}
|
|
for _, column := range []string{"status", "current_phase", "last_error", "runtime_context_json"} {
|
|
present, err := store.hasColumn(ctx, "workspace_agent_sessions", column)
|
|
if err != nil || !present {
|
|
t.Fatalf("rolled back column %s present=%v err=%v", column, present, err)
|
|
}
|
|
}
|
|
if _, err := db.Exec(`DROP INDEX block_runtime_context_drop`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.applyWorkspaceAgentSessionMetadataV2(ctx); err != nil {
|
|
t.Fatalf("drop migration retry: %v", err)
|
|
}
|
|
for _, column := range []string{"status", "current_phase", "last_error", "runtime_context_json"} {
|
|
present, err := store.hasColumn(ctx, "workspace_agent_sessions", column)
|
|
if err != nil || present {
|
|
t.Fatalf("dropped column %s present=%v err=%v", column, present, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A database that already recorded (claimed) the one-time v5 backfill — which
|
|
// ran before the cursor provider existed — must still backfill legacy cursor
|
|
// sessions: v7 replays the target-id backfill for providers added to
|
|
// TargetIDBackfillByProvider after v5, and only for those.
|
|
func TestStoreMigrateBackfillsCursorTargetsOnClaimedLegacyDatabase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := openTestDB(t)
|
|
createLegacyTuttidDatabase(t, db)
|
|
if _, err := db.Exec(`
|
|
INSERT INTO workspace_agent_sessions (
|
|
workspace_id, agent_session_id, origin, agent_target_id, provider, title, status,
|
|
created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES ('ws-legacy', 'session-cursor-legacy', 'runtime', '', 'cursor', 'Legacy Cursor', 'completed', 1, 1);
|
|
`); err != nil {
|
|
t.Fatalf("insert legacy cursor session: %v", err)
|
|
}
|
|
options := testOptions(&staticProjectPaths{})
|
|
options.TargetIDBackfillByProvider["cursor"] = "local:cursor"
|
|
store := New(db, options)
|
|
ctx := context.Background()
|
|
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
|
|
var cursorTarget sql.NullString
|
|
if err := db.QueryRowContext(ctx, `
|
|
SELECT agent_target_id FROM workspace_agent_sessions
|
|
WHERE workspace_id = 'ws-legacy' AND agent_session_id = 'session-cursor-legacy'
|
|
`).Scan(&cursorTarget); err != nil {
|
|
t.Fatalf("read legacy cursor session: %v", err)
|
|
}
|
|
if cursorTarget.String != "local:cursor" {
|
|
t.Fatalf("legacy cursor agent_target_id = %q, want local:cursor (v7 backfill)", cursorTarget.String)
|
|
}
|
|
|
|
// v7 is scoped to providers added after v5: the untargeted codex session
|
|
// keeps the claimed-v5 outcome.
|
|
var codexTarget sql.NullString
|
|
if err := db.QueryRowContext(ctx, `
|
|
SELECT agent_target_id FROM workspace_agent_sessions
|
|
WHERE workspace_id = 'ws-legacy' AND agent_session_id = 'session-untargeted'
|
|
`).Scan(&codexTarget); err != nil {
|
|
t.Fatalf("read untargeted codex session: %v", err)
|
|
}
|
|
if codexTarget.String != "" {
|
|
t.Fatalf("untargeted codex agent_target_id = %q, want empty (v7 must not replay the full v5 backfill)", codexTarget.String)
|
|
}
|
|
|
|
if applied, err := store.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV7); err != nil || !applied {
|
|
t.Fatalf("v7 marker recorded = %v error = %v, want recorded", applied, err)
|
|
}
|
|
}
|
|
|
|
func TestStoreMigrateUpgradesPartiallyMigratedLegacyDatabase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := openTestDB(t)
|
|
// A database at agent activity v3: no agent_target_id column, no
|
|
// agent_targets table, no rail columns.
|
|
if _, err := db.Exec(`
|
|
CREATE TABLE tuttid_schema_migrations (
|
|
id TEXT PRIMARY KEY,
|
|
applied_at_unix_ms INTEGER NOT NULL
|
|
);
|
|
INSERT INTO tuttid_schema_migrations (id, applied_at_unix_ms) VALUES
|
|
('workspace_agent_activity_v1', 21),
|
|
('workspace_agent_activity_v2', 22),
|
|
('workspace_agent_activity_v3', 23);
|
|
|
|
CREATE TABLE workspace_agent_sessions (
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
origin TEXT NOT NULL DEFAULT '',
|
|
provider TEXT NOT NULL DEFAULT '',
|
|
provider_session_id TEXT NOT NULL DEFAULT '',
|
|
model TEXT NOT NULL DEFAULT '',
|
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
|
runtime_context_json TEXT NOT NULL DEFAULT '{}',
|
|
cwd TEXT NOT NULL DEFAULT '',
|
|
title TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
current_phase TEXT NOT NULL DEFAULT '',
|
|
last_error TEXT NOT NULL DEFAULT '',
|
|
message_version INTEGER NOT NULL DEFAULT 0,
|
|
last_event_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
ended_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
pinned_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
PRIMARY KEY (workspace_id, agent_session_id)
|
|
);
|
|
INSERT INTO workspace_agent_sessions (
|
|
workspace_id, agent_session_id, origin, provider, status, created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES ('ws-partial', 'session-codex', 'runtime', 'codex', 'completed', 1, 1);
|
|
|
|
CREATE TABLE workspace_agent_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
message_id TEXT NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
turn_id TEXT NOT NULL DEFAULT '',
|
|
role TEXT NOT NULL DEFAULT '',
|
|
kind TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
occurred_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
completed_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE (workspace_id, agent_session_id, message_id)
|
|
);
|
|
`); err != nil {
|
|
t.Fatalf("create partial legacy database: %v", err)
|
|
}
|
|
|
|
store := New(db, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
|
|
// v4 added the column and v5 (not claimed) backfilled it.
|
|
session, ok, err := store.GetSession(ctx, "ws-partial", "session-codex")
|
|
if err != nil || !ok {
|
|
t.Fatalf("GetSession() ok=%v error=%v", ok, err)
|
|
}
|
|
if session.AgentTargetID != testTargetIDCodex {
|
|
t.Fatalf("backfilled agent target id = %q, want %s", session.AgentTargetID, testTargetIDCodex)
|
|
}
|
|
// Targets table exists with seeds; rail columns exist.
|
|
targets, err := store.ListAgentTargets(ctx)
|
|
if err != nil || len(targets) != 2 {
|
|
t.Fatalf("ListAgentTargets() len=%d error=%v, want 2 seeded targets", len(targets), err)
|
|
}
|
|
hasRailKey, err := store.hasColumn(ctx, "workspace_agent_sessions", "rail_section_key")
|
|
if err != nil || !hasRailKey {
|
|
t.Fatalf("rail_section_key present = %v error = %v", hasRailKey, err)
|
|
}
|
|
}
|
|
|
|
func TestStoreMigrateReconcilesLegacySystemTargetIDs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
now := int64(1700000000000)
|
|
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
DELETE FROM agent_targets WHERE id = ?;
|
|
`, testTargetIDCodex); err != nil {
|
|
t.Fatalf("delete seeded target: %v", err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
INSERT 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"}', 'Legacy Codex', 'codex', 1, 'system', 10, ?, ?);
|
|
`, now, now); err != nil {
|
|
t.Fatalf("insert legacy target fixture: %v", err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
INSERT INTO workspace_agent_sessions (workspace_id, agent_session_id, origin, agent_target_id, provider, created_at_unix_ms, updated_at_unix_ms)
|
|
VALUES ('ws-reconcile', 'session-1', 'runtime', 'local-codex', 'codex', ?, ?);
|
|
`, now, now); err != nil {
|
|
t.Fatalf("insert legacy session fixture: %v", err)
|
|
}
|
|
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
|
|
if _, err := store.GetAgentTarget(ctx, "local-codex"); !errors.Is(err, ErrAgentTargetNotFound) {
|
|
t.Fatalf("GetAgentTarget(legacy) error = %v, want ErrAgentTargetNotFound", err)
|
|
}
|
|
if _, err := store.GetAgentTarget(ctx, testTargetIDCodex); err != nil {
|
|
t.Fatalf("GetAgentTarget(current) error = %v", err)
|
|
}
|
|
sessions, ok, err := store.ListSessions(ctx, "ws-reconcile")
|
|
if err != nil || !ok || len(sessions) != 1 {
|
|
t.Fatalf("ListSessions() ok=%v len=%d error=%v", ok, len(sessions), err)
|
|
}
|
|
if sessions[0].AgentTargetID != testTargetIDCodex {
|
|
t.Fatalf("session agent target id = %q, want %s", sessions[0].AgentTargetID, testTargetIDCodex)
|
|
}
|
|
}
|
|
|
|
func TestStoreMigrateBackfillsRailSectionsFromInjectedProjects(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := openTestDB(t)
|
|
repoRoot := t.TempDir()
|
|
repo := repoRoot + "/repo"
|
|
repoSubdir := repo + "/pkg"
|
|
otherDir := repoRoot + "/other"
|
|
for _, path := range []string{repoSubdir, otherDir} {
|
|
if err := mkdirAll(path); err != nil {
|
|
t.Fatalf("mkdir %q error = %v", path, err)
|
|
}
|
|
}
|
|
|
|
// Legacy database at v5: rail migration not applied yet.
|
|
if _, err := db.Exec(`
|
|
CREATE TABLE tuttid_schema_migrations (
|
|
id TEXT PRIMARY KEY,
|
|
applied_at_unix_ms INTEGER NOT NULL
|
|
);
|
|
INSERT INTO tuttid_schema_migrations (id, applied_at_unix_ms) VALUES
|
|
('workspace_agent_activity_v1', 21),
|
|
('workspace_agent_activity_v2', 22),
|
|
('workspace_agent_activity_v3', 23),
|
|
('workspace_agent_activity_v4', 24),
|
|
('workspace_agent_activity_v5', 25),
|
|
('agent_targets_v1', 26);
|
|
|
|
CREATE TABLE workspace_agent_sessions (
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
origin TEXT NOT NULL DEFAULT '',
|
|
agent_target_id TEXT,
|
|
provider TEXT NOT NULL DEFAULT '',
|
|
provider_session_id TEXT NOT NULL DEFAULT '',
|
|
model TEXT NOT NULL DEFAULT '',
|
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
|
runtime_context_json TEXT NOT NULL DEFAULT '{}',
|
|
cwd TEXT NOT NULL DEFAULT '',
|
|
title TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
current_phase TEXT NOT NULL DEFAULT '',
|
|
last_error TEXT NOT NULL DEFAULT '',
|
|
message_version INTEGER NOT NULL DEFAULT 0,
|
|
last_event_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
ended_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
pinned_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
PRIMARY KEY (workspace_id, agent_session_id)
|
|
);
|
|
CREATE TABLE workspace_agent_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
message_id TEXT NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
turn_id TEXT NOT NULL DEFAULT '',
|
|
role TEXT NOT NULL DEFAULT '',
|
|
kind TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
occurred_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
completed_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE (workspace_id, agent_session_id, message_id)
|
|
);
|
|
CREATE TABLE agent_targets (
|
|
id TEXT PRIMARY KEY,
|
|
provider TEXT NOT NULL,
|
|
launch_ref_json TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
icon_key TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
source TEXT NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at_ms INTEGER NOT NULL,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
`); err != nil {
|
|
t.Fatalf("create legacy pre-rail database: %v", err)
|
|
}
|
|
if _, err := db.Exec(`
|
|
INSERT INTO workspace_agent_sessions (workspace_id, agent_session_id, provider, cwd, runtime_context_json, created_at_unix_ms, updated_at_unix_ms)
|
|
VALUES
|
|
('ws-rail-backfill', 'session-project', 'codex', ?, '{}', 1, 1),
|
|
('ws-rail-backfill', 'session-no-project', 'codex', ?, '{"externalImportNoProject":true}', 1, 1),
|
|
('ws-rail-backfill', 'session-conversations', 'codex', ?, '{}', 1, 1);
|
|
`, repoSubdir, repoSubdir, otherDir); err != nil {
|
|
t.Fatalf("insert pre-rail sessions: %v", err)
|
|
}
|
|
|
|
store := New(db, testOptions(&staticProjectPaths{paths: []string{repo}}))
|
|
ctx := context.Background()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
|
|
repoCanonical := NormalizeProjectPath(repo)
|
|
wantKeys := map[string]string{
|
|
"session-project": RailSectionKeyForProject(repoCanonical),
|
|
"session-no-project": RailSectionKeyConversations,
|
|
"session-conversations": RailSectionKeyConversations,
|
|
}
|
|
for sessionID, wantKey := range wantKeys {
|
|
var key string
|
|
if err := db.QueryRowContext(ctx, `
|
|
SELECT rail_section_key FROM workspace_agent_sessions
|
|
WHERE workspace_id = 'ws-rail-backfill' AND agent_session_id = ?
|
|
`, sessionID).Scan(&key); err != nil {
|
|
t.Fatalf("read rail key for %s: %v", sessionID, err)
|
|
}
|
|
if key != wantKey {
|
|
t.Fatalf("rail key for %s = %q, want %q", sessionID, key, wantKey)
|
|
}
|
|
}
|
|
}
|