Skip to content

Commit 900e780

Browse files
fix(resources): add 'vector' to ResourceType — fixes missing connection_url + broken icon (#154)
When POST /vector/new shipped 2026-05-20, the dashboard's typed surface was not updated alongside it (rule 22 — contract changes touch all surfaces). Wire effect: a vector resource (pgvector-enabled postgres) came back with resource_type="vector" and the dashboard: 1. Skipped the GET /resources/:id/credentials fetch on the detail page (vector was missing from CREDENTIALED_RESOURCE_TYPES), so users provisioning a vector resource could never see their postgres:// connection URL through the dashboard. 2. Rendered the ResourceIcon as `undefined res-name-ico` — the icon class lookup returned undefined for the missing union member. Fix: - Add 'vector' to ResourceType (now exported as RESOURCE_TYPES const so tests can iterate the registry). - Add 'vector' to CREDENTIALED_RESOURCE_TYPES (and export it for the coverage test). - Add 'vector' to ResourceIcon map (reuses ico-pg — pgvector is a Postgres extension; wire distinction stays for audit/scan splits). - Registry-iterating regression tests: every RESOURCE_TYPES member must (a) yield a real ico-* class on ResourceIcon and (b) appear in exactly one of CREDENTIALED_RESOURCE_TYPES or the test's NON_CREDENTIALED list. A future POST /something/new addition will auto-fail until both decisions are wired. Local gate: `npm run build` (tsc + vite build + prerender) + `npm test` both green. 1101 passing tests (added 10).
1 parent 9e674bc commit 900e780

4 files changed

Lines changed: 96 additions & 10 deletions

File tree

src/api/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,8 +622,14 @@ type ResourceGetResp = { ok: boolean; item: any }
622622
// telemetry. Gating the fetch to these three types removes the noise
623623
// without changing behaviour for db/redis/mongo (the catch below still
624624
// guards genuine permission-hidden cases).
625-
const CREDENTIALED_RESOURCE_TYPES: ReadonlySet<ResourceType> = new Set<ResourceType>([
625+
export const CREDENTIALED_RESOURCE_TYPES: ReadonlySet<ResourceType> = new Set<ResourceType>([
626626
'postgres',
627+
// 'vector' is wire-distinct from 'postgres' but uses the same Postgres
628+
// credentials shape — `/api/v1/resources/:id/credentials` returns a
629+
// working postgres:// URL. Without this entry, opening a vector
630+
// resource's detail page never fetches the connection_url and the
631+
// "Connection string" panel renders empty.
632+
'vector',
627633
'redis',
628634
'mongodb',
629635
])

src/api/types.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,29 @@ export type Tier = 'anonymous' | 'free' | 'hobby' | 'hobby_plus' | 'pro' | 'team
1111
export type Role = 'owner' | 'admin' | 'developer' | 'viewer' | 'member'
1212
export type Env = 'production' | 'staging' | 'development' | string
1313

14-
export type ResourceType =
15-
| 'postgres'
16-
| 'redis'
17-
| 'mongodb'
18-
| 'queue'
19-
| 'storage'
20-
| 'webhook'
21-
| 'deploy'
14+
// RESOURCE_TYPES — runtime-iterable registry of every wire resource_type
15+
// the dashboard can receive on /api/v1/resources. Tests iterate this to
16+
// guarantee that every type has a ResourceIcon entry and an explicit
17+
// CREDENTIALED_RESOURCE_TYPES decision, so a future addition (e.g. a new
18+
// /something/new endpoint) cannot silently render an empty icon class or
19+
// drop the credentials fetch on the resource detail page.
20+
//
21+
// `vector` was added when POST /vector/new shipped on 2026-05-20 — it
22+
// provisions a real Postgres with the pgvector extension installed, so on
23+
// the wire it's a distinct resource_type even though the credentials
24+
// shape is identical to plain postgres.
25+
export const RESOURCE_TYPES = [
26+
'postgres',
27+
'vector',
28+
'redis',
29+
'mongodb',
30+
'queue',
31+
'storage',
32+
'webhook',
33+
'deploy',
34+
] as const
35+
36+
export type ResourceType = (typeof RESOURCE_TYPES)[number]
2237

2338
export type ResourceStatus = 'active' | 'paused' | 'expired' | 'tombstoned' | 'deleted' | 'reaped'
2439

src/components/Common.test.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
*/
99

1010
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
11-
import { copyToClipboard, displayName, isUnnamed } from './Common'
11+
import { render } from '@testing-library/react'
12+
import { copyToClipboard, displayName, isUnnamed, ResourceIcon } from './Common'
13+
import { RESOURCE_TYPES } from '../api/types'
14+
import { CREDENTIALED_RESOURCE_TYPES } from '../api'
1215

1316
describe('displayName / isUnnamed — mandatory-name fallback', () => {
1417
it('returns the name verbatim when present', () => {
@@ -41,6 +44,62 @@ describe('displayName / isUnnamed — mandatory-name fallback', () => {
4144
})
4245
})
4346

47+
// Registry-iterating regression: every wire `resource_type` must have a
48+
// real icon class on ResourceIcon AND an explicit decision (in or out) in
49+
// CREDENTIALED_RESOURCE_TYPES. The bug we're guarding: when /vector/new
50+
// shipped on 2026-05-20, the dashboard's typed surface forgot to add
51+
// 'vector' to ResourceType — the icon rendered as `undefined res-name-ico`
52+
// and the detail page silently skipped the credentials fetch, so vector
53+
// users could never see their connection_url. Iterating the registry (vs
54+
// hand-typing a list of cases) means a future POST /foo/new added to
55+
// RESOURCE_TYPES auto-fails the icon test until the map is updated.
56+
describe('ResourceIcon — every ResourceType has a non-empty icon class', () => {
57+
for (const type of RESOURCE_TYPES) {
58+
it(`renders a real ico-* class for resource_type="${type}"`, () => {
59+
const { container } = render(<ResourceIcon type={type} />)
60+
const span = container.querySelector('span')
61+
expect(span).not.toBeNull()
62+
const cls = span?.getAttribute('class') ?? ''
63+
// Must include a registered ico-* prefix — `undefined` slips
64+
// through when the map entry is missing, so guard explicitly.
65+
expect(cls).not.toContain('undefined')
66+
expect(/\bico-[a-z]{2,}\b/.test(cls)).toBe(true)
67+
})
68+
}
69+
})
70+
71+
describe('CREDENTIALED_RESOURCE_TYPES — coverage check', () => {
72+
it('every wire resource_type has an explicit in/out decision', () => {
73+
// The set is small enough that an inverted list is the readable form.
74+
// If a new ResourceType is added, this test forces the author to make
75+
// an explicit decision: either add it to CREDENTIALED_RESOURCE_TYPES
76+
// (db-shaped resources where /credentials returns connection_url) or
77+
// update NON_CREDENTIALED below (webhook/storage/queue/deploy where
78+
// /credentials 400s — see BugBash P3-02).
79+
const NON_CREDENTIALED: ReadonlySet<string> = new Set([
80+
'queue',
81+
'storage',
82+
'webhook',
83+
'deploy',
84+
])
85+
for (const type of RESOURCE_TYPES) {
86+
const isCredentialed = CREDENTIALED_RESOURCE_TYPES.has(type)
87+
const isNonCredentialed = NON_CREDENTIALED.has(type)
88+
expect(
89+
isCredentialed !== isNonCredentialed,
90+
`resource_type="${type}" must appear in exactly one of CREDENTIALED_RESOURCE_TYPES or the test's NON_CREDENTIALED list`,
91+
).toBe(true)
92+
}
93+
})
94+
95+
it('vector is wired into the credentialed set (regression guard for 2026-05-30)', () => {
96+
// Standalone marker: the bug surface was specifically vector being
97+
// dropped. Leaving an explicit assertion makes a future revert
98+
// surface in the test name, not just a count change.
99+
expect(CREDENTIALED_RESOURCE_TYPES.has('vector')).toBe(true)
100+
})
101+
})
102+
44103
describe('copyToClipboard', () => {
45104
let originalClipboardDescriptor: PropertyDescriptor | undefined
46105
let originalExecCommand: typeof document.execCommand

src/components/Common.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,14 @@ export function ScopePill({ scope }: { scope: 'read' | 'write' | 'agent' }) {
136136

137137
// ------------- icons -------------
138138
export function ResourceIcon({ type, size = 22 }: { type: ResourceType; size?: number }) {
139+
// Exhaustive map over ResourceType — TS will block a future addition to
140+
// the union that forgets to wire an icon class here. `vector` reuses the
141+
// Postgres glyph because pgvector is a Postgres extension; the wire
142+
// distinction is preserved upstream so audit + storage scans can split
143+
// it, but visually it belongs to the Postgres family.
139144
const map: Record<ResourceType, string> = {
140145
postgres: 'ico-pg',
146+
vector: 'ico-pg',
141147
redis: 'ico-rd',
142148
mongodb: 'ico-mg',
143149
queue: 'ico-qu',

0 commit comments

Comments
 (0)