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
359 lines
10 KiB
Go
359 lines
10 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
workspaceissues "github.com/tutti-os/tutti/packages/workspace/issues"
|
|
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
|
|
)
|
|
|
|
func TestSQLiteStoreCreateWorkspaceCreatesDefaultIssueTopic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-default-topic",
|
|
Name: "Default Topic Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
topics, err := store.ListTopics(ctx, "ws-default-topic")
|
|
if err != nil {
|
|
t.Fatalf("ListTopics() error = %v", err)
|
|
}
|
|
if len(topics.Items) != 1 {
|
|
t.Fatalf("topics len = %d, want 1: %+v", len(topics.Items), topics.Items)
|
|
}
|
|
topic := topics.Items[0]
|
|
if topic.TopicID != workspaceissues.DefaultTopicID || !topic.IsDefault {
|
|
t.Fatalf("default topic = %+v", topic)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteStoreDeleteIssueTopic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-delete-topic",
|
|
Name: "Delete Topic Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
if _, err := store.CreateTopic(ctx, workspaceissues.Topic{
|
|
TopicID: "topic-delete",
|
|
WorkspaceID: "ws-delete-topic",
|
|
Title: "Delete me",
|
|
LastActivityAtUnixMS: 100,
|
|
CreatedAtUnixMS: 100,
|
|
UpdatedAtUnixMS: 100,
|
|
}); err != nil {
|
|
t.Fatalf("CreateTopic() error = %v", err)
|
|
}
|
|
|
|
removed, err := store.DeleteTopic(ctx, "ws-delete-topic", "topic-delete")
|
|
if err != nil {
|
|
t.Fatalf("DeleteTopic() error = %v", err)
|
|
}
|
|
if !removed {
|
|
t.Fatal("DeleteTopic() removed = false, want true")
|
|
}
|
|
if _, err := store.GetTopic(ctx, "ws-delete-topic", "topic-delete"); err != workspaceissues.ErrTopicNotFound {
|
|
t.Fatalf("GetTopic() after delete error = %v, want ErrTopicNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteStoreIssueTopicsMigrationRepairsMissingDefaultTopic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-repair-default-topic",
|
|
Name: "Repair Default Topic Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
if _, err := store.writeDB.ExecContext(ctx, `
|
|
DELETE FROM workspace_issue_topics
|
|
WHERE workspace_id = ?;
|
|
`, "ws-repair-default-topic"); err != nil {
|
|
t.Fatalf("delete default topic fixture: %v", err)
|
|
}
|
|
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() repair error = %v", err)
|
|
}
|
|
|
|
topics, err := store.ListTopics(ctx, "ws-repair-default-topic")
|
|
if err != nil {
|
|
t.Fatalf("ListTopics() error = %v", err)
|
|
}
|
|
if len(topics.Items) != 1 {
|
|
t.Fatalf("topics len = %d, want 1: %+v", len(topics.Items), topics.Items)
|
|
}
|
|
topic := topics.Items[0]
|
|
if topic.TopicID != workspaceissues.DefaultTopicID || !topic.IsDefault {
|
|
t.Fatalf("default topic = %+v", topic)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteStoreIssueTopicsMigrationBackfillsLegacyIssues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
|
|
createLegacyIssueV2Database(t, dbPath)
|
|
|
|
store, err := OpenSQLiteStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("OpenSQLiteStore() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = store.Close()
|
|
})
|
|
if err := store.Migrate(context.Background()); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
topics, err := store.ListTopics(ctx, "ws-legacy-topic")
|
|
if err != nil {
|
|
t.Fatalf("ListTopics() error = %v", err)
|
|
}
|
|
if len(topics.Items) != 1 {
|
|
t.Fatalf("topics len = %d, want 1: %+v", len(topics.Items), topics.Items)
|
|
}
|
|
topic := topics.Items[0]
|
|
if topic.TopicID != workspaceissues.DefaultTopicID || !topic.IsDefault {
|
|
t.Fatalf("default topic = %+v", topic)
|
|
}
|
|
|
|
issue, err := store.GetIssue(ctx, "ws-legacy-topic", "issue-legacy")
|
|
if err != nil {
|
|
t.Fatalf("GetIssue() error = %v", err)
|
|
}
|
|
if issue.TopicID != workspaceissues.DefaultTopicID {
|
|
t.Fatalf("issue topic id = %q, want %q", issue.TopicID, workspaceissues.DefaultTopicID)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteStoreIssueTopicsMigrationIsIdempotent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
|
|
createLegacyIssueV2Database(t, dbPath)
|
|
|
|
store, err := OpenSQLiteStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("OpenSQLiteStore() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = store.Close()
|
|
})
|
|
|
|
ctx := context.Background()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() first error = %v", err)
|
|
}
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() second error = %v", err)
|
|
}
|
|
|
|
applied, err := store.hasMigration(ctx, schemaMigrationWorkspaceIssuesV3)
|
|
if err != nil {
|
|
t.Fatalf("hasMigration() error = %v", err)
|
|
}
|
|
if !applied {
|
|
t.Fatalf("migration %q was not recorded", schemaMigrationWorkspaceIssuesV3)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteStoreIssueTopicsMigrationRepairsMissingMarker(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
|
|
createLegacyIssueV2Database(t, dbPath)
|
|
|
|
store, err := OpenSQLiteStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("OpenSQLiteStore() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = store.Close()
|
|
})
|
|
|
|
ctx := context.Background()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() initial error = %v", err)
|
|
}
|
|
|
|
if _, err := store.writeDB.ExecContext(ctx, `
|
|
DELETE FROM tuttid_schema_migrations
|
|
WHERE id = ?;
|
|
INSERT INTO tuttid_schema_migrations (id, applied_at_unix_ms)
|
|
VALUES ('1780543054474', 1780543054474);
|
|
`, schemaMigrationWorkspaceIssuesV3); err != nil {
|
|
t.Fatalf("simulate missing migration marker: %v", err)
|
|
}
|
|
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() repair error = %v", err)
|
|
}
|
|
|
|
applied, err := store.hasMigration(ctx, schemaMigrationWorkspaceIssuesV3)
|
|
if err != nil {
|
|
t.Fatalf("hasMigration() error = %v", err)
|
|
}
|
|
if !applied {
|
|
t.Fatalf("migration %q was not repaired", schemaMigrationWorkspaceIssuesV3)
|
|
}
|
|
}
|
|
|
|
func createLegacyIssueV2Database(t *testing.T, dbPath string) {
|
|
t.Helper()
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("open legacy db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, 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', 1),
|
|
('workspaces_v2', 1),
|
|
('workspaces_v3', 1),
|
|
('workspaces_v4', 1),
|
|
('workspace_issues_v1', 1),
|
|
('workspace_issues_v2', 1);
|
|
|
|
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,
|
|
last_opened_at_unix_ms INTEGER
|
|
);
|
|
CREATE TABLE workspace_workbench_snapshots (
|
|
workspace_id TEXT PRIMARY KEY,
|
|
schema_version INTEGER NOT NULL,
|
|
snapshot_json TEXT NOT NULL,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE workspace_issues (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
issue_id TEXT NOT NULL,
|
|
workspace_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
content TEXT NOT NULL DEFAULT '',
|
|
search_text TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL,
|
|
task_count INTEGER NOT NULL DEFAULT 0,
|
|
not_started_count INTEGER NOT NULL DEFAULT 0,
|
|
running_count INTEGER NOT NULL DEFAULT 0,
|
|
pending_acceptance_count INTEGER NOT NULL DEFAULT 0,
|
|
completed_count INTEGER NOT NULL DEFAULT 0,
|
|
failed_count INTEGER NOT NULL DEFAULT 0,
|
|
canceled_count INTEGER NOT NULL DEFAULT 0,
|
|
creator_user_id TEXT NOT NULL DEFAULT '',
|
|
creator_display_name TEXT NOT NULL DEFAULT '',
|
|
creator_avatar_url TEXT NOT NULL DEFAULT '',
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE(workspace_id, issue_id)
|
|
);
|
|
CREATE TABLE workspace_issue_tasks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
task_id TEXT NOT NULL,
|
|
issue_id TEXT NOT NULL,
|
|
workspace_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
content TEXT NOT NULL DEFAULT '',
|
|
search_text TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL,
|
|
priority TEXT NOT NULL,
|
|
sort_index INTEGER NOT NULL,
|
|
due_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
creator_user_id TEXT NOT NULL DEFAULT '',
|
|
creator_display_name TEXT NOT NULL DEFAULT '',
|
|
creator_avatar_url TEXT NOT NULL DEFAULT '',
|
|
latest_run_id TEXT NOT NULL DEFAULT '',
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE(workspace_id, issue_id, task_id)
|
|
);
|
|
CREATE TABLE workspace_issue_context_refs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
context_ref_id TEXT NOT NULL,
|
|
workspace_id TEXT NOT NULL,
|
|
issue_id TEXT NOT NULL,
|
|
task_id TEXT NOT NULL DEFAULT '',
|
|
parent_kind TEXT NOT NULL,
|
|
ref_type TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE(workspace_id, context_ref_id)
|
|
);
|
|
CREATE TABLE workspace_issue_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
run_id TEXT NOT NULL,
|
|
task_id TEXT NOT NULL DEFAULT '',
|
|
issue_id TEXT NOT NULL,
|
|
workspace_id TEXT NOT NULL,
|
|
requester_user_id TEXT NOT NULL DEFAULT '',
|
|
agent_user_id TEXT NOT NULL DEFAULT '',
|
|
agent_session_id TEXT NOT NULL DEFAULT '',
|
|
agent_provider TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL,
|
|
summary TEXT NOT NULL DEFAULT '',
|
|
error_message TEXT NOT NULL DEFAULT '',
|
|
output_dir TEXT NOT NULL DEFAULT '',
|
|
execution_directory TEXT NOT NULL DEFAULT '',
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
completed_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE(workspace_id, issue_id, task_id, run_id)
|
|
);
|
|
CREATE TABLE workspace_issue_run_outputs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
output_id TEXT NOT NULL,
|
|
run_id TEXT NOT NULL,
|
|
task_id TEXT NOT NULL DEFAULT '',
|
|
issue_id TEXT NOT NULL,
|
|
workspace_id TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
media_type TEXT NOT NULL DEFAULT '',
|
|
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
UNIQUE(workspace_id, issue_id, task_id, run_id, output_id)
|
|
);
|
|
|
|
INSERT INTO workspaces (id, name, created_at_unix_ms, updated_at_unix_ms, last_opened_at_unix_ms)
|
|
VALUES ('ws-legacy-topic', 'Legacy Topic Workspace', 10, 10, NULL);
|
|
INSERT INTO workspace_issues (
|
|
issue_id, workspace_id, title, content, search_text, status,
|
|
created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES (
|
|
'issue-legacy', 'ws-legacy-topic', 'Legacy issue', '', '', 'not_started', 20, 30
|
|
);
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("create legacy issue db: %v", err)
|
|
}
|
|
}
|