Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,14 @@ type ResourceGetResp = { ok: boolean; item: any }
// telemetry. Gating the fetch to these three types removes the noise
// without changing behaviour for db/redis/mongo (the catch below still
// guards genuine permission-hidden cases).
const CREDENTIALED_RESOURCE_TYPES: ReadonlySet<ResourceType> = new Set<ResourceType>([
export const CREDENTIALED_RESOURCE_TYPES: ReadonlySet<ResourceType> = new Set<ResourceType>([
'postgres',
// 'vector' is wire-distinct from 'postgres' but uses the same Postgres
// credentials shape — `/api/v1/resources/:id/credentials` returns a
// working postgres:// URL. Without this entry, opening a vector
// resource's detail page never fetches the connection_url and the
// "Connection string" panel renders empty.
'vector',
'redis',
'mongodb',
])
Expand Down
31 changes: 23 additions & 8 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,29 @@ export type Tier = 'anonymous' | 'free' | 'hobby' | 'hobby_plus' | 'pro' | 'team
export type Role = 'owner' | 'admin' | 'developer' | 'viewer' | 'member'
export type Env = 'production' | 'staging' | 'development' | string

export type ResourceType =
| 'postgres'
| 'redis'
| 'mongodb'
| 'queue'
| 'storage'
| 'webhook'
| 'deploy'
// RESOURCE_TYPES — runtime-iterable registry of every wire resource_type
// the dashboard can receive on /api/v1/resources. Tests iterate this to
// guarantee that every type has a ResourceIcon entry and an explicit
// CREDENTIALED_RESOURCE_TYPES decision, so a future addition (e.g. a new
// /something/new endpoint) cannot silently render an empty icon class or
// drop the credentials fetch on the resource detail page.
//
// `vector` was added when POST /vector/new shipped on 2026-05-20 — it
// provisions a real Postgres with the pgvector extension installed, so on
// the wire it's a distinct resource_type even though the credentials
// shape is identical to plain postgres.
export const RESOURCE_TYPES = [
'postgres',
'vector',
'redis',
'mongodb',
'queue',
'storage',
'webhook',
'deploy',
] as const

export type ResourceType = (typeof RESOURCE_TYPES)[number]

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

Expand Down
61 changes: 60 additions & 1 deletion src/components/Common.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
*/

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

describe('displayName / isUnnamed — mandatory-name fallback', () => {
it('returns the name verbatim when present', () => {
Expand Down Expand Up @@ -41,6 +44,62 @@ describe('displayName / isUnnamed — mandatory-name fallback', () => {
})
})

// Registry-iterating regression: every wire `resource_type` must have a
// real icon class on ResourceIcon AND an explicit decision (in or out) in
// CREDENTIALED_RESOURCE_TYPES. The bug we're guarding: when /vector/new
// shipped on 2026-05-20, the dashboard's typed surface forgot to add
// 'vector' to ResourceType — the icon rendered as `undefined res-name-ico`
// and the detail page silently skipped the credentials fetch, so vector
// users could never see their connection_url. Iterating the registry (vs
// hand-typing a list of cases) means a future POST /foo/new added to
// RESOURCE_TYPES auto-fails the icon test until the map is updated.
describe('ResourceIcon — every ResourceType has a non-empty icon class', () => {
for (const type of RESOURCE_TYPES) {
it(`renders a real ico-* class for resource_type="${type}"`, () => {
const { container } = render(<ResourceIcon type={type} />)
const span = container.querySelector('span')
expect(span).not.toBeNull()
const cls = span?.getAttribute('class') ?? ''
// Must include a registered ico-* prefix — `undefined` slips
// through when the map entry is missing, so guard explicitly.
expect(cls).not.toContain('undefined')
expect(/\bico-[a-z]{2,}\b/.test(cls)).toBe(true)
})
}
})

describe('CREDENTIALED_RESOURCE_TYPES — coverage check', () => {
it('every wire resource_type has an explicit in/out decision', () => {
// The set is small enough that an inverted list is the readable form.
// If a new ResourceType is added, this test forces the author to make
// an explicit decision: either add it to CREDENTIALED_RESOURCE_TYPES
// (db-shaped resources where /credentials returns connection_url) or
// update NON_CREDENTIALED below (webhook/storage/queue/deploy where
// /credentials 400s — see BugBash P3-02).
const NON_CREDENTIALED: ReadonlySet<string> = new Set([
'queue',
'storage',
'webhook',
'deploy',
])
for (const type of RESOURCE_TYPES) {
const isCredentialed = CREDENTIALED_RESOURCE_TYPES.has(type)
const isNonCredentialed = NON_CREDENTIALED.has(type)
expect(
isCredentialed !== isNonCredentialed,
`resource_type="${type}" must appear in exactly one of CREDENTIALED_RESOURCE_TYPES or the test's NON_CREDENTIALED list`,
).toBe(true)
}
})

it('vector is wired into the credentialed set (regression guard for 2026-05-30)', () => {
// Standalone marker: the bug surface was specifically vector being
// dropped. Leaving an explicit assertion makes a future revert
// surface in the test name, not just a count change.
expect(CREDENTIALED_RESOURCE_TYPES.has('vector')).toBe(true)
})
})

describe('copyToClipboard', () => {
let originalClipboardDescriptor: PropertyDescriptor | undefined
let originalExecCommand: typeof document.execCommand
Expand Down
6 changes: 6 additions & 0 deletions src/components/Common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,14 @@ export function ScopePill({ scope }: { scope: 'read' | 'write' | 'agent' }) {

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