diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c8d45674..e68dc719a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,7 +167,7 @@ jobs: - suite: integration path: tests/integration max_workers: 2 - test_timeout: 10000 + test_timeout: 20000 env: DATABASE_URL: postgres://junior:junior@localhost:5432/junior REDIS_URL: redis://localhost:6379 diff --git a/packages/junior/tests/component/artifacts.test.ts b/packages/junior/tests/component/artifacts.test.ts index 0071bfe320..48cdc88ddf 100644 --- a/packages/junior/tests/component/artifacts.test.ts +++ b/packages/junior/tests/component/artifacts.test.ts @@ -1,14 +1,11 @@ import { eq } from "drizzle-orm"; import { afterEach, describe, expect, it } from "vitest"; import type { AttachmentStorage } from "@/chat/attachments/storage"; -import { - publishImage, - unpublishArtifact, -} from "@/chat/artifacts/store"; +import { publishImage, unpublishArtifact } from "@/chat/artifacts/store"; import { publicArtifactGET } from "@/handlers/artifacts"; import { juniorArtifacts } from "@/db/schema"; import { - createLocalJuniorSqlFixture, + createJuniorSqlFixture, type LocalJuniorSqlFixture, } from "../fixtures/sql"; import { migrateSchema } from "@/chat/conversations/sql/migrations"; @@ -58,7 +55,7 @@ describe("public artifact route", () => { }); async function setup() { - fixture = await createLocalJuniorSqlFixture(); + fixture = await createJuniorSqlFixture(); await migrateSchema(fixture.sql); return fixture; } diff --git a/packages/junior/tests/component/briefs/conversation-brief-search.test.ts b/packages/junior/tests/component/briefs/conversation-brief-search.test.ts index f9c49d574e..f41dd93be9 100644 --- a/packages/junior/tests/component/briefs/conversation-brief-search.test.ts +++ b/packages/junior/tests/component/briefs/conversation-brief-search.test.ts @@ -7,7 +7,7 @@ import { createSqlStore } from "@/chat/conversations/sql/store"; import { createPluginAnnotations } from "@/chat/plugins/annotations"; import { juniorConversationBriefs, juniorConversations } from "@/db/schema"; import { conversationBriefFixture } from "../../fixtures/conversation-brief"; -import { createLocalJuniorSqlFixture } from "../../fixtures/sql"; +import { createJuniorSqlFixture } from "../../fixtures/sql"; const tenantScope = { kind: "public_provider_tenant" as const, @@ -17,7 +17,7 @@ const tenantScope = { describe("Conversation Brief search", () => { it("searches only the latest public root Briefs in the authorized scope", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { await migrateSchema(fixture.sql); const db = fixture.sql.db(); diff --git a/packages/junior/tests/component/briefs/conversation-brief-task.test.ts b/packages/junior/tests/component/briefs/conversation-brief-task.test.ts index 94f21a086f..c33fb11566 100644 --- a/packages/junior/tests/component/briefs/conversation-brief-task.test.ts +++ b/packages/junior/tests/component/briefs/conversation-brief-task.test.ts @@ -22,7 +22,7 @@ import { } from "@/db/schema"; import { and, eq } from "drizzle-orm"; import { - createLocalJuniorSqlFixture, + createJuniorSqlFixture, type LocalJuniorSqlFixture, } from "../../fixtures/sql"; @@ -195,7 +195,7 @@ describe("Conversation Brief task", () => { beforeEach(async () => { setBriefsConfig({ enabled: true }); process.env.JUNIOR_STATE_ADAPTER = "memory"; - fixture = await createLocalJuniorSqlFixture(); + fixture = await createJuniorSqlFixture(); TEST.sql = fixture.sql; TEST.calls.length = 0; await migrateSchema(fixture.sql); diff --git a/packages/junior/tests/component/config/location-configuration-sql.test.ts b/packages/junior/tests/component/config/location-configuration-sql.test.ts index c6871861fc..fb3deccef4 100644 --- a/packages/junior/tests/component/config/location-configuration-sql.test.ts +++ b/packages/junior/tests/component/config/location-configuration-sql.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { createDurableLocationConfigurationService } from "@/chat/configuration/sql"; import { migrateSchema } from "@/chat/conversations/sql/migrations"; -import { createLocalJuniorSqlFixture } from "../../fixtures/sql"; +import { createJuniorSqlFixture } from "../../fixtures/sql"; const DESTINATION = { platform: "slack" as const, @@ -27,7 +27,7 @@ function legacyConfiguration(value: string) { describe("SQL location configuration", () => { it("persists configuration independently of the legacy cache", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); await migrateSchema(fixture.sql); try { @@ -61,7 +61,7 @@ describe("SQL location configuration", () => { }); it("copies a live legacy record into SQL once", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); await migrateSchema(fixture.sql); try { @@ -96,7 +96,7 @@ describe("SQL location configuration", () => { }); it("keeps a concurrent SQL write over a stale legacy cutover", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); await migrateSchema(fixture.sql); const destination = { platform: "slack" as const, diff --git a/packages/junior/tests/component/conversation-message-search.test.ts b/packages/junior/tests/component/conversation-message-search.test.ts index 5122117599..4d1609579a 100644 --- a/packages/junior/tests/component/conversation-message-search.test.ts +++ b/packages/junior/tests/component/conversation-message-search.test.ts @@ -6,11 +6,11 @@ import { createSqlConversationMessageSearchStore } from "@/chat/conversations/sq import { createSqlStore } from "@/chat/conversations/sql/store"; import type { ConversationPrivacy } from "@/chat/conversation-privacy"; import { createPluginAnnotations } from "@/chat/plugins/annotations"; -import { createLocalJuniorSqlFixture } from "../fixtures/sql"; +import { createJuniorSqlFixture } from "../fixtures/sql"; describe("conversation message search", () => { it("returns only public user and assistant messages from the authorized workspace", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { await migrateSchema(fixture.sql); diff --git a/packages/junior/tests/component/conversation-sql-store.test.ts b/packages/junior/tests/component/conversation-sql-store.test.ts index aca933faaa..4381659f7e 100644 --- a/packages/junior/tests/component/conversation-sql-store.test.ts +++ b/packages/junior/tests/component/conversation-sql-store.test.ts @@ -27,12 +27,12 @@ import { } from "../fixtures/conversation-work"; import { createConfiguredJuniorSqlFixture, - createLocalJuniorSqlFixture, + createJuniorSqlFixture, } from "../fixtures/sql"; describe("conversation SQL store", () => { it("binds one provider conversation to an existing durable conversation", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -89,7 +89,7 @@ describe("conversation SQL store", () => { }); it("rejects provider conversation rebinding to another conversation", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -127,7 +127,7 @@ describe("conversation SQL store", () => { }); it("rejects updates to a child whose parent has no persisted root", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -168,7 +168,7 @@ describe("conversation SQL store", () => { }); it("requires a destination on first root upsert", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -188,7 +188,7 @@ describe("conversation SQL store", () => { }); it("persists queryable conversation records and linked identities", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -384,7 +384,7 @@ describe("conversation SQL store", () => { }); it("links actor identities to users by case-insensitive verified email", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -493,7 +493,7 @@ describe("conversation SQL store", () => { }); it("rejects a provider account already linked to another user", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -537,7 +537,7 @@ describe("conversation SQL store", () => { }); it("falls back to the provider name when an actor has no linked user", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -575,7 +575,7 @@ describe("conversation SQL store", () => { }); it("preserves an existing verified identity email when linking its user", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -647,7 +647,7 @@ describe("conversation SQL store", () => { }, 15_000); it("fills missing actor identity from later trusted profile observations", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -694,7 +694,7 @@ describe("conversation SQL store", () => { }); it("persists session source set-once and ignores later turn anchors", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -757,7 +757,7 @@ describe("conversation SQL store", () => { }); it("persists visibility from source signals and converges on newer signals", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -824,7 +824,7 @@ describe("conversation SQL store", () => { }); it("leaves Slack visibility missing without a live signal", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -854,7 +854,7 @@ describe("conversation SQL store", () => { }); it("resolves a known Slack destination by exact display name", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -899,7 +899,7 @@ describe("conversation SQL store", () => { }); it("fails closed when a destination display name is ambiguous", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -940,7 +940,7 @@ describe("conversation SQL store", () => { }); it("rejects legacy JSON metadata that was not migrated to foreign keys", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -1000,7 +1000,7 @@ INSERT INTO junior_conversations ( }); it("keeps newer SQL execution when a stale mirror arrives later", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -1054,7 +1054,7 @@ INSERT INTO junior_conversations ( }); it("replaces the matching run metrics after execution cursor changes", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -1197,7 +1197,7 @@ ORDER BY metric }); it("keeps SQL execution timestamps when a fresh summary omits them", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -1248,7 +1248,7 @@ ORDER BY metric }); it("keeps the earliest creation time across SQL metadata updates", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { const store = createSqlStore(fixture.sql); @@ -1442,7 +1442,7 @@ ORDER BY metric }); it("mirrors worker check-ins into SQL execution progress", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { vi.useFakeTimers({ now: 1_000 }); @@ -1496,7 +1496,7 @@ ORDER BY metric }); it("mirrors mailbox drains into SQL execution progress", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { await disconnectStateAdapter(); diff --git a/packages/junior/tests/component/conversation-storage-sql.test.ts b/packages/junior/tests/component/conversation-storage-sql.test.ts index abca021286..46f0ac5a64 100644 --- a/packages/junior/tests/component/conversation-storage-sql.test.ts +++ b/packages/junior/tests/component/conversation-storage-sql.test.ts @@ -20,7 +20,7 @@ import type { JuniorSqlDatabase } from "@/db/db"; import { juniorConversationEvents, juniorConversations } from "@/db/schema"; import { buildJuniorSqlConversation, - createLocalJuniorSqlFixture, + createEmptyJuniorSqlFixture, type LocalJuniorSqlFixture, } from "../fixtures/sql"; import { @@ -384,7 +384,7 @@ describe("SQL conversation storage", () => { }); it("applies Drizzle migrations idempotently", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -400,7 +400,7 @@ describe("SQL conversation storage", () => { }); it("assigns sequential seq and fences conflicting appends loudly", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -458,7 +458,7 @@ describe("SQL conversation storage", () => { }); it("loads the latest matching structured event directly", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -513,7 +513,7 @@ describe("SQL conversation storage", () => { }); it("loads the latest user instruction", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -556,7 +556,7 @@ describe("SQL conversation storage", () => { }); it("does not refresh a conversation for duplicate appends", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -623,7 +623,7 @@ describe("SQL conversation storage", () => { }); it("deduplicates repeated keys within one append without leaving seq gaps", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const store = createSqlConversationEventStore(fixture.sql); try { @@ -681,7 +681,7 @@ describe("SQL conversation storage", () => { }); it("persists only the first conflicting terminal turn event", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -725,7 +725,7 @@ describe("SQL conversation storage", () => { }); it("replaces NUL characters before persisting conversation events", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -751,7 +751,7 @@ describe("SQL conversation storage", () => { }); it("returns only the active history version", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -794,7 +794,7 @@ describe("SQL conversation storage", () => { }, 15_000); it("loads exactly the history version containing an event cursor", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -867,7 +867,7 @@ describe("SQL conversation storage", () => { }); it("does not decode events after a fixed epoch boundary", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -911,7 +911,7 @@ INSERT INTO junior_conversation_events ( }); it("narrow reads do not decode unrelated or superseded events", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -962,7 +962,7 @@ INSERT INTO junior_conversation_events ( }); it("keeps a bounded visible suffix after compacting more than 864 messages", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -1034,7 +1034,7 @@ WHERE conversation_id = $1 AND seq = 0 }, 30_000); it("round trips provider-neutral isolated subagent history", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -1056,7 +1056,7 @@ WHERE conversation_id = $1 AND seq = 0 }); it("rolls back a failed history replacement transaction", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -1113,7 +1113,7 @@ WHERE conversation_id = $1 AND seq = 0 ])( "preserves unsupported stored events as opaque facts %#", async ({ schemaVersion, type, payload }) => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -1157,7 +1157,7 @@ INSERT INTO junior_conversation_events ( ); it("rejects malformed payloads for supported stored events", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -1190,7 +1190,7 @@ INSERT INTO junior_conversation_events ( }); it("uses physical event columns as authoritative when decoding rows", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -1237,7 +1237,7 @@ INSERT INTO junior_conversation_events ( }); it("records message and handled facts idempotently", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -1310,7 +1310,7 @@ INSERT INTO junior_conversation_events ( }); it("advances last_activity_at on content writes without regressing on backdated content", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); async function lastActivityMs(): Promise { const rows = await fixture.sql @@ -1388,7 +1388,7 @@ INSERT INTO junior_conversation_events ( }); it("purges conversation events for a conversation and its descendants", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); diff --git a/packages/junior/tests/component/conversations/retention.test.ts b/packages/junior/tests/component/conversations/retention.test.ts index 0d30073b78..6bb65fd6a6 100644 --- a/packages/junior/tests/component/conversations/retention.test.ts +++ b/packages/junior/tests/component/conversations/retention.test.ts @@ -28,7 +28,7 @@ import { } from "@/chat/attachments/store"; import type { AttachmentStorage } from "@/chat/attachments/storage"; import { - createLocalJuniorSqlFixture, + createJuniorSqlFixture, type LocalJuniorSqlFixture, } from "../../fixtures/sql"; @@ -141,7 +141,7 @@ describe("retention purge job", () => { let fixture: LocalJuniorSqlFixture; beforeEach(async () => { - fixture = await createLocalJuniorSqlFixture(); + fixture = await createJuniorSqlFixture(); await migrateSchema(fixture.sql); }); diff --git a/packages/junior/tests/component/memory-plugin-storage.test.ts b/packages/junior/tests/component/memory-plugin-storage.test.ts index 1dbd00541a..7af60c9e98 100644 --- a/packages/junior/tests/component/memory-plugin-storage.test.ts +++ b/packages/junior/tests/component/memory-plugin-storage.test.ts @@ -17,7 +17,7 @@ import { readActorIdentity, resolveViewerUser } from "@/chat/plugins/viewer"; import { readPluginUserPage } from "@/chat/plugins/user-pages"; import { migratePluginsToSql } from "@/cli/upgrade/migrations/plugin-sql"; import { runUpgrade } from "@/cli/upgrade"; -import { createLocalJuniorSqlFixture } from "../fixtures/sql"; +import { createEmptyJuniorSqlFixture } from "../fixtures/sql"; import { createSlackSource, defineJuniorPlugin, @@ -26,7 +26,7 @@ import { const NEON = vi.hoisted(() => ({ sql: undefined as - | Awaited>["sql"] + | Awaited>["sql"] | undefined, originalDatabaseUrl: process.env.DATABASE_URL, })); @@ -97,7 +97,7 @@ function memoryMigrationFiles(): string[] { } async function migrateMemorySchema( - fixture: Awaited>, + fixture: Awaited>, ) { await migratePluginSchemas(fixture.sql, [ { @@ -108,7 +108,7 @@ async function migrateMemorySchema( } async function recordPrivateConversation( - fixture: Awaited>, + fixture: Awaited>, args: { channelId: string; conversationId: string; @@ -184,7 +184,7 @@ function memoryToolsFor(args: { describe("memory plugin host wiring", () => { it("adopts exact legacy migration hashes without replaying them", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const migrations = readMigrationFiles({ migrationsFolder: memoryMigrationsDir(), }); @@ -235,7 +235,7 @@ CREATE TABLE junior_schema_migrations ( }); it("does not adopt an unknown memory legacy checksum", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const migrationCount = readMigrationFiles({ migrationsFolder: memoryMigrationsDir(), }).length; @@ -273,7 +273,7 @@ CREATE TABLE junior_schema_migrations ( }); it("applies packaged migrations through plugin discovery", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); NEON.sql = fixture.sql; try { @@ -332,7 +332,7 @@ WHERE indexname = 'junior_memory_memories_search_idx' }, 15_000); it("reports core and nonempty plugin migration journals", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); NEON.sql = fixture.sql; try { @@ -377,7 +377,7 @@ WHERE indexname = 'junior_memory_memories_search_idx' }, 15_000); it("reads public memory everywhere and private memory only for its User", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const plugin = memoryPlugin(); setPlugins([plugin]); NEON.sql = fixture.sql; @@ -484,7 +484,7 @@ WHERE indexname = 'junior_memory_memories_search_idx' }, 15_000); it("registers memory tools with runtime-provided plugin DB access", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); setPlugins([memoryPlugin()]); NEON.sql = fixture.sql; diff --git a/packages/junior/tests/component/scheduled-automations-sql.test.ts b/packages/junior/tests/component/scheduled-automations-sql.test.ts index 64f6349ed8..00b0aaae9e 100644 --- a/packages/junior/tests/component/scheduled-automations-sql.test.ts +++ b/packages/junior/tests/component/scheduled-automations-sql.test.ts @@ -18,7 +18,7 @@ import type { ScheduledAutomation } from "@/chat/scheduled-automations/types"; import { migrateSchema } from "@/chat/conversations/sql/migrations"; import { upsertIdentity } from "@/chat/identities/sql"; import { deferred } from "../fixtures/conversation-work"; -import { createLocalJuniorSqlFixture } from "../fixtures/sql"; +import { createEmptyJuniorSqlFixture } from "../fixtures/sql"; const TEST_RUN_AT_MS = Date.parse("2026-05-26T12:00:00.000Z"); const TEST_NOW_MS = Date.parse("2026-05-26T12:05:00.000Z"); @@ -97,7 +97,7 @@ function copyPreSchedulerCoreMigrations(): string { } async function createLegacySchedulerTables( - fixture: Awaited>, + fixture: Awaited>, ): Promise { await fixture.sql.execute(` CREATE TABLE junior_scheduler_tasks ( @@ -121,7 +121,7 @@ CREATE TABLE junior_scheduler_runs ( describe("scheduled-automation SQL storage", () => { it("adopts deployed Scheduler rows into the core schema", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const oldCoreMigrations = copyPreSchedulerCoreMigrations(); try { @@ -291,7 +291,7 @@ describe("scheduled-automation SQL storage", () => { }, 30_000); it("creates and claims scheduled work from a fresh core database", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -327,7 +327,7 @@ describe("scheduled-automation SQL storage", () => { }, 30_000); it("skips pending runs and stops claiming after a task is deleted", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -369,7 +369,7 @@ describe("scheduled-automation SQL storage", () => { }, 30_000); it("does not overwrite a concurrent task save while skipping missed work", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -430,7 +430,7 @@ describe("scheduled-automation SQL storage", () => { }, 30_000); it("skips malformed rows while claiming later valid work", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); diff --git a/packages/junior/tests/component/viewer-display-name.test.ts b/packages/junior/tests/component/viewer-display-name.test.ts index 508ad736d2..0114eb2bc7 100644 --- a/packages/junior/tests/component/viewer-display-name.test.ts +++ b/packages/junior/tests/component/viewer-display-name.test.ts @@ -6,11 +6,11 @@ import { updateViewerDisplayNameFromSql, } from "@/chat/plugins/viewer"; import { juniorUsers } from "@/db/schema"; -import { createLocalJuniorSqlFixture } from "../fixtures/sql"; +import { createJuniorSqlFixture } from "../fixtures/sql"; describe("viewer display name", () => { it("persists a display name for the canonical user", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { await migrateSchema(fixture.sql); const db = fixture.sql.db(); diff --git a/packages/junior/tests/fixtures/postgres/fixture.ts b/packages/junior/tests/fixtures/postgres/fixture.ts index 2a806dceae..82adf4eb98 100644 --- a/packages/junior/tests/fixtures/postgres/fixture.ts +++ b/packages/junior/tests/fixtures/postgres/fixture.ts @@ -2,14 +2,11 @@ import { inject } from "vitest"; import { parsePostgresHarnessConfig, createEmptyPostgresDatabase, - createPostgresTransactionFixture, + getPostgresWorkerDatabaseUrl, type PostgresHarnessConfig, } from "@sentry/junior-testing/postgres"; import type { JuniorSqlExecutor } from "@/db/db"; -import { - createClientJuniorSqlExecutor, - createPooledJuniorSqlExecutor, -} from "./executor"; +import { createPooledJuniorSqlExecutor } from "./executor"; export interface JuniorPostgresFixture { sql: JuniorSqlExecutor; @@ -36,15 +33,16 @@ function getHarnessConfig(): PostgresHarnessConfig { return parsePostgresHarnessConfig(config); } -/** Create a rollback-isolated fixture from the migrated Junior template DB. */ +/** Use the migrated per-worker database that the shared setup truncates per test. */ export async function createMigratedJuniorSqlFixture(): Promise { - const transaction = await createPostgresTransactionFixture( - getHarnessConfig(), - ({ client, close }) => createClientJuniorSqlExecutor(client, close), - ); + const config = getHarnessConfig(); + const pooled = createPooledJuniorSqlExecutor({ + applicationName: config.applicationName, + connectionString: await getPostgresWorkerDatabaseUrl(config), + }); return { - sql: transaction.resource, - close: () => transaction.close(), + sql: pooled.db, + close: () => pooled.close(), }; } diff --git a/packages/junior/tests/fixtures/sql.ts b/packages/junior/tests/fixtures/sql.ts index d6c18ae178..6070ef7833 100644 --- a/packages/junior/tests/fixtures/sql.ts +++ b/packages/junior/tests/fixtures/sql.ts @@ -8,10 +8,13 @@ import { import { migrate } from "drizzle-orm/pglite/migrator"; import type { PgliteDatabase } from "drizzle-orm/pglite"; import { - createEmptyJuniorSqlFixture, - hasJuniorPostgresTestDatabase, + createEmptyJuniorSqlFixture as createEmptyPostgresJuniorSqlFixture, + createMigratedJuniorSqlFixture, + type JuniorPostgresDatabaseFixture, + hasJuniorPostgresTestDatabase as hasJuniorPostgresTestDatabaseFixture, } from "./postgres/fixture"; import { closeDb, getSqlExecutor } from "@/chat/db"; +import { migrateSchema } from "@/chat/conversations/sql/migrations"; export type JuniorSqlConversationInsert = typeof juniorConversations.$inferInsert; @@ -21,23 +24,16 @@ export interface LocalJuniorSqlFixture { close(): Promise; } -/** - * Create a local Postgres-compatible Junior SQL fixture for integration tests. - */ -export async function createLocalJuniorSqlFixture(): Promise { - if (hasJuniorPostgresTestDatabase()) { - const fixture = await createEmptyJuniorSqlFixture(); - return { - sql: fixture.sql, - close: () => fixture.close(), - }; - } +/** Return whether tests use the shared Postgres harness. */ +export function hasJuniorPostgresTestDatabase(): boolean { + return hasJuniorPostgresTestDatabaseFixture(); +} +async function createPgliteJuniorSqlFixture(): Promise { const fixture = await createLocalPgliteFixture>( juniorSqlSchema, ); - const sql: JuniorSqlExecutor = { close: () => fixture.close(), db: () => fixture.db() as JuniorDatabase, @@ -49,7 +45,6 @@ export async function createLocalJuniorSqlFixture(): Promise fixture.withLock(lockName, callback), withMigrationLock: (_migrationTable, callback) => callback(), }; - return { client: fixture.client, sql, @@ -57,6 +52,32 @@ export async function createLocalJuniorSqlFixture(): Promise { + if (hasJuniorPostgresTestDatabase()) { + return await createMigratedJuniorSqlFixture(); + } + const fixture = await createPgliteJuniorSqlFixture(); + await migrateSchema(fixture.sql); + return fixture; +} + +/** Create an empty committed database for migration contract tests. */ +export async function createEmptyJuniorSqlFixture(): Promise { + if (hasJuniorPostgresTestDatabase()) { + return await createEmptyPostgresJuniorSqlFixture(); + } + return await createPgliteJuniorSqlFixture(); +} + +/** Create an empty Postgres database for connection-level migration tests. */ +export async function createEmptyJuniorPostgresFixture(): Promise { + if (!hasJuniorPostgresTestDatabase()) { + throw new Error("Postgres test database is required"); + } + return await createEmptyPostgresJuniorSqlFixture(); +} + /** Use the product-configured SQL connection for API boundary tests. */ export function createConfiguredJuniorSqlFixture(): LocalJuniorSqlFixture { return { diff --git a/packages/junior/tests/integration/conversation-sql.test.ts b/packages/junior/tests/integration/conversation-sql.test.ts index 3bf58c857e..7e29b6e79e 100644 --- a/packages/junior/tests/integration/conversation-sql.test.ts +++ b/packages/junior/tests/integration/conversation-sql.test.ts @@ -16,12 +16,10 @@ import { disconnectStateAdapter } from "@/chat/state/adapter"; import { recordTurnSummary } from "@/chat/task-execution/turn-cursor"; import { buildJuniorSqlConversation, - createLocalJuniorSqlFixture, -} from "../fixtures/sql"; -import { + createEmptyJuniorPostgresFixture, createEmptyJuniorSqlFixture, hasJuniorPostgresTestDatabase, -} from "../fixtures/postgres/fixture"; +} from "../fixtures/sql"; import { applyCoreMigrations as applyCoreMigrationSlice, insertLegacyConversation, @@ -46,7 +44,7 @@ SELECT EXISTS ( } async function applyCoreMigrations( - fixture: Awaited>, + fixture: Awaited>, fromIndex: number, toIndexExclusive?: number, ): Promise { @@ -60,7 +58,7 @@ async function applyCoreMigrations( describe("conversation SQL local mode", () => { it("backfills session sources from matching conversation destinations", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const sourceMigrationIndex = coreMigrations.findIndex((migration) => migration.sql.some((statement) => statement.includes('ADD COLUMN "source_json" jsonb'), @@ -163,7 +161,7 @@ ORDER BY conversation_id }); it("migrates unowned MCP connection events into replayable facts", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const ownershipMigrationIndex = coreMigrations.findIndex((migration) => migration.sql.some((statement) => statement.includes("mcp_provider_connected_unowned"), @@ -214,7 +212,7 @@ ORDER BY conversation_id }); it("migrates legacy agent history to native event types", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); const nativeHistoryMigrationIndex = coreMigrations.findIndex((migration) => migration.sql.some((statement) => statement.includes( @@ -459,7 +457,7 @@ ORDER BY conversation_id }); it("creates migrated tables matching the Drizzle schema", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); @@ -562,7 +560,7 @@ ORDER BY table_name ASC, constraint_name ASC }); it("backfills the owning root for existing conversation trees", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await fixture.sql.execute(` @@ -627,7 +625,7 @@ ORDER BY conversation_id }); it("keeps core migrations separate from another Drizzle journal", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await fixture.sql.execute("CREATE SCHEMA IF NOT EXISTS drizzle"); @@ -661,7 +659,7 @@ VALUES ('host-migration', 9999999999999) it.skipIf(!hasJuniorPostgresTestDatabase())( "cancels runtime queries after the configured statement timeout", async () => { - const fixture = await createEmptyJuniorSqlFixture(); + const fixture = await createEmptyJuniorPostgresFixture(); const executor = createPostgresJuniorSqlExecutor({ connectionString: fixture.connectionString, statementTimeoutMs: 10, @@ -681,7 +679,7 @@ VALUES ('host-migration', 9999999999999) it.skipIf(!hasJuniorPostgresTestDatabase())( "serializes concurrent core migrations", async () => { - const fixture = await createEmptyJuniorSqlFixture(); + const fixture = await createEmptyJuniorPostgresFixture(); const second = createPostgresJuniorSqlExecutor({ connectionString: fixture.connectionString, }); @@ -705,7 +703,7 @@ VALUES ('host-migration', 9999999999999) ); it("runs migrations and stores metadata through the Drizzle schema", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { const migrationLock = vi.spyOn(fixture.sql, "withMigrationLock"); @@ -824,7 +822,7 @@ CREATE TABLE junior_conversations ( ); it("mirrors completed scheduler turns into SQL conversation record", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createEmptyJuniorSqlFixture(); try { await migrateSchema(fixture.sql); diff --git a/packages/junior/tests/integration/slack-schedule-tools.test.ts b/packages/junior/tests/integration/slack-schedule-tools.test.ts index ce86dce30b..41502371f5 100644 --- a/packages/junior/tests/integration/slack-schedule-tools.test.ts +++ b/packages/junior/tests/integration/slack-schedule-tools.test.ts @@ -19,12 +19,11 @@ import * as dbModule from "@/chat/db"; import { getPluginTools, setPlugins } from "@/chat/plugins/agent-hooks"; import { createTools } from "@/chat/tools"; import { disconnectStateAdapter } from "@/chat/state/adapter"; -import { migrateSchema } from "@/chat/conversations/sql/migrations"; import type { ToolExecuteOptions } from "@/chat/tools/definition"; import { ToolInputError } from "@/chat/tools/execution/tool-input-error"; import type { JuniorDatabase } from "@/db/db"; import { - createLocalJuniorSqlFixture, + createJuniorSqlFixture, type LocalJuniorSqlFixture, } from "../fixtures/sql"; import { createSlackSource } from "@sentry/junior-plugin-api"; @@ -38,8 +37,7 @@ let currentFixture: LocalJuniorSqlFixture | undefined; let toolCallSequence = 0; async function useSchedulerSqlPlugin() { - const fixture = await createLocalJuniorSqlFixture(); - await migrateSchema(fixture.sql); + const fixture = await createJuniorSqlFixture(); vi.spyOn(dbModule, "getDb").mockReturnValue(fixture.sql.db()); return fixture; } diff --git a/packages/junior/tests/integration/slack-send-files.test.ts b/packages/junior/tests/integration/slack-send-files.test.ts index c7873ff90e..8299baf712 100644 --- a/packages/junior/tests/integration/slack-send-files.test.ts +++ b/packages/junior/tests/integration/slack-send-files.test.ts @@ -17,7 +17,7 @@ import { ToolInputError } from "@/chat/tools/execution/tool-input-error"; import { readSandboxFileUpload } from "@/chat/tools/sandbox/file-uploads"; import type { ToolState } from "@/chat/tools/types"; import { juniorAttachments } from "@/db/schema"; -import { createLocalJuniorSqlFixture } from "../fixtures/sql"; +import { createJuniorSqlFixture } from "../fixtures/sql"; import { getCapturedSlackApiCalls } from "../msw/handlers/slack-api"; function createToolState(): ToolState { @@ -565,7 +565,7 @@ describe("Slack sendFiles", () => { }); it("deletes the blob when SQL insert fails after put", async () => { - const fixture = await createLocalJuniorSqlFixture(); + const fixture = await createJuniorSqlFixture(); try { await migrateSchema(fixture.sql); // No conversation row: FK on junior_attachments.conversation_id fails. diff --git a/packages/junior/tests/integration/workspace-snapshot-migration.test.ts b/packages/junior/tests/integration/workspace-snapshot-migration.test.ts index 014f911c05..3d0239fa40 100644 --- a/packages/junior/tests/integration/workspace-snapshot-migration.test.ts +++ b/packages/junior/tests/integration/workspace-snapshot-migration.test.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from "node:url"; import { readMigrationFiles } from "drizzle-orm/migrator"; import { describe, expect, it } from "vitest"; import { applyCoreMigrations } from "../fixtures/conversation-sql-migrations"; -import { createEmptyJuniorSqlFixture } from "../fixtures/postgres/fixture"; +import { createEmptyJuniorSqlFixture } from "../fixtures/sql"; const coreMigrations = readMigrationFiles({ migrationsFolder: fileURLToPath(new URL("../../migrations", import.meta.url)), diff --git a/policies/testing.md b/policies/testing.md index fd89b3f523..093d8619b9 100644 --- a/policies/testing.md +++ b/policies/testing.md @@ -51,6 +51,10 @@ refactors should not churn brittle unit tests. calls. Put those fakes in component tests when the contract needs them. - Prefer existing harnesses, shared fixtures, memory adapters, MSW handlers, and outboxes over ad hoc mocks or local payload schemas. +- Database tests must use `tests/fixtures/sql`. Normal tests use + `createJuniorSqlFixture`, which uses the migrated worker database that shared + setup truncates before each test. Only migration contract tests use an explicit + empty fixture. Do not import Postgres fixture internals from a test. - Assert user-visible outcomes and external contracts before implementation details. Logs, spans, and status telemetry are not behavior contracts unless the test is explicitly about instrumentation. diff --git a/scripts/check-test-architecture.mjs b/scripts/check-test-architecture.mjs index 0f715cc9ce..a9d307edde 100644 --- a/scripts/check-test-architecture.mjs +++ b/scripts/check-test-architecture.mjs @@ -6,7 +6,18 @@ const TEST_ROOTS = [ "packages/junior/tests/integration", "packages/junior-dashboard/e2e", ]; +const DATABASE_TEST_ROOTS = [ + "packages/junior/tests/component", + "packages/junior/tests/integration", +]; const DASHBOARD_E2E_ROOT = "packages/junior-dashboard/e2e/"; +const EMPTY_DATABASE_TESTS = new Set([ + "packages/junior/tests/component/conversation-storage-sql.test.ts", + "packages/junior/tests/component/memory-plugin-storage.test.ts", + "packages/junior/tests/component/scheduled-automations-sql.test.ts", + "packages/junior/tests/integration/conversation-sql.test.ts", + "packages/junior/tests/integration/workspace-snapshot-migration.test.ts", +]); const RULES = [ { @@ -85,8 +96,43 @@ export function checkIntegrationTestArchitecture(files) { return errors; } -function collectTests(root) { - return TEST_ROOTS.flatMap((testRoot) => { +/** Report SQL fixture boundary violations. */ +export function checkDatabaseTestFixtures(files) { + const errors = []; + for (const file of files) { + const directFixtureImports = countMatches( + file.contents, + /(?:fixtures\/postgres\/fixture|@sentry\/junior-testing\/postgres)/g, + ); + if (directFixtureImports > 0) { + errors.push( + `${file.path}: database tests must import fixtures through tests/fixtures/sql (${directFixtureImports} found, 0 allowed)`, + ); + } + const legacyFixtures = countMatches( + file.contents, + /\bcreateLocalJuniorSqlFixture\b/g, + ); + if (legacyFixtures > 0) { + errors.push( + `${file.path}: database tests must use createJuniorSqlFixture or createEmptyJuniorSqlFixture (${legacyFixtures} found, 0 allowed)`, + ); + } + const emptyFixtures = countMatches( + file.contents, + /\bcreateEmptyJunior(?:Postgres|Sql)Fixture\b/g, + ); + if (emptyFixtures > 0 && !EMPTY_DATABASE_TESTS.has(file.path)) { + errors.push( + `${file.path}: only migration contract tests may use empty Junior SQL fixtures (${emptyFixtures} found, 0 allowed)`, + ); + } + } + return errors; +} + +function collectTests(root, testRoots = TEST_ROOTS) { + return testRoots.flatMap((testRoot) => { const directory = path.join(root, testRoot); return fs .readdirSync(directory, { recursive: true, withFileTypes: true }) @@ -108,7 +154,10 @@ function collectTests(root) { function main() { const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(scriptDirectory, ".."); - const errors = checkIntegrationTestArchitecture(collectTests(root)); + const errors = [ + ...checkIntegrationTestArchitecture(collectTests(root)), + ...checkDatabaseTestFixtures(collectTests(root, DATABASE_TEST_ROOTS)), + ]; if (errors.length === 0) { console.log("Tests follow test architecture policy."); return; diff --git a/scripts/check-test-architecture.test.mjs b/scripts/check-test-architecture.test.mjs index 1c4d3c7d90..49c44cc6e6 100644 --- a/scripts/check-test-architecture.test.mjs +++ b/scripts/check-test-architecture.test.mjs @@ -1,6 +1,9 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { checkIntegrationTestArchitecture } from "./check-test-architecture.mjs"; +import { + checkDatabaseTestFixtures, + checkIntegrationTestArchitecture, +} from "./check-test-architecture.mjs"; const TEST_PATH = "packages/junior/tests/integration/new.test.ts"; const DASHBOARD_E2E_PATH = @@ -160,6 +163,57 @@ test("allows screenshots in dashboard E2E tests", () => { ); }); +test("rejects database fixture boundary bypasses", () => { + assert.deepEqual( + checkDatabaseTestFixtures([ + integrationTest( + [ + 'import { createEmptyJuniorSqlFixture } from "../fixtures/postgres/fixture";', + 'import { createPostgresTransactionFixture } from "@sentry/junior-testing/postgres";', + ].join("\n"), + ), + ]), + [ + `${TEST_PATH}: database tests must import fixtures through tests/fixtures/sql (2 found, 0 allowed)`, + `${TEST_PATH}: only migration contract tests may use empty Junior SQL fixtures (1 found, 0 allowed)`, + ], + ); +}); + +test("rejects the ambiguous local SQL fixture", () => { + assert.deepEqual( + checkDatabaseTestFixtures([ + integrationTest("await createLocalJuniorSqlFixture();"), + ]), + [ + `${TEST_PATH}: database tests must use createJuniorSqlFixture or createEmptyJuniorSqlFixture (1 found, 0 allowed)`, + ], + ); +}); + +test("allows the shared migrated SQL fixture", () => { + assert.deepEqual( + checkDatabaseTestFixtures([ + integrationTest( + 'import { createJuniorSqlFixture } from "../fixtures/sql";\nawait createJuniorSqlFixture();', + ), + ]), + [], + ); +}); + +test("allows the empty SQL fixture only in migration contract tests", () => { + assert.deepEqual( + checkDatabaseTestFixtures([ + integrationTest( + 'import { createEmptyJuniorSqlFixture } from "../fixtures/sql";\nawait createEmptyJuniorSqlFixture();', + "packages/junior/tests/integration/conversation-sql.test.ts", + ), + ]), + [], + ); +}); + test("rejects broad browser error assertions in dashboard E2E tests", () => { assert.deepEqual( checkIntegrationTestArchitecture([