mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-12 05:49:00 +00:00
584ac9a806
`pad init` connects an agent (installs the skill, stores credentials) and creates a workspace, but the web UI still showed the "connect an agent" banner and onboarding launchpad. The only signal for "agent connected" was has_agent_activity — an item existing with source cli/mcp — and a fresh pad-init workspace has zero items, so the UI nagged to connect an agent the user already had. Give the server a truthful signal: a workspace created through an agent surface already has an agent wired up before it creates its first item. Add a `source` column to workspaces (web/cli/mcp), attributed authoritatively server-side from the request auth shape (actorFromRequest) — never from the request body, so a web client can't spoof "cli" to self-suppress the prompts. The dashboard ORs source in (cli,mcp) into has_agent_activity when the cheap item check comes up empty. - migrations 069 (sqlite) / 047 (postgres): workspaces.source NOT NULL DEFAULT '' (legacy rows stay "unknown", never treated as agent-created) - models.Workspace.Source + WorkspaceCreate.Source (json:"-", server-set) - thread source through the CreateWorkspace INSERT + all 7 workspace scan sites (workspaces.go, workspace_members.go) - handleCreateWorkspace derives source from actorFromRequest - OnboardingLaunchpad step 1 collapses to "Agent connected" when the agent is already wired up, shifting emphasis to "tell it to set up" Web modal and cloud-signup auto-create flows are unchanged and still correctly prompt to connect (source web / empty). Tests: store source round-trip across reads; dashboard reports agent-connected for a cli-created workspace with zero items; web-created stays not-connected until an agent item exists; a web body-spoofed source is ignored. Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST
59 lines
1.8 KiB
Go
59 lines
1.8 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
)
|
|
|
|
// TestWorkspaceSourcePersisted covers migration 069 / BUG-1557: the
|
|
// workspaces.source column round-trips through CreateWorkspace and every
|
|
// read that scans a models.Workspace. A missed scan site would surface here
|
|
// (or as a "sql: expected N destination arguments" failure) rather than at
|
|
// runtime.
|
|
func TestWorkspaceSourcePersisted(t *testing.T) {
|
|
s := testStore(t)
|
|
|
|
cli, err := s.CreateWorkspace(models.WorkspaceCreate{Name: "Agent WS", Source: "cli"})
|
|
if err != nil {
|
|
t.Fatalf("create cli workspace: %v", err)
|
|
}
|
|
if cli.Source != "cli" {
|
|
t.Fatalf("CreateWorkspace return: expected source=cli, got %q", cli.Source)
|
|
}
|
|
|
|
// CreateWorkspace returns via GetWorkspaceBySlug, but assert the other
|
|
// single-row read (by ID) round-trips it too.
|
|
byID, err := s.GetWorkspaceByID(cli.ID)
|
|
if err != nil || byID == nil {
|
|
t.Fatalf("get by id: %v (nil=%v)", err, byID == nil)
|
|
}
|
|
if byID.Source != "cli" {
|
|
t.Fatalf("GetWorkspaceByID: expected source=cli, got %q", byID.Source)
|
|
}
|
|
|
|
// A workspace created without an explicit source (the common
|
|
// direct-store path — cloud auto-create, import, test helpers) stays
|
|
// "" so it is never treated as agent-created.
|
|
web := createTestWorkspace(t, s, "Web WS")
|
|
if web.Source != "" {
|
|
t.Fatalf("default CreateWorkspace: expected empty source, got %q", web.Source)
|
|
}
|
|
|
|
// ListWorkspaces (cross-tenant) round-trips source for every row.
|
|
all, err := s.ListWorkspaces()
|
|
if err != nil {
|
|
t.Fatalf("list workspaces: %v", err)
|
|
}
|
|
got := map[string]string{}
|
|
for _, w := range all {
|
|
got[w.ID] = w.Source
|
|
}
|
|
if got[cli.ID] != "cli" {
|
|
t.Fatalf("ListWorkspaces: expected cli workspace source=cli, got %q", got[cli.ID])
|
|
}
|
|
if got[web.ID] != "" {
|
|
t.Fatalf("ListWorkspaces: expected web workspace source=\"\", got %q", got[web.ID])
|
|
}
|
|
}
|