You've already forked agentic-coding-workflow
189 lines
6.1 KiB
Go
189 lines
6.1 KiB
Go
//go:build integration
|
|
|
|
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/stretchr/testify/require"
|
|
tcpg "github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/db"
|
|
)
|
|
|
|
// setupRepo 起一个临时 postgres 容器、跑迁移、注入一条 user/project 行做外键,
|
|
// 返回可直接调用的 Repository 与所属 project_id。每个测试用独立容器,互不干扰,
|
|
// 便于 Parallel。
|
|
func setupRepo(t *testing.T) (Repository, *pgxpool.Pool, uuid.UUID) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
container, err := tcpg.Run(ctx, "postgres:16-alpine",
|
|
tcpg.WithDatabase("acw"),
|
|
tcpg.WithUsername("acw"),
|
|
tcpg.WithPassword("acw"),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = container.Terminate(ctx) })
|
|
|
|
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, db.Migrate(ctx, dsn, "file://../../migrations"))
|
|
|
|
pool, err := db.NewPool(ctx, dsn)
|
|
require.NoError(t, err)
|
|
t.Cleanup(pool.Close)
|
|
|
|
uid := uuid.New()
|
|
_, err = pool.Exec(ctx, `INSERT INTO users (id,email,password_hash,display_name,is_admin,created_at,updated_at)
|
|
VALUES ($1,'a@b.c','x','admin',true,now(),now())`, uid)
|
|
require.NoError(t, err)
|
|
pid := uuid.New()
|
|
_, err = pool.Exec(ctx, `INSERT INTO projects (id,slug,name,owner_id,visibility) VALUES ($1,'p','P',$2,'private')`, pid, uid)
|
|
require.NoError(t, err)
|
|
return NewPostgresRepository(pool), pool, pid
|
|
}
|
|
|
|
func TestRepo_CreateWorkspaceAndRead(t *testing.T) {
|
|
t.Parallel()
|
|
repo, _, pid := setupRepo(t)
|
|
ctx := context.Background()
|
|
id := uuid.New()
|
|
ws, err := repo.CreateWorkspace(ctx, &Workspace{
|
|
ID: id, ProjectID: pid, Slug: "ws1", Name: "W1",
|
|
GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/data/ws1/main",
|
|
SyncStatus: SyncStatusCloning,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ws1", ws.Slug)
|
|
|
|
got, err := repo.GetWorkspaceByID(ctx, id)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "W1", got.Name)
|
|
}
|
|
|
|
func TestRepo_SyncStatusRoundtrip(t *testing.T) {
|
|
t.Parallel()
|
|
repo, _, pid := setupRepo(t)
|
|
ctx := context.Background()
|
|
id := uuid.New()
|
|
_, err := repo.CreateWorkspace(ctx, &Workspace{
|
|
ID: id, ProjectID: pid, Slug: "ws2", Name: "W2",
|
|
GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusCloning,
|
|
})
|
|
require.NoError(t, err)
|
|
now := time.Now().UTC()
|
|
_, err = repo.UpdateWorkspaceSyncStatus(ctx, id, SyncStatusError, &now, "boom")
|
|
require.NoError(t, err)
|
|
got, err := repo.GetWorkspaceByID(ctx, id)
|
|
require.NoError(t, err)
|
|
require.Equal(t, SyncStatusError, got.SyncStatus)
|
|
require.Equal(t, "boom", got.LastSyncError)
|
|
}
|
|
|
|
func TestRepo_AcquireRelease_Concurrent(t *testing.T) {
|
|
t.Parallel()
|
|
repo, _, pid := setupRepo(t)
|
|
ctx := context.Background()
|
|
wsID := uuid.New()
|
|
_, err := repo.CreateWorkspace(ctx, &Workspace{
|
|
ID: wsID, ProjectID: pid, Slug: "ws3", Name: "W3",
|
|
GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusIdle,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
first := make(chan error, 1)
|
|
second := make(chan error, 1)
|
|
holderA, holderB := "user:a", "user:b"
|
|
go func() {
|
|
first <- repo.InTx(ctx, func(tx Tx) error {
|
|
wt, err := tx.GetWorktreeForUpdate(ctx, wsID, "feat")
|
|
if err == nil && wt.Status == WorktreeStatusActive {
|
|
return nil
|
|
}
|
|
_, err = tx.InsertWorktree(ctx, &Worktree{
|
|
ID: uuid.New(), WorkspaceID: wsID, Branch: "feat",
|
|
Path: "/x/.worktrees/feat", Status: WorktreeStatusActive,
|
|
ActiveHolder: holderA,
|
|
})
|
|
return err
|
|
})
|
|
}()
|
|
<-first
|
|
go func() {
|
|
second <- repo.InTx(ctx, func(tx Tx) error {
|
|
wt, err := tx.GetWorktreeForUpdate(ctx, wsID, "feat")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if wt.Status == WorktreeStatusActive && wt.ActiveHolder != holderB {
|
|
return errsAlreadyActive()
|
|
}
|
|
return nil
|
|
})
|
|
}()
|
|
require.Error(t, <-second)
|
|
}
|
|
|
|
func errsAlreadyActive() error {
|
|
return &shimErr{}
|
|
}
|
|
|
|
type shimErr struct{}
|
|
|
|
func (e *shimErr) Error() string { return "already active" }
|
|
|
|
// TestRepo_SetWorktreeActive_ClearsPruneWarning 验证 SetWorktreeActive
|
|
// 会清空 prune_warning_at 列;同样验证 SetWorktreeIdle。前置:用直接 SQL
|
|
// 把列写成非 NULL;行为:repo 调用后再读 DB 应回到 NULL。
|
|
func TestRepo_SetWorktreeActive_ClearsPruneWarning(t *testing.T) {
|
|
t.Parallel()
|
|
repo, pool, pid := setupRepo(t)
|
|
ctx := context.Background()
|
|
wsID := uuid.New()
|
|
_, err := repo.CreateWorkspace(ctx, &Workspace{
|
|
ID: wsID, ProjectID: pid, Slug: "ws-pw", Name: "PW",
|
|
GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusIdle,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
wtID := uuid.New()
|
|
require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
|
|
_, e := tx.InsertWorktree(ctx, &Worktree{
|
|
ID: wtID, WorkspaceID: wsID, Branch: "warn-branch",
|
|
Path: "/x/.worktrees/warn", Status: WorktreeStatusIdle,
|
|
})
|
|
return e
|
|
}))
|
|
|
|
// 把 prune_warning_at 写成非 NULL。
|
|
_, err = pool.Exec(ctx, `UPDATE workspace_worktrees SET prune_warning_at = now() WHERE id = $1`, wtID)
|
|
require.NoError(t, err)
|
|
|
|
var pwAt *time.Time
|
|
require.NoError(t, pool.QueryRow(ctx, `SELECT prune_warning_at FROM workspace_worktrees WHERE id = $1`, wtID).Scan(&pwAt))
|
|
require.NotNil(t, pwAt, "precondition: prune_warning_at should be set")
|
|
|
|
// SetWorktreeActive 应该清空它。
|
|
require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
|
|
_, e := tx.SetWorktreeActive(ctx, wtID, "user:tester")
|
|
return e
|
|
}))
|
|
require.NoError(t, pool.QueryRow(ctx, `SELECT prune_warning_at FROM workspace_worktrees WHERE id = $1`, wtID).Scan(&pwAt))
|
|
require.Nil(t, pwAt, "SetWorktreeActive should clear prune_warning_at")
|
|
|
|
// 再写回非 NULL,验证 SetWorktreeIdle 也会清空。
|
|
_, err = pool.Exec(ctx, `UPDATE workspace_worktrees SET prune_warning_at = now() WHERE id = $1`, wtID)
|
|
require.NoError(t, err)
|
|
require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
|
|
_, e := tx.SetWorktreeIdle(ctx, wtID)
|
|
return e
|
|
}))
|
|
require.NoError(t, pool.QueryRow(ctx, `SELECT prune_warning_at FROM workspace_worktrees WHERE id = $1`, wtID).Scan(&pwAt))
|
|
require.Nil(t, pwAt, "SetWorktreeIdle should clear prune_warning_at")
|
|
}
|