package domain_test import ( "context" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" "github.com/krow/krow-backend/go-api/internal/testutil" ) // Phase 4C — the shape of the authored-definition tables. // // These test the MIGRATION, not any Go code: there is no agent or skill package // yet, and there deliberately is not one until Phase 4D. What is under test is // whether the database refuses the things it is supposed to refuse. // // An external test package (`domain_test`) rather than `package domain`, // because testutil imports seeder which imports domain — reachable from an // external test binary, an import cycle from an internal one. const ( agents = "agent_definitions" skills = "skill_definitions" ) // fixture is a migrated sandbox with one organization and two users. type fixture struct { pool *pgxpool.Pool ctx context.Context orgID string alice string bob string } func newFixture(t *testing.T, label string) *fixture { t.Helper() ctx := context.Background() pool := testutil.Sandbox(t, label) testutil.ApplyAllMigrations(ctx, t, pool) f := &fixture{pool: pool, ctx: ctx} if err := pool.QueryRow(ctx, `INSERT INTO organizations (name, slug) VALUES ('Defs Org','defs-org') RETURNING id::text`). Scan(&f.orgID); err != nil { t.Fatalf("create organization: %v", err) } f.alice = f.newUser(t, "alice@example.test") f.bob = f.newUser(t, "bob@example.test") return f } func (f *fixture) newUser(t *testing.T, email string) string { t.Helper() var id string if err := f.pool.QueryRow(f.ctx, `INSERT INTO users (org_id, email, full_name) VALUES ($1::uuid, $2::citext, $3) RETURNING id::text`, f.orgID, email, email).Scan(&id); err != nil { t.Fatalf("create user %s: %v", email, err) } return id } // row is one candidate definition. Any field may be made deliberately wrong. type row struct { table string defID string orgID string visibility string owner *string createdBy *string markdown string status string version *int } func (f *fixture) insert(r row) (string, error) { cols := []string{"definition_id", "org_id", "visibility", "owner_user_id", "created_by", "markdown"} vals := []string{"$1::text", "$2::uuid", "$3::text", "$4::uuid", "$5::uuid", "$6::text"} args := []any{r.defID, r.orgID, r.visibility, r.owner, r.createdBy, r.markdown} if r.status != "" { cols, vals = append(cols, "status"), append(vals, "$7::text") args = append(args, r.status) } if r.version != nil { cols = append(cols, "version") vals = append(vals, "$"+itoa(len(args)+1)+"::integer") args = append(args, *r.version) } var id string err := f.pool.QueryRow(f.ctx, "INSERT INTO "+r.table+" ("+strings.Join(cols, ", ")+") VALUES ("+ strings.Join(vals, ", ")+") RETURNING id::text", args...).Scan(&id) return id, err } func itoa(n int) string { if n < 10 { return string(rune('0' + n)) } return string(rune('0'+n/10)) + string(rune('0'+n%10)) } // personal and organization build a valid row of each tier, so a test can // change exactly one thing and see whether the database notices. func (f *fixture) personal(table, defID, owner string) row { return row{table: table, defID: defID, orgID: f.orgID, visibility: "personal", owner: &owner, createdBy: &owner, markdown: "---\nid: " + defID + "\n---\n"} } func (f *fixture) organization(table, defID, author string) row { return row{table: table, defID: defID, orgID: f.orgID, visibility: "organization", owner: nil, createdBy: &author, markdown: "---\nid: " + defID + "\n---\n"} } func mustInsert(t *testing.T, f *fixture, r row) string { t.Helper() id, err := f.insert(r) if err != nil { t.Fatalf("a valid %s row was refused: %v", r.table, err) } return id } func refused(t *testing.T, f *fixture, r row, wantConstraint, why string) { t.Helper() _, err := f.insert(r) if err == nil { t.Fatalf("%s: the row was ACCEPTED — %s", r.table, why) } if wantConstraint != "" && !strings.Contains(err.Error(), wantConstraint) { t.Errorf("%s: refused by %v, want the %s constraint", r.table, err, wantConstraint) } } /* ── 1, 2. The ownership invariant ──────────────────────────────────────── */ func TestVisibilityRequiresMatchingOwnership(t *testing.T) { f := newFixture(t, "defs_ownership") for _, table := range []string{agents, skills} { t.Run(table, func(t *testing.T) { // The two valid shapes. mustInsert(t, f, f.personal(table, "valid-personal", f.alice)) mustInsert(t, f, f.organization(table, "valid-org", f.alice)) // 1. A personal definition with no owner belongs to nobody. bad := f.personal(table, "no-owner", f.alice) bad.owner = nil refused(t, f, bad, "visibility_owner", "a personal definition must have an owner") // 2. An organization definition with an owner is two answers to // "whose is this", which is one too many. bad = f.organization(table, "with-owner", f.alice) bad.owner = &f.alice refused(t, f, bad, "visibility_owner", "an organization definition must not have an owner") // And an unrecognised tier is not a tier. bad = f.personal(table, "bad-tier", f.alice) bad.visibility = "public" refused(t, f, bad, "visibility_check", "`public` is not a visibility") }) } } /* ── 3. definition_id format ────────────────────────────────────────────── */ // The same rule the frontend validator enforces, restated in the database so a // caller that bypasses the application cannot store an id the registry could // never address. func TestDefinitionIDFormat(t *testing.T) { f := newFixture(t, "defs_idformat") valid := []string{"a", "board", "krow-workforce-agent", "x1", "a-1-b", "0abc"} invalid := map[string]string{ "leading dash": "-board", "upper case": "Board", "underscore": "my_skill", "space": "my skill", "trailing dot": "board.", "empty": "", "slash": "custom/board", "unicode": "bòard", "sql-ish": "a'; DROP TABLE users; --", "newline": "board\nx", } for _, table := range []string{agents, skills} { t.Run(table, func(t *testing.T) { for _, id := range valid { if _, err := f.insert(f.personal(table, id, f.alice)); err != nil { t.Errorf("valid id %q was refused: %v", id, err) } } for name, id := range invalid { bad := f.personal(table, id, f.bob) refused(t, f, bad, "definition_id_format", "id "+name+" ("+id+") is not a valid id") } }) } } /* ── 4, 5, 6, 7. Status vocabularies and version ────────────────────────── */ func TestAgentStatusAndVersion(t *testing.T) { f := newFixture(t, "defs_agentstatus") // 4. The three agent statuses, and nothing else. for _, status := range []string{"draft", "published", "archived"} { r := f.personal(agents, "s-"+status, f.alice) r.status = status mustInsert(t, f, r) } for _, status := range []string{"active", "inactive", "live", "DRAFT", ""} { r := f.personal(agents, "bad-status", f.bob) r.status = status if status == "" { continue // an omitted status takes the default; tested below } refused(t, f, r, "status_check", "`"+status+"` is not an agent status") } // The default is draft: creating an agent must never publish it. id := mustInsert(t, f, f.personal(agents, "defaulted", f.bob)) var status string var version int if err := f.pool.QueryRow(f.ctx, `SELECT status, version FROM agent_definitions WHERE id = $1::uuid`, id). Scan(&status, &version); err != nil { t.Fatalf("read back: %v", err) } if status != "draft" { t.Errorf("default status = %q, want draft", status) } if version != 1 { t.Errorf("default version = %d, want 1", version) } // 6. A version is a whole number of 1 or more. for _, v := range []int{0, -1, -100} { r := f.personal(agents, "bad-version", f.bob) r.version = &v refused(t, f, r, "version_check", "version must be at least 1") } for _, v := range []int{1, 2, 9999} { r := f.personal(agents, "v-ok", f.alice) r.version = &v r.defID = "v-ok-" + itoa(v%100) if _, err := f.insert(r); err != nil { t.Errorf("version %d was refused: %v", v, err) } } } func TestSkillStatusAndNoVersion(t *testing.T) { f := newFixture(t, "defs_skillstatus") // 5. The two skill statuses, and nothing else. for _, status := range []string{"active", "inactive"} { r := f.personal(skills, "s-"+status, f.alice) r.status = status mustInsert(t, f, r) } for _, status := range []string{"draft", "published", "archived", "ACTIVE"} { r := f.personal(skills, "bad-status", f.bob) r.status = status refused(t, f, r, "status_check", "`"+status+"` is not a skill status") } // The default is active — a skill is on unless somebody turns it off. id := mustInsert(t, f, f.personal(skills, "defaulted", f.bob)) var status string if err := f.pool.QueryRow(f.ctx, `SELECT status FROM skill_definitions WHERE id = $1::uuid`, id).Scan(&status); err != nil { t.Fatalf("read back: %v", err) } if status != "active" { t.Errorf("default status = %q, want active", status) } // 7. Skills have NO version. The frontend has no notion of one, so the // column must not exist — inventing it "for symmetry" would create a field // nothing can set and nothing can mean. var exists int if err := f.pool.QueryRow(f.ctx, `SELECT count(*)::int FROM information_schema.columns WHERE table_schema='public' AND table_name='skill_definitions' AND column_name='version'`). Scan(&exists); err != nil { t.Fatalf("look for a version column: %v", err) } if exists != 0 { t.Error("skill_definitions has a version column; skills have no version concept") } } /* ── 8. Markdown bound ──────────────────────────────────────────────────── */ func TestMarkdownSizeBound(t *testing.T) { f := newFixture(t, "defs_markdown") for _, table := range []string{agents, skills} { t.Run(table, func(t *testing.T) { // The largest definition shipped with the product is 3,156 bytes, // so anything realistic is far inside the bound. ok := f.personal(table, "big-but-fine", f.alice) ok.markdown = strings.Repeat("x", 65536) mustInsert(t, f, ok) over := f.personal(table, "too-big", f.bob) over.markdown = strings.Repeat("x", 65537) refused(t, f, over, "markdown_size", "a definition over the size bound") empty := f.personal(table, "empty-md", f.bob) empty.markdown = "" refused(t, f, empty, "markdown_size", "an empty definition cannot parse") }) } } // The Markdown is stored byte-for-byte. A definition has to survive a round // trip to a .md file on disk, so anything that rewrote it here — trimming, // newline normalisation, unicode folding — would break that. func TestMarkdownIsStoredVerbatim(t *testing.T) { f := newFixture(t, "defs_verbatim") // A BOM, CRLF endings, trailing spaces and a tab — exactly the four things // normalizeDefinition exists to tolerate. The database must not "help" by // removing any of them: normalising is the parser's job, on read. source := "\ufeff---\r\nid: verbatim\r\nname: Verbatim\r\n---\r\n\r\n# Verbatim \r\n\ttabbed\n" r := f.personal(agents, "verbatim", f.alice) r.markdown = source id := mustInsert(t, f, r) var stored string if err := f.pool.QueryRow(f.ctx, `SELECT markdown FROM agent_definitions WHERE id = $1::uuid`, id).Scan(&stored); err != nil { t.Fatalf("read back: %v", err) } if stored != source { t.Errorf("the stored Markdown differs from what was written:\n in %q\n out %q", source, stored) } } /* ── 9, 10, 11, 12. Foreign keys and deletion ───────────────────────────── */ func TestForeignKeysAndDeleteBehaviour(t *testing.T) { f := newFixture(t, "defs_fk") missing := "00000000-0000-0000-0000-000000000000" for _, table := range []string{agents, skills} { t.Run(table+"/rejects unknown references", func(t *testing.T) { // 9. An organization that does not exist. bad := f.personal(table, "bad-org", f.alice) bad.orgID = missing refused(t, f, bad, "org_id_fkey", "org_id must reference a real organization") // 10. An owner that does not exist. bad = f.personal(table, "bad-owner", f.alice) bad.owner = &missing refused(t, f, bad, "owner_user_id_fkey", "owner_user_id must reference a real user") // 11. An author that does not exist. bad = f.organization(table, "bad-author", f.alice) bad.createdBy = &missing refused(t, f, bad, "created_by_fkey", "created_by must reference a real user") }) } // 12. Deletion, three behaviours, each different and each deliberate. t.Run("deleting the owner destroys their personal definitions", func(t *testing.T) { carol := f.newUser(t, "carol@example.test") mustInsert(t, f, f.personal(agents, "carols-agent", carol)) mustInsert(t, f, f.personal(skills, "carols-skill", carol)) if _, err := f.pool.Exec(f.ctx, `DELETE FROM users WHERE id = $1::uuid`, carol); err != nil { t.Fatalf("delete the user: %v", err) } for _, table := range []string{agents, skills} { var n int if err := f.pool.QueryRow(f.ctx, "SELECT count(*)::int FROM "+table+" WHERE definition_id LIKE 'carols-%'").Scan(&n); err != nil { t.Fatalf("count: %v", err) } if n != 0 { t.Errorf("%s: %d personal definitions survive their deleted owner, want 0", table, n) } } }) t.Run("deleting the author keeps the organization's definition", func(t *testing.T) { dave := f.newUser(t, "dave@example.test") id := mustInsert(t, f, f.organization(agents, "daves-shared-agent", dave)) if _, err := f.pool.Exec(f.ctx, `DELETE FROM users WHERE id = $1::uuid`, dave); err != nil { t.Fatalf("delete the user: %v", err) } var author *string if err := f.pool.QueryRow(f.ctx, `SELECT created_by::text FROM agent_definitions WHERE id = $1::uuid`, id).Scan(&author); err != nil { t.Fatalf("the shared definition did not survive its author: %v", err) } if author != nil { t.Errorf("created_by = %v, want NULL after the author was deleted", *author) } }) t.Run("deleting the organization destroys both tiers", func(t *testing.T) { g := newFixture(t, "defs_orgcascade") mustInsert(t, g, g.personal(agents, "doomed-personal", g.alice)) mustInsert(t, g, g.organization(skills, "doomed-shared", g.alice)) if _, err := g.pool.Exec(g.ctx, `DELETE FROM organizations WHERE id = $1::uuid`, g.orgID); err != nil { t.Fatalf("delete the organization: %v", err) } for _, table := range []string{agents, skills} { var n int if err := g.pool.QueryRow(g.ctx, "SELECT count(*)::int FROM "+table).Scan(&n); err != nil { t.Fatalf("count: %v", err) } if n != 0 { t.Errorf("%s: %d rows survive their deleted organization, want 0", table, n) } } }) } /* ── 13, 14. Uniqueness, per tier ───────────────────────────────────────── */ func TestUniquenessPerTier(t *testing.T) { f := newFixture(t, "defs_unique") for _, table := range []string{agents, skills} { t.Run(table, func(t *testing.T) { // 13. One personal definition per id per owner. mustInsert(t, f, f.personal(table, "board", f.alice)) refused(t, f, f.personal(table, "board", f.alice), "personal_key", "one user cannot hold two personal definitions of the same id") // A different user may hold their own, which is the whole point of // personal definitions. mustInsert(t, f, f.personal(table, "board", f.bob)) // 14. One organization definition per id per organization. mustInsert(t, f, f.organization(table, "board", f.alice)) refused(t, f, f.organization(table, "board", f.bob), "org_key", "one organization cannot hold two shared definitions of the same id") // Personal and organization definitions of the SAME id coexist: // that is shadow-by-id, and it is the reason definition_id is not // globally unique. var personal, shared int if err := f.pool.QueryRow(f.ctx, "SELECT count(*) FILTER (WHERE visibility='personal'), "+ "count(*) FILTER (WHERE visibility='organization') "+ "FROM "+table+" WHERE definition_id = 'board'").Scan(&personal, &shared); err != nil { t.Fatalf("count: %v", err) } if personal != 2 || shared != 1 { t.Errorf("board: %d personal + %d shared, want 2 + 1", personal, shared) } }) } // A second organization may hold its own definition of the same id. t.Run("across organizations", func(t *testing.T) { var otherOrg string if err := f.pool.QueryRow(f.ctx, `INSERT INTO organizations (name, slug) VALUES ('Other','other-defs') RETURNING id::text`). Scan(&otherOrg); err != nil { t.Fatalf("create the second organization: %v", err) } var erin string if err := f.pool.QueryRow(f.ctx, `INSERT INTO users (org_id, email, full_name) VALUES ($1::uuid,'erin@example.test','Erin') RETURNING id::text`, otherOrg).Scan(&erin); err != nil { t.Fatalf("create a user in the second organization: %v", err) } r := f.organization(agents, "board", erin) r.orgID = otherOrg mustInsert(t, f, r) }) } /* ── Schema shape ───────────────────────────────────────────────────────── */ func TestDefinitionTablesShape(t *testing.T) { f := newFixture(t, "defs_shape") shared := map[string]string{ "id": "uuid", "definition_id": "text", "org_id": "uuid", "visibility": "text", "owner_user_id": "uuid", "created_by": "text-or-uuid", // placeholder, replaced below "markdown": "text", "status": "text", "name": "text", "description": "text", "pages": "ARRAY", "created_date": "timestamp with time zone", "updated_date": "timestamp with time zone", } shared["created_by"] = "uuid" nullable := map[string]bool{"owner_user_id": true, "created_by": true} for _, table := range []string{agents, skills} { want := map[string]string{} for k, v := range shared { want[k] = v } if table == agents { want["version"] = "integer" } t.Run(table, func(t *testing.T) { rows, err := f.pool.Query(f.ctx, `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name=$1`, table) if err != nil { t.Fatalf("read columns: %v", err) } got := map[string]string{} for rows.Next() { var name, kind, isNullable string if err := rows.Scan(&name, &kind, &isNullable); err != nil { t.Fatalf("scan: %v", err) } got[name] = kind if (isNullable == "YES") != nullable[name] { t.Errorf("%s.%s is_nullable=%s, want nullable=%v", table, name, isNullable, nullable[name]) } } rows.Close() if err := rows.Err(); err != nil { t.Fatalf("read columns: %v", err) } for name, kind := range want { if got[name] == "" { t.Errorf("%s.%s is missing", table, name) } else if got[name] != kind { t.Errorf("%s.%s is %s, want %s", table, name, got[name], kind) } } for name := range got { if _, expected := want[name]; !expected { t.Errorf("%s has an unexpected column %q", table, name) } } }) } } func TestDefinitionIndexes(t *testing.T) { f := newFixture(t, "defs_indexes") want := map[string][]string{ agents: { "agent_definitions_pkey", "agent_definitions_personal_key", "agent_definitions_org_key", "agent_definitions_org_visibility_idx", "agent_definitions_owner_idx", "agent_definitions_published_idx", }, skills: { "skill_definitions_pkey", "skill_definitions_personal_key", "skill_definitions_org_key", "skill_definitions_org_visibility_idx", "skill_definitions_owner_idx", "skill_definitions_active_idx", }, } for table, names := range want { rows, err := f.pool.Query(f.ctx, `SELECT indexname, indexdef FROM pg_indexes WHERE schemaname='public' AND tablename=$1`, table) if err != nil { t.Fatalf("list indexes: %v", err) } got := map[string]string{} for rows.Next() { var name, def string if err := rows.Scan(&name, &def); err != nil { t.Fatalf("scan: %v", err) } got[name] = def } rows.Close() for _, name := range names { if got[name] == "" { t.Errorf("%s: index %s is missing", table, name) } } // The two uniqueness indexes must be partial and unique, or they mean // something other than what they are named. for _, name := range []string{table + "_personal_key", table + "_org_key"} { def := got[name] if !strings.Contains(def, "UNIQUE") { t.Errorf("%s is not UNIQUE: %s", name, def) } if !strings.Contains(def, "WHERE") { t.Errorf("%s is not partial: %s", name, def) } } } // created_by is deliberately unindexed: attribution only, no listing is // keyed by it, and its SET NULL scan happens only when a user is deleted. for _, table := range []string{agents, skills} { var n int if err := f.pool.QueryRow(f.ctx, `SELECT count(*)::int FROM pg_indexes WHERE schemaname='public' AND tablename=$1 AND indexdef LIKE '%(created_by)%'`, table).Scan(&n); err != nil { t.Fatalf("look for a created_by index: %v", err) } if n != 0 { t.Errorf("%s has an index on created_by; it was deliberately omitted", table) } } } /* ── Reversibility ──────────────────────────────────────────────────────── */ func TestMigration000005IsReversible(t *testing.T) { ctx := context.Background() pool := testutil.Sandbox(t, "defs_reversible") testutil.ApplyAllMigrations(ctx, t, pool) const up = "000005_agent_skill_definitions.up.sql" const down = "000005_agent_skill_definitions.down.sql" exists := func(name string) bool { var reg *string if err := pool.QueryRow(ctx, `SELECT to_regclass('public.' || $1)::text`, name).Scan(®); err != nil { t.Fatalf("to_regclass(%s): %v", name, err) } return reg != nil } for _, table := range []string{agents, skills} { if !exists(table) { t.Fatalf("%s does not exist before the rollback", table) } } if err := testutil.ApplyMigration(ctx, t, pool, down); err != nil { t.Fatalf("apply %s: %v", down, err) } for _, table := range []string{agents, skills} { if exists(table) { t.Errorf("%s survived the rollback", table) } } // The rollback must reach nothing that predates it. for _, table := range []string{"users", "organizations", "sessions", "user_preferences", "job_postings"} { if !exists(table) { t.Fatalf("the rollback dropped %s, which 000005 did not create", table) } } // And no enum type was created, so none can be left behind. var leftover int if err := pool.QueryRow(ctx, `SELECT count(*)::int FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname='public' AND t.typtype='e' AND t.typname IN ('definition_visibility','agent_status','skill_status')`).Scan(&leftover); err != nil { t.Fatalf("look for leftover types: %v", err) } if leftover != 0 { t.Errorf("%d enum types left behind by the rollback", leftover) } // Re-applying restores exactly what was removed. if err := testutil.ApplyMigration(ctx, t, pool, up); err != nil { t.Fatalf("re-apply %s: %v", up, err) } for _, table := range []string{agents, skills} { if !exists(table) { t.Errorf("%s did not come back", table) } } } // Every migration has a matching down file, and the set is what we think it is. // // The list is written out rather than counted. A migration is the one kind of // change that cannot be undone by editing a file, so adding one should require // naming it here — a bare count would let a stray file slip in by incrementing // a number, which is exactly the review nobody performs. func TestMigrationPairsAreComplete(t *testing.T) { ups := testutil.MigrationFiles(t, ".up.sql") downs := testutil.MigrationFiles(t, ".down.sql") if len(ups) != len(downs) { t.Fatalf("%d up and %d down migrations", len(ups), len(downs)) } for i, up := range ups { want := strings.TrimSuffix(up, ".up.sql") + ".down.sql" if downs[i] != want { t.Errorf("%s has no matching down migration (found %s)", up, downs[i]) } } want := []string{ "000001_initial_schema.up.sql", "000002_application_interview_id.up.sql", "000003_drop_screened_consistent_check.up.sql", "000004_auth_sessions.up.sql", "000005_agent_skill_definitions.up.sql", "000006_agent_runs.up.sql", "000007_agent_confirmations.up.sql", "000008_knowledge.up.sql", "000009_confirmation_replay.up.sql", "000010_definition_versions.up.sql", "000011_employee_roles.up.sql", // Phase 3: the OAuth 2.1 authorization server behind the MCP surface. // Three tables, added together because they are one feature: a client // registers, is issued a code, and exchanges it for tokens. "000012_oauth_clients.up.sql", "000013_oauth_grants.up.sql", "000014_oauth_tokens.up.sql", // Phase 5: shared rate limit counters, so a limit means the same thing // behind one instance and behind ten. "000015_rate_limits.up.sql", } if len(ups) != len(want) { t.Fatalf("%d migrations, want %d — update this list deliberately", len(ups), len(want)) } for i, name := range want { if ups[i] != name { t.Errorf("migration %d is %s, want %s", i+1, ups[i], name) } } } // The tables that exist, counted, plus the ones that deliberately do not. // // The Phase 4B decision was explicit about which tables must NOT appear, and // that half of this test is the durable half — the forbidden list below is a // design decision, not a snapshot. The count is the snapshot, and it is here so // that a table arriving without a decision behind it fails somewhere. func TestMigrationsAddOnlyTheTablesWeDecidedOn(t *testing.T) { f := newFixture(t, "defs_tablecount") var n int if err := f.pool.QueryRow(f.ctx, `SELECT count(*)::int FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'`).Scan(&n); err != nil { t.Fatalf("count tables: %v", err) } // 17 from 000001, + auth_sessions (000004), + agent_definitions and // skill_definitions (000005), + agent_runs (000006), + agent_confirmations // (000007), + knowledge_documents and knowledge_chunks (000008), // + definition_versions (000010), + employee_roles (000011), // + oauth_clients (000012), + oauth_grants (000013), + oauth_tokens // (000014), + rate_limits (000015). // schema_migrations is golang-migrate's and is absent when the files are // applied directly. if n != 30 { t.Errorf("%d base tables after every migration, want 30", n) } // The three OAuth tables, named rather than merely counted. The count // above catches a table arriving without a decision; this catches one of // these three going missing, which the count alone would not if another // arrived in the same change. for _, required := range []string{"oauth_clients", "oauth_grants", "oauth_tokens", "rate_limits"} { var reg *string if err := f.pool.QueryRow(f.ctx, `SELECT to_regclass('public.' || $1)::text`, required).Scan(®); err != nil { t.Fatalf("check %s: %v", required, err) } if reg == nil { t.Errorf("%s is missing; the MCP OAuth surface cannot work without it", required) } } // `definition_versions` was on this list, deferred by the Phase 4B decision. // It is built now — §3's "specs are immutable once published" needs it, and // a run recording an agent_version that resolves to nothing is a record // nobody can explain. Removed from the list deliberately rather than // silently, which is the whole reason the list is written out. for _, forbidden := range []string{ "definition_permissions", "agent_skills", "agent_subagents", "agent_knowledge", "conversations", "conversation_messages", "conversation_feedback", } { var reg *string if err := f.pool.QueryRow(f.ctx, `SELECT to_regclass('public.' || $1)::text`, forbidden).Scan(®); err != nil { t.Fatalf("to_regclass: %v", err) } if reg != nil { t.Errorf("table %s exists; Phase 4B deferred or rejected it", forbidden) } } }