Skip to content

Commit 87cc346

Browse files
committed
fix(tests): address review — shared disposable-DB guard, optional Redis suite, restored client-info wire contract
1 parent 1464adb commit 87cc346

28 files changed

Lines changed: 296 additions & 408 deletions

‎.agents/skills/cleanup/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Otherwise apply the surviving changes yourself (in the main context, not delegat
4444

4545
For each pass in turn, apply all of that pass's changes, then move to the next pass. A file touched by several passes is therefore edited once per pass, in this order — not once as a merged patch. This is what makes the ordering real: a single merged-per-file patch would collapse all passes into one edit and lose it.
4646

47-
Comments apply last, on purpose: that pass operates on whatever the earlier structural passes settled the code into, so it never edits lines a sibling pass is about to delete or rewrite.
47+
Comments apply after every structural pass, on purpose: that pass operates on whatever the earlier passes settled the code into, so it never edits lines a sibling pass is about to delete or rewrite. Tests apply last because they only touch test files; in Step 2, drop any other pass's proposal on a test file the tests pass deletes.
4848

4949
**Treat every Step 1 proposal as snapshot-relative, not authoritative.** All passes analyzed the *original* files in parallel, so a proposal's line ranges and before/after text describe the code as it was *before* any edits — once an earlier pass has run, a later pass's snippet may no longer match. So for each change, before applying:
5050

‎.agents/skills/ship/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ When the user runs `/ship`:
3030
- Types: `fix`, `feat`, `improvement`, `chore`
3131
- Scope: short identifier (e.g., `undo-redo`, `api`, `ui`)
3232
- Keep it concise
33-
4. **Run the cleanup pass** — only if the diff modifies UI code (any `.tsx` file, or anything under `apps/sim/components/`, `apps/sim/hooks/`, or `apps/sim/stores/`): `/cleanup`
34-
- `/cleanup` fans out the React/UI passes (effects, memo, callbacks, state, React Query, emcn, url-state) plus the comment pass; skip it when no UI was touched. When it runs, it applies fixes so they land in this commit.
35-
- If the diff adds or changes tests (`*.test.ts(x)`, `*.integration.ts`, `e2e/**`) and `/cleanup` did not run, run `/test-audit audit <changed test files>` on its own. Every new or changed test must pass the authoring gate; delete the ones that don't rather than shipping them.
33+
4. **Run the cleanup and test gates**
34+
- If the diff modifies UI code (any non-test `.tsx` file, or anything under `apps/sim/components/`, `apps/sim/hooks/`, or `apps/sim/stores/`), run `/cleanup`. It fans out the React/UI passes (effects, memo, callbacks, state, React Query, emcn, url-state), the comment pass, and the test-audit pass, and applies fixes so they land in this commit.
35+
- Otherwise, if the diff adds or changes tests (`*.test.ts(x)`, `*.integration.ts`, `e2e/**`), run `/test-audit audit <changed test files>` on its own. Every new or changed test must pass the authoring gate; delete the ones that don't rather than shipping them.
3636
5. **Run migration safety** — only if the diff touches `packages/db/migrations/**` or `packages/db/schema.ts`:
3737
- Run `/db-migrate` to review the migration for zero-downtime safety (expand/contract phasing, backward-compatibility with the deployed app version).
3838
- `bun run check:migrations origin/staging` must pass (staging is the PR base). Do not silence a flagged statement with a `-- migration-safe:` annotation unless `/db-migrate` confirmed the old code no longer depends on it; otherwise split the destructive change into a later deploy.

‎.agents/skills/test-audit/SKILL.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ is not a deletion reason.
9999

100100
Keep discovery read-only and report evidence before editing. Before judging a candidate, read the
101101
complete test and its production owner, callers, sibling implementations, overlapping tests, CI
102-
routing (`.github/workflows/test-build.yml` names DB suites by path), and relevant history
102+
routing (CI discovers `*.integration.ts` by glob; `.github/workflows/*.yml` names a few scripts and
103+
files by path), and relevant history
103104
(`git log --format='%h %s' -5 -- <file>`).
104105

105106
Record for every deletion candidate: the test and location; the failure it can actually detect;
@@ -118,7 +119,7 @@ that restate the same implementation.
118119
For a whole subsystem or the whole repo:
119120

120121
1. Partition test files into lanes of ~150–350 files by owning directory, and list the protected
121-
set (every `*.integration.ts`, `*.postgres.test.ts`, `__integration__/**`, `apps/desktop/e2e/**`,
122+
set (every `*.integration.ts`, `*.live.test.ts`, `__integration__/**`, `apps/desktop/e2e/**`,
122123
and every path named in `.github/workflows/*.yml`).
123124
2. Give each lane its own git worktree and branch (`git worktree add -b <branch> <path> <base>`,
124125
then `bun install --frozen-lockfile` inside it). Lanes never share a checkout, never symlink

‎apps/sim/background/cleanup-table-row-ttl.integration.ts‎

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { writeFileSync } from 'node:fs'
77
import { tmpdir } from 'node:os'
88
import { join } from 'node:path'
9+
import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
910
import { createDeferred } from '@sim/testing'
1011
import { sleep } from '@sim/utils/helpers'
1112
import { generateId } from '@sim/utils/id'
@@ -34,25 +35,14 @@ import type { TableSchema } from '@/lib/table/types'
3435
import { checkBatchUniqueConstraintsDb, coerceRowToSchema } from '@/lib/table/validation'
3536
import { runCleanupTableRowTtl } from '@/background/cleanup-table-row-ttl'
3637

37-
const url = process.env.TEST_DATABASE_URL
38-
if (url) {
39-
const parsed = new URL(url)
40-
const otherDatabase = Object.entries(process.env).some(
41-
([key, value]) => /^DATABASE_(URL|REPLICA_URL)(_|$)/.test(key) && value && value !== url
42-
)
43-
if (
44-
!['127.0.0.1', 'localhost'].includes(parsed.hostname) ||
45-
!/(^|_)test(_|$)/.test(parsed.pathname.slice(1)) ||
46-
process.env.DATABASE_URL !== url ||
47-
otherDatabase
48-
) {
49-
throw new Error('This suite requires only the disposable local test database')
50-
}
38+
const url = readTestDatabaseUrl()
39+
const otherDatabase = Object.entries(process.env).some(
40+
([key, value]) => /^DATABASE_(URL|REPLICA_URL)(_|$)/.test(key) && value && value !== url
41+
)
42+
if (process.env.DATABASE_URL !== url || otherDatabase) {
43+
throw new Error('This suite requires only the disposable local test database')
5144
}
52-
const control = postgres(url ?? 'postgres://localhost/disabled_expiration_test', {
53-
max: 4,
54-
onnotice: () => {},
55-
})
45+
const control = postgres(url, { max: 4, onnotice: () => {} })
5646
const workspaceId = generateId()
5747
const userId = generateId()
5848
const expired = '2020-01-01T00:00:00Z'
@@ -113,6 +103,8 @@ async function waitForSleepingDelete(): Promise<number> {
113103
const [{ migrated }] = await control<{ migrated: boolean }[]>`SELECT EXISTS (
114104
SELECT 1 FROM pg_trigger WHERE tgname = 'user_table_rows_insert_stmt_trigger'
115105
) AS migrated`
106+
/** The suite's own `afterAll` never runs when it is skipped, so release the probe connection here. */
107+
if (!migrated) await control.end()
116108

117109
describe.skipIf(!migrated)('Expiration with real PostgreSQL transactions', () => {
118110
beforeAll(async () => {

‎apps/sim/ee/access-requests/lib/application/flow.integration.ts‎

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AuditAction, recordAudit } from '@sim/audit'
22
import type { SessionPrincipal } from '@sim/auth/principal'
33
import * as schema from '@sim/db/schema'
4+
import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
45
import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing'
56
import { generateId } from '@sim/utils/id'
67
import { eq } from 'drizzle-orm'
@@ -9,18 +10,7 @@ import postgres from 'postgres'
910
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
1011
import type { DbOrTx } from '@/lib/db/types'
1112

12-
const { databaseUrl, select, transaction } = vi.hoisted(() => {
13-
const databaseUrl = process.env.TEST_DATABASE_URL
14-
if (databaseUrl) {
15-
const url = new URL(databaseUrl)
16-
if (
17-
!['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) ||
18-
!/(^|_)test(_|$)/.test(url.pathname.slice(1))
19-
)
20-
throw new Error('Use a disposable local test database')
21-
}
22-
return { databaseUrl, select: vi.fn(), transaction: vi.fn() }
23-
})
13+
const { select, transaction } = vi.hoisted(() => ({ select: vi.fn(), transaction: vi.fn() }))
2414
vi.mock('@/lib/core/config/env-flags', async () => (await import('@sim/testing')).envFlagsMock)
2515
vi.mock('@sim/db', async () => ({
2616
...(await import('@sim/db/schema')),
@@ -54,19 +44,17 @@ import {
5444
} from '@/ee/access-requests/lib/notification-events'
5545

5646
const schemaName = `access_flow_${generateId().replaceAll('-', '')}`
57-
const connection = databaseUrl
58-
? postgres(databaseUrl, {
59-
max: 3,
60-
prepare: false,
61-
connection: {
62-
search_path: schemaName,
63-
application_name: schemaName,
64-
statement_timeout: 5000,
65-
},
66-
onnotice: () => undefined,
67-
})
68-
: undefined
69-
const database = connection ? (drizzle(connection, { schema }) as DbOrTx) : undefined
47+
const connection = postgres(readTestDatabaseUrl(), {
48+
max: 3,
49+
prepare: false,
50+
connection: {
51+
search_path: schemaName,
52+
application_name: schemaName,
53+
statement_timeout: 5000,
54+
},
55+
onnotice: () => undefined,
56+
})
57+
const database = drizzle(connection, { schema }) as DbOrTx
7058
const session = (userId: string): SessionPrincipal => ({
7159
kind: 'session',
7260
userId,
@@ -79,7 +67,6 @@ const target = { kind: 'feature', configKey: 'hideTablesTab' } as const
7967
const page = { limit: 10, offset: 0 }
8068

8169
beforeAll(async () => {
82-
if (!connection) return
8370
await connection.unsafe(`CREATE SCHEMA "${schemaName}"`)
8471
await connection.unsafe(`
8572
CREATE TABLE "user" (
@@ -158,12 +145,11 @@ beforeAll(async () => {
158145
created_at timestamp DEFAULT now(), processed_at timestamp
159146
);
160147
`)
161-
select.mockImplementation((fields) => database!.select(fields))
162-
transaction.mockImplementation((callback) => database!.transaction(callback))
148+
select.mockImplementation((fields) => database.select(fields))
149+
transaction.mockImplementation((callback) => database.transaction(callback))
163150
})
164151

165152
beforeEach(async () => {
166-
if (!connection) return
167153
vi.mocked(recordAudit).mockClear()
168154
setEnvFlags({ isHosted: true, isBillingEnabled: true, isAccessControlEnabled: true })
169155
await connection.unsafe(`
@@ -205,7 +191,6 @@ beforeEach(async () => {
205191

206192
afterAll(async () => {
207193
resetEnvFlagsMock()
208-
if (!connection) return
209194
try {
210195
await connection.unsafe(`DROP SCHEMA "${schemaName}" CASCADE`)
211196
} finally {
@@ -235,7 +220,7 @@ function apply(requestId: string, expectedFingerprint: string) {
235220
}
236221

237222
async function authorizeTables(principal = member, workspaceId = 'primary') {
238-
const workspace = await getWorkspaceWithOwner(workspaceId, { executor: database! })
223+
const workspace = await getWorkspaceWithOwner(workspaceId, { executor: database })
239224
if (!workspace) throw new Error('Missing fixture workspace')
240225
return authorizeWorkspaceOperation(
241226
principal,
@@ -245,26 +230,26 @@ async function authorizeTables(principal = member, workspaceId = 'primary') {
245230
workspaceOrganizationId: workspace.organizationId,
246231
allowPersonalApiKeys: workspace.allowPersonalApiKeys,
247232
},
248-
{ executor: database! }
233+
{ executor: database }
249234
)
250235
}
251236

252237
async function storedState() {
253-
const [group] = await database!
238+
const [group] = await database
254239
.select({ config: schema.permissionGroup.config })
255240
.from(schema.permissionGroup)
256241
.where(eq(schema.permissionGroup.id, 'restricted'))
257242
return {
258243
config: group.config,
259-
requests: await connection!`
244+
requests: await connection`
260245
SELECT id, status, decision, decision_reason, decided_by, decided_at, updated_at
261246
FROM permission_access_request ORDER BY id
262247
`,
263-
memberLimits: await connection!`
248+
memberLimits: await connection`
264249
SELECT organization_id, user_id, usage_limit, set_by, updated_at
265250
FROM organization_member_usage_limit ORDER BY organization_id, user_id
266251
`,
267-
events: await connection!`SELECT event_type FROM outbox_event ORDER BY event_type`,
252+
events: await connection`SELECT event_type FROM outbox_event ORDER BY event_type`,
268253
audit: vi.mocked(recordAudit).mock.calls.map(([entry]) => ({
269254
action: entry.action,
270255
actorId: entry.actorId,
@@ -276,7 +261,7 @@ async function storedState() {
276261
}
277262
}
278263

279-
describe.skipIf(!databaseUrl)('access request member-to-admin flow on PostgreSQL', () => {
264+
describe('access request member-to-admin flow on PostgreSQL', () => {
280265
it('refuses workspace API keys before any protected read or transaction', async () => {
281266
const readsBefore = select.mock.calls.length
282267
const transactionsBefore = transaction.mock.calls.length
@@ -342,7 +327,7 @@ describe.skipIf(!databaseUrl)('access request member-to-admin flow on PostgreSQL
342327
'member',
343328
'primary',
344329
'org',
345-
database!
330+
database
346331
)
347332
expect(effective).toMatchObject({
348333
entitled: true,
@@ -369,7 +354,7 @@ describe.skipIf(!databaseUrl)('access request member-to-admin flow on PostgreSQL
369354
})
370355

371356
it('fulfills an organization member credit-cap request without changing another member', async () => {
372-
await connection!`
357+
await connection`
373358
INSERT INTO organization_member_usage_limit (id, organization_id, user_id, usage_limit, set_by)
374359
VALUES ('member-cap', 'org', 'member', 10, 'admin'), ('peer-cap', 'org', 'peer', 7, 'admin')
375360
`
@@ -498,9 +483,9 @@ describe.skipIf(!databaseUrl)('access request member-to-admin flow on PostgreSQL
498483
const { request } = await create()
499484
const prepared = await preview(request.id)
500485
if (change === 'policy') {
501-
await connection!`UPDATE permission_group SET config = config || '{"disableTableExport":true}'::jsonb WHERE id = 'restricted'`
486+
await connection`UPDATE permission_group SET config = config || '{"disableTableExport":true}'::jsonb WHERE id = 'restricted'`
502487
} else {
503-
await connection!`DELETE FROM permissions WHERE id = 'peer-primary'`
488+
await connection`DELETE FROM permissions WHERE id = 'peer-primary'`
504489
}
505490
const persisted = await storedState()
506491
await expect(apply(request.id, prepared.fingerprint)).rejects.toMatchObject({
@@ -565,7 +550,7 @@ describe.skipIf(!databaseUrl)('access request member-to-admin flow on PostgreSQL
565550
input: { scope: { kind: 'organization', organizationId: 'org' }, ...page },
566551
})
567552
).rejects.toMatchObject({ code: 'not_found' })
568-
await connection!`DELETE FROM permissions WHERE id = 'external-primary'`
553+
await connection`DELETE FROM permissions WHERE id = 'external-primary'`
569554
const prepared = await preview(request.id)
570555
expect(prepared.canApply).toBe(false)
571556
await expect(apply(request.id, prepared.fingerprint)).rejects.toMatchObject({

‎apps/sim/ee/access-requests/lib/impact.integration.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
1+
import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
12
import { generateId } from '@sim/utils/id'
23
import { drizzle } from 'drizzle-orm/postgres-js'
34
import postgres from 'postgres'
45
import { describe, expect, it } from 'vitest'
56
import type { DbOrTx } from '@/lib/db/types'
67
import { loadAccessRequestGroupImpact } from '@/ee/access-requests/lib/impact'
78

8-
const databaseUrl = process.env.TEST_DATABASE_URL
9+
const databaseUrl = readTestDatabaseUrl()
910

1011
async function createFixture() {
11-
const url = new URL(databaseUrl ?? '')
12-
if (
13-
!['localhost', '127.0.0.1'].includes(url.hostname) ||
14-
!/(^|_)test(_|$)/.test(url.pathname.slice(1))
15-
)
16-
throw new Error('Use a disposable local test database')
1712
const schema = `access_impact_${generateId().replaceAll('-', '')}`
18-
const client = postgres(url.toString(), { max: 1, onnotice: () => undefined })
13+
const client = postgres(databaseUrl, { max: 1, onnotice: () => undefined })
1914
await client.unsafe(`CREATE SCHEMA "${schema}"`)
2015
await client.unsafe(`SET search_path TO "${schema}"`)
2116
await client.unsafe(`
@@ -45,7 +40,7 @@ async function createFixture() {
4540
}
4641
}
4742

48-
describe.skipIf(!databaseUrl)('access request impact on PostgreSQL', () => {
43+
describe('access request impact on PostgreSQL', () => {
4944
it('counts scoped people once without requiring or scanning the global user table', async () => {
5045
const fixture = await createFixture()
5146
try {

‎apps/sim/ee/access-requests/lib/repository.integration.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { permissionAccessRequest } from '@sim/db/schema'
2+
import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
23
import { generateId } from '@sim/utils/id'
34
import { and, eq } from 'drizzle-orm'
45
import { drizzle } from 'drizzle-orm/postgres-js'
@@ -8,17 +9,11 @@ import type { CursorKey } from '@/lib/api/list-query'
89
import type { DbOrTx } from '@/lib/db/types'
910
import { listAccessRequestRecords } from '@/ee/access-requests/lib/repository'
1011

11-
const databaseUrl = process.env.TEST_DATABASE_URL
12+
const databaseUrl = readTestDatabaseUrl()
1213

1314
async function createFixture() {
14-
const url = new URL(databaseUrl ?? '')
15-
if (
16-
!['localhost', '127.0.0.1'].includes(url.hostname) ||
17-
!/(^|_)test(_|$)/.test(url.pathname.slice(1))
18-
)
19-
throw new Error('Use a disposable local test database')
2015
const schema = `access_search_${generateId().replaceAll('-', '')}`
21-
const client = postgres(url.toString(), { max: 1, onnotice: () => undefined })
16+
const client = postgres(databaseUrl, { max: 1, onnotice: () => undefined })
2217
await client.unsafe(`CREATE SCHEMA "${schema}"`)
2318
await client.unsafe(`SET search_path TO "${schema}"`)
2419
await client.unsafe(`
@@ -52,7 +47,7 @@ async function createFixture() {
5247
}
5348
}
5449

55-
describe.skipIf(!databaseUrl)('organization request search on PostgreSQL', () => {
50+
describe('organization request search on PostgreSQL', () => {
5651
let fixture: Awaited<ReturnType<typeof createFixture>>
5752
beforeAll(async () => {
5853
fixture = await createFixture()

‎apps/sim/lib/billing/calculations/usage-reservation.integration.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/**
2-
* Pooled reservations against real Redis Lua scripts. Requires `TEST_REDIS_URL`; the hosted
2+
* Pooled reservations against real Redis Lua scripts. Skipped without `TEST_REDIS_URL`; the hosted
33
* billing flags and the Redis client accessor are the only fixtures.
44
*/
5+
import { readTestRedisUrl } from '@sim/db/testing/test-infrastructure'
56
import { envFlagsMock, setEnvFlags } from '@sim/testing/mocks/env-flags.mock'
67
import { redisConfigMock, redisConfigMockFns } from '@sim/testing/mocks/redis-config.mock'
78
import { generateId } from '@sim/utils/id'
@@ -16,16 +17,15 @@ import {
1617
vi.mock('@/lib/core/config/env-flags', () => envFlagsMock)
1718
vi.mock('@/lib/core/config/redis', () => redisConfigMock)
1819

19-
const redisUrl = process.env.TEST_REDIS_URL
20-
if (!redisUrl) throw new Error('Set TEST_REDIS_URL to a disposable local Redis')
20+
const redisUrl = readTestRedisUrl()
2121

22-
describe('pooled usage reservations with Redis', () => {
22+
describe.runIf(Boolean(redisUrl))('pooled usage reservations with Redis', () => {
2323
let redis: Redis
2424
const reservations: string[] = []
2525
const payer = { type: 'organization' as const, id: generateId() }
2626

2727
beforeAll(async () => {
28-
redis = new Redis(redisUrl, { lazyConnect: true, maxRetriesPerRequest: 0 })
28+
redis = new Redis(redisUrl!, { lazyConnect: true, maxRetriesPerRequest: 0 })
2929
await redis.connect()
3030
})
3131

‎apps/sim/lib/knowledge/__integration__/migration-fixture.ts‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import { readFile } from 'node:fs/promises'
2+
import { assertDisposableTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
23
import { generateId } from '@sim/utils/id'
34
import postgres from 'postgres'
45

56
/** Minimal pre-migration tables in an isolated schema; the entire migration runs unchanged. */
67
export async function createEnterpriseSearchMigrationFixture(databaseUrl: string) {
7-
const url = new URL(databaseUrl)
8-
if (
9-
!['localhost', '127.0.0.1'].includes(url.hostname) ||
10-
!/(^|_)test(_|$)/.test(url.pathname.slice(1))
11-
) {
12-
throw new Error('Search migration tests require a disposable local test database')
13-
}
8+
assertDisposableTestDatabaseUrl(databaseUrl)
149
const client = postgres(databaseUrl, { max: 1, fetch_types: false })
1510
const schemaName = `search_migration_${generateId().replaceAll('-', '')}`
1611
await client.unsafe(`CREATE SCHEMA "${schemaName}"`)

‎apps/sim/lib/knowledge/__integration__/reranker-admission.integration.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,6 @@ describe('reranker shared PostgreSQL admission', () => {
7171
await expect(rerank('Orion', items, options)).rejects.toMatchObject({
7272
name: 'ProviderAdmissionTimeoutError',
7373
})
74-
expect(upstream).toHaveBeenCalledTimes(2)
74+
expect(upstream).toHaveBeenCalledOnce()
7575
})
7676
})

0 commit comments

Comments
 (0)