diff --git a/internal/workspace/queries/worktrees.sql b/internal/workspace/queries/worktrees.sql index 0c987c1..255e070 100644 --- a/internal/workspace/queries/worktrees.sql +++ b/internal/workspace/queries/worktrees.sql @@ -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 *; diff --git a/internal/workspace/repository_test.go b/internal/workspace/repository_test.go index 85be473..a35481c 100644 --- a/internal/workspace/repository_test.go +++ b/internal/workspace/repository_test.go @@ -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") +} diff --git a/internal/workspace/sqlc/models.go b/internal/workspace/sqlc/models.go index 09bf0fb..00dca3b 100644 --- a/internal/workspace/sqlc/models.go +++ b/internal/workspace/sqlc/models.go @@ -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"` } diff --git a/internal/workspace/sqlc/worktrees.sql.go b/internal/workspace/sqlc/worktrees.sql.go index 67614ec..c7f4eba 100644 --- a/internal/workspace/sqlc/worktrees.sql.go +++ b/internal/workspace/sqlc/worktrees.sql.go @@ -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 }