feat(workspace): clear prune_warning_at on SetWorktreeActive/Idle

This commit is contained in:
2026-05-05 16:45:25 +08:00
parent e768adb3ff
commit ba1d657973
4 changed files with 208 additions and 21 deletions
+10 -2
View File
@@ -23,13 +23,21 @@ ORDER BY created_at DESC;
-- name: SetWorktreeActive :one
UPDATE workspace_worktrees
SET status = 'active', active_holder = $2, acquired_at = now(), last_used_at = now()
SET status = 'active',
active_holder = $2,
acquired_at = now(),
last_used_at = now(),
prune_warning_at = NULL
WHERE id = $1
RETURNING *;
-- name: SetWorktreeIdle :one
UPDATE workspace_worktrees
SET status = 'idle', active_holder = NULL, acquired_at = NULL, last_used_at = now()
SET status = 'idle',
active_holder = NULL,
acquired_at = NULL,
last_used_at = now(),
prune_warning_at = NULL
WHERE id = $1
RETURNING *;
+50
View File
@@ -136,3 +136,53 @@ func errsAlreadyActive() error {
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")
}
+122 -9
View File
@@ -21,6 +21,21 @@ type AuditLog struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Conversation struct {
ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"`
ProjectID pgtype.UUID `json:"project_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
IssueID pgtype.UUID `json:"issue_id"`
ModelID pgtype.UUID `json:"model_id"`
SystemPrompt string `json:"system_prompt"`
Title *string `json:"title"`
TitleGeneratedAt pgtype.Timestamptz `json:"title_generated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type GitCredential struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
@@ -48,6 +63,93 @@ type Issue struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
type Job struct {
ID pgtype.UUID `json:"id"`
Type string `json:"type"`
Payload []byte `json:"payload"`
Status string `json:"status"`
ScheduledAt pgtype.Timestamptz `json:"scheduled_at"`
Attempts int32 `json:"attempts"`
MaxAttempts int32 `json:"max_attempts"`
LeasedAt pgtype.Timestamptz `json:"leased_at"`
LeasedBy *string `json:"leased_by"`
LastError *string `json:"last_error"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type LlmEndpoint struct {
ID pgtype.UUID `json:"id"`
Provider string `json:"provider"`
DisplayName string `json:"display_name"`
BaseUrl string `json:"base_url"`
ApiKeyEncrypted []byte `json:"api_key_encrypted"`
CreatedBy pgtype.UUID `json:"created_by"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type LlmModel struct {
ID pgtype.UUID `json:"id"`
EndpointID pgtype.UUID `json:"endpoint_id"`
ModelID string `json:"model_id"`
DisplayName string `json:"display_name"`
Capabilities []byte `json:"capabilities"`
ContextWindow int32 `json:"context_window"`
MaxOutputTokens int32 `json:"max_output_tokens"`
PromptPricePerMillionUsd pgtype.Numeric `json:"prompt_price_per_million_usd"`
CompletionPricePerMillionUsd pgtype.Numeric `json:"completion_price_per_million_usd"`
ThinkingPricePerMillionUsd pgtype.Numeric `json:"thinking_price_per_million_usd"`
IsTitleGenerator bool `json:"is_title_generator"`
Enabled bool `json:"enabled"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type LlmUsage struct {
ID int64 `json:"id"`
ConversationID pgtype.UUID `json:"conversation_id"`
MessageID int64 `json:"message_id"`
UserID pgtype.UUID `json:"user_id"`
EndpointID pgtype.UUID `json:"endpoint_id"`
ModelID pgtype.UUID `json:"model_id"`
PromptTokens int32 `json:"prompt_tokens"`
CompletionTokens int32 `json:"completion_tokens"`
ThinkingTokens int32 `json:"thinking_tokens"`
CostUsd pgtype.Numeric `json:"cost_usd"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Message struct {
ID int64 `json:"id"`
ConversationID pgtype.UUID `json:"conversation_id"`
Role string `json:"role"`
Content string `json:"content"`
Thinking *string `json:"thinking"`
ToolCalls []byte `json:"tool_calls"`
Status string `json:"status"`
ErrorMessage *string `json:"error_message"`
PromptTokens *int32 `json:"prompt_tokens"`
CompletionTokens *int32 `json:"completion_tokens"`
ThinkingTokens *int32 `json:"thinking_tokens"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type MessageAttachment struct {
ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"`
MessageID *int64 `json:"message_id"`
Filename string `json:"filename"`
MimeType string `json:"mime_type"`
SizeBytes int64 `json:"size_bytes"`
Sha256 string `json:"sha256"`
StoragePath string `json:"storage_path"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Notification struct {
ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"`
@@ -73,6 +175,16 @@ type Project struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type PromptTemplate struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
Content string `json:"content"`
Scope string `json:"scope"`
OwnerID pgtype.UUID `json:"owner_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Requirement struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
@@ -124,13 +236,14 @@ type Workspace struct {
}
type WorkspaceWorktree struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
Branch string `json:"branch"`
Path string `json:"path"`
Status string `json:"status"`
ActiveHolder *string `json:"active_holder"`
AcquiredAt pgtype.Timestamptz `json:"acquired_at"`
LastUsedAt pgtype.Timestamptz `json:"last_used_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
Branch string `json:"branch"`
Path string `json:"path"`
Status string `json:"status"`
ActiveHolder *string `json:"active_holder"`
AcquiredAt pgtype.Timestamptz `json:"acquired_at"`
LastUsedAt pgtype.Timestamptz `json:"last_used_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
PruneWarningAt pgtype.Timestamptz `json:"prune_warning_at"`
}
+26 -10
View File
@@ -15,7 +15,7 @@ const createWorktree = `-- name: CreateWorktree :one
INSERT INTO workspace_worktrees (
id, workspace_id, branch, path, status, active_holder, acquired_at
) VALUES ($1,$2,$3,$4,$5,$6,$7)
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at
`
type CreateWorktreeParams struct {
@@ -49,6 +49,7 @@ func (q *Queries) CreateWorktree(ctx context.Context, arg CreateWorktreeParams)
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
)
return i, err
}
@@ -63,7 +64,7 @@ func (q *Queries) DeleteWorktree(ctx context.Context, id pgtype.UUID) error {
}
const getWorktreeByBranchForUpdate = `-- name: GetWorktreeByBranchForUpdate :one
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at FROM workspace_worktrees
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at FROM workspace_worktrees
WHERE workspace_id = $1 AND branch = $2
FOR UPDATE
`
@@ -86,12 +87,13 @@ func (q *Queries) GetWorktreeByBranchForUpdate(ctx context.Context, arg GetWorkt
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
)
return i, err
}
const getWorktreeByID = `-- name: GetWorktreeByID :one
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at FROM workspace_worktrees WHERE id = $1
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at FROM workspace_worktrees WHERE id = $1
`
func (q *Queries) GetWorktreeByID(ctx context.Context, id pgtype.UUID) (WorkspaceWorktree, error) {
@@ -107,12 +109,13 @@ func (q *Queries) GetWorktreeByID(ctx context.Context, id pgtype.UUID) (Workspac
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
)
return i, err
}
const getWorktreeByIDForUpdate = `-- name: GetWorktreeByIDForUpdate :one
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at FROM workspace_worktrees WHERE id = $1
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at FROM workspace_worktrees WHERE id = $1
FOR UPDATE
`
@@ -129,12 +132,13 @@ func (q *Queries) GetWorktreeByIDForUpdate(ctx context.Context, id pgtype.UUID)
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
)
return i, err
}
const listWorktreesByWorkspace = `-- name: ListWorktreesByWorkspace :many
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at FROM workspace_worktrees
SELECT id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at FROM workspace_worktrees
WHERE workspace_id = $1
ORDER BY created_at DESC
`
@@ -158,6 +162,7 @@ func (q *Queries) ListWorktreesByWorkspace(ctx context.Context, workspaceID pgty
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
); err != nil {
return nil, err
}
@@ -171,9 +176,13 @@ func (q *Queries) ListWorktreesByWorkspace(ctx context.Context, workspaceID pgty
const setWorktreeActive = `-- name: SetWorktreeActive :one
UPDATE workspace_worktrees
SET status = 'active', active_holder = $2, acquired_at = now(), last_used_at = now()
SET status = 'active',
active_holder = $2,
acquired_at = now(),
last_used_at = now(),
prune_warning_at = NULL
WHERE id = $1
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at
`
type SetWorktreeActiveParams struct {
@@ -194,15 +203,20 @@ func (q *Queries) SetWorktreeActive(ctx context.Context, arg SetWorktreeActivePa
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
)
return i, err
}
const setWorktreeIdle = `-- name: SetWorktreeIdle :one
UPDATE workspace_worktrees
SET status = 'idle', active_holder = NULL, acquired_at = NULL, last_used_at = now()
SET status = 'idle',
active_holder = NULL,
acquired_at = NULL,
last_used_at = now(),
prune_warning_at = NULL
WHERE id = $1
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at
`
func (q *Queries) SetWorktreeIdle(ctx context.Context, id pgtype.UUID) (WorkspaceWorktree, error) {
@@ -218,6 +232,7 @@ func (q *Queries) SetWorktreeIdle(ctx context.Context, id pgtype.UUID) (Workspac
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
)
return i, err
}
@@ -226,7 +241,7 @@ const setWorktreePruning = `-- name: SetWorktreePruning :one
UPDATE workspace_worktrees
SET status = 'pruning', last_used_at = now()
WHERE id = $1
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at
`
func (q *Queries) SetWorktreePruning(ctx context.Context, id pgtype.UUID) (WorkspaceWorktree, error) {
@@ -242,6 +257,7 @@ func (q *Queries) SetWorktreePruning(ctx context.Context, id pgtype.UUID) (Works
&i.AcquiredAt,
&i.LastUsedAt,
&i.CreatedAt,
&i.PruneWarningAt,
)
return i, err
}