Files
pad/internal/store/workspaces_source_test.go
T
xarmian 584ac9a806 fix(web): treat CLI/MCP-created workspaces as agent-connected (BUG-1557) (#781)
`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
2026-07-01 23:25:24 -04:00

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])
}
}