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
76 changes: 56 additions & 20 deletions turbo/apps/api/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,42 @@ const promiseChainAllowlist = [
const apiTestExternalBehaviorMessage =
"API tests must exercise external behavior through API endpoints. Do not test internal implementation details. See docs/testing/testing-external-behavior.md.";

const apiTestDirectDbImportMessage =
"API tests must not import DB handles directly. Exercise setup and assertions through API endpoints; add a test route only when an external-behavior exception is justified.";

const productionRouteTestImportMessage =
"Production route composition must not import test-only routes. Mount required test fixture routes explicitly from tests.";

const apiTestDirectDbImportPatterns = [
"./external/db",
"./external/db.ts",
"../external/db",
"../external/db.ts",
"../signals/external/db",
"../signals/external/db.ts",
"../../external/db",
"../../external/db.ts",
"../../signals/external/db",
"../../signals/external/db.ts",
"../../../external/db",
"../../../external/db.ts",
"../../../signals/external/db",
"../../../signals/external/db.ts",
"../../../../external/db",
"../../../../external/db.ts",
"../../../../signals/external/db",
"../../../../signals/external/db.ts",
"src/signals/external/db",
"src/signals/external/db.ts",
];

const apiTestServiceImportPatterns = [
"./*.service",
"./*.service.ts",
"./services/*",
"./services/**/*",
"../*.service",
"../*.service.ts",
"../services/*",
"../services/**/*",
"../signals/services/*",
Expand All @@ -114,12 +147,6 @@ const apiTestServiceImportPatterns = [
"src/signals/services/**/*",
];

const apiServiceTestParentImportPatterns = [
"../*.service",
"../*.utils",
"../assistant-message-id",
];

export default [
...config,
{
Expand Down Expand Up @@ -182,33 +209,42 @@ export default [
},
...oxlint.buildFromOxlintConfigFile("./.oxlintrc.json"),
{
files: ["src/**/__tests__/**/*.ts", "src/**/*.test.ts"],
files: ["src/signals/services/**/*.test.ts"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Program",
message:
"Service-directory tests must live behind API endpoint boundaries. Put coverage under routes/__tests__ or document a narrow exception in services/__tests__.",
},
],
},
},
{
files: ["src/signals/route.ts"],
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "@vm0/db/schema",
message: apiTestExternalBehaviorMessage,
},
],
patterns: [
{
group: ["@vm0/db/schema/*"],
message: apiTestExternalBehaviorMessage,
group: ["./routes/test-*", "./routes/test-*/**"],
message: productionRouteTestImportMessage,
},
],
paths: [
{
group: apiTestServiceImportPatterns,
message: apiTestExternalBehaviorMessage,
name: "./routes/cli-auth-test",
message: productionRouteTestImportMessage,
},
],
},
],
},
},
{
files: ["src/signals/services/__tests__/**/*.ts"],
files: ["src/**/__tests__/**/*.ts", "src/**/*.test.ts"],
rules: {
"no-restricted-imports": [
"error",
Expand All @@ -229,8 +265,8 @@ export default [
message: apiTestExternalBehaviorMessage,
},
{
group: apiServiceTestParentImportPatterns,
message: apiTestExternalBehaviorMessage,
group: apiTestDirectDbImportPatterns,
message: apiTestDirectDbImportMessage,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import { testContext } from "../../../__tests__/test-context";
import { computeHmacSignature } from "../../event-consumer/hmac";
import { now } from "../../time";
import { seedAgentRunCallback$ } from "../../../signals/routes/__tests__/helpers/agent-run-callback";
import {
seedCompose$,
seedRun$,
} from "../../../signals/routes/__tests__/helpers/zero-usage-insight";
import { createBddApi } from "../../../signals/routes/__tests__/helpers/api-bdd";
import { createRunsAutomationsApi } from "../../../signals/routes/__tests__/helpers/api-bdd-runs-automations";
import type { RouteEntry } from "../../../signals/route-entry";

import { callbackPayload$, callbackRoute } from "../callback-route";
Expand Down Expand Up @@ -65,28 +63,34 @@ async function seedCallback(): Promise<{
runId: string;
callbackId: string;
}> {
const orgId = `org_${randomUUID().slice(0, 8)}`;
const userId = `user_${randomUUID().slice(0, 8)}`;
const { composeId } = await store.set(
seedCompose$,
{ orgId, userId },
context.signal,
);
const { runId } = await store.set(
seedRun$,
{ orgId, userId, composeId },
context.signal,
);
const bdd = createBddApi(context);
const api = createRunsAutomationsApi(context);
const actor = bdd.user();
bdd.acceptAgentStorageWrites();
api.acceptStorageDownloads();
api.acceptTelemetryIngest();
api.configureRunnerGroup();
await api.grantProEntitlement(actor);
await api.ensureOrgModelProvider(actor);
const agent = await bdd.createAgent(actor, {
displayName: "Callback route probe agent",
visibility: "private",
});
const run = await api.createRun(actor, {
agentId: agent.agentId,
prompt: "callback route probe run",
modelProvider: "anthropic-api-key",
});
const { callbackId } = await store.set(
seedAgentRunCallback$,
{
runId,
runId: run.runId,
url: `http://localhost${PATH}`,
payload: {},
},
context.signal,
);
return { runId, callbackId };
return { runId: run.runId, callbackId };
}

describe("callbackRoute$ primitive", () => {
Expand Down
53 changes: 53 additions & 0 deletions turbo/apps/api/src/signals/e2e-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { RouteEntry } from "./route-entry";
import { cliAuthTestRoutes } from "./routes/cli-auth-test";
import { testOAuthProviderAuthorizeRoutes } from "./routes/test-oauth-provider-authorize";
import { testOAuthProviderDeviceAuthRoutes } from "./routes/test-oauth-provider-device-auth";
import { testOAuthProviderEchoRoutes } from "./routes/test-oauth-provider-echo";
import { testOAuthProviderTokenRoutes } from "./routes/test-oauth-provider-token";
import { testOAuthProviderUserinfoRoutes } from "./routes/test-oauth-provider-userinfo";
import { testSlackDispatchProbeRoutes } from "./routes/test-slack-dispatch-probe";
import { testSlackMockRoutes } from "./routes/test-slack-mock";
import { testSlackStateRoutes } from "./routes/test-slack-state";
import { testTelegramDispatchProbeRoutes } from "./routes/test-telegram-dispatch-probe";
import { testTelegramMockRoutes } from "./routes/test-telegram-mock";
import { testTelegramStateRoutes } from "./routes/test-telegram-state";
import { testZeroAgentStateRoutes } from "./routes/test-zero-agent-state";

/**
* Deployed end-to-end infrastructure routes.
*
* These endpoints exist for the CLI E2E and Playwright suites that run against
* deployed preview environments, where in-process mocking (MSW) is impossible:
*
* - `cli-auth-test`: E2E account/token/connector provisioning used by the
* deploy workflow (`Generate E2E test tokens`) and `e2e/` suites.
* - `test-oauth-provider-*`: the synthetic OAuth provider backing the
* `test-oauth`/`test-oauth-device` connectors in `packages/connectors`.
* - `test-slack-mock` / `test-telegram-mock`: provider stand-ins that Slack
* and Telegram Web API traffic is redirected to on previews via
* `E2E_SLACK_MOCK_ENABLED` / `E2E_TELEGRAM_MOCK_ENABLED`.
* - state/probe routes: fixture seeding and dispatch probes used by
* `e2e/helpers/slack.bash` and `e2e/helpers/telegram.bash`.
*
* Every route here is gated by `isTestEndpointAllowed` (development or
* preview-with-bypass only) and returns 404 in production.
*
* API integration tests (vitest) must NOT use these routes: construct state
* through real production APIs, mock external providers with MSW, and assert
* through product read surfaces instead.
*/
export const E2E_ROUTES: readonly RouteEntry[] = [
...cliAuthTestRoutes,
...testOAuthProviderAuthorizeRoutes,
...testOAuthProviderDeviceAuthRoutes,
...testOAuthProviderEchoRoutes,
...testOAuthProviderTokenRoutes,
...testOAuthProviderUserinfoRoutes,
...testSlackDispatchProbeRoutes,
...testSlackMockRoutes,
...testSlackStateRoutes,
...testTelegramDispatchProbeRoutes,
...testTelegramMockRoutes,
...testTelegramStateRoutes,
...testZeroAgentStateRoutes,
];
56 changes: 2 additions & 54 deletions turbo/apps/api/src/signals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { agentSessionsRoutes } from "./routes/agent-sessions-id";
import { authMeRoutes } from "./routes/auth-me";
import { audioTranscriptionsV1Routes } from "./routes/audio-transcriptions-v1";
import { cliAuthRoutes } from "./routes/cli-auth";
import { cliAuthTestRoutes } from "./routes/cli-auth-test";
import { E2E_ROUTES } from "./e2e-routes";
import type { RouteEntry } from "./route-entry";
import { chatThreadsV1Routes } from "./routes/chat-threads-v1";
import { connectorsTypeCallbackRoutes } from "./routes/connectors-type-callback";
Expand Down Expand Up @@ -189,32 +189,6 @@ import { storagesCommitRoutes } from "./routes/storages-commit";
import { storagesDownloadRoutes } from "./routes/storages-download";
import { storagesListRoutes } from "./routes/storages-list";
import { storagesPrepareRoutes } from "./routes/storages-prepare";
import { testOAuthProviderAuthorizeRoutes } from "./routes/test-oauth-provider-authorize";
import { testOAuthProviderDeviceAuthRoutes } from "./routes/test-oauth-provider-device-auth";
import { testOAuthProviderEchoRoutes } from "./routes/test-oauth-provider-echo";
import { testOAuthProviderTokenRoutes } from "./routes/test-oauth-provider-token";
import { testOAuthProviderUserinfoRoutes } from "./routes/test-oauth-provider-userinfo";
import { testSlackDispatchProbeRoutes } from "./routes/test-slack-dispatch-probe";
import { testSlackMockRoutes } from "./routes/test-slack-mock";
import { testSlackStateRoutes } from "./routes/test-slack-state";
import { testSystemStoragePresignedUrlCacheStateRoutes } from "./routes/test-system-storage-presigned-url-cache-state";
import { testWorkflowSkillStoragePresignedUrlCacheStateRoutes } from "./routes/test-workflow-skill-storage-presigned-url-cache-state";
import { testEmailStateRoutes } from "./routes/test-email-state";
import { testBillingRedeemStateRoutes } from "./routes/test-billing-redeem-state";
import { testTelegramDispatchProbeRoutes } from "./routes/test-telegram-dispatch-probe";
import { testTelegramMockRoutes } from "./routes/test-telegram-mock";
import { testTelegramStateRoutes } from "./routes/test-telegram-state";
import { testGenerationStateRoutes } from "./routes/test-generation-state";
import { testOnboardingStatusStateRoutes } from "./routes/test-onboarding-status-state";
import { testMemoryStateRoutes } from "./routes/test-memory-state";
import { testRelationshipStateRoutes } from "./routes/test-relationship-state";
import { testUsageInsightStateRoutes } from "./routes/test-usage-insight-state";
import { testUsageStateRoutes } from "./routes/test-usage-state";
import { testUserExportStateRoutes } from "./routes/test-user-export-state";
import { testWorkflowTriggerStateRoutes } from "./routes/test-workflow-trigger-state";
import { testChatMessagesStateRoutes } from "./routes/test-chat-messages-state";
import { testWebhooksStateRoutes } from "./routes/test-webhooks-state";
import { testZeroAgentStateRoutes } from "./routes/test-zero-agent-state";

export const ROUTES: readonly RouteEntry[] = [
{
Expand All @@ -231,7 +205,6 @@ export const ROUTES: readonly RouteEntry[] = [
},
...authMeRoutes,
...cliAuthRoutes,
...cliAuthTestRoutes,
...desktopAuthRoutes,
...desktopUpdateRoutes,
...healthAuthProbeRoutes,
Expand Down Expand Up @@ -412,30 +385,5 @@ export const ROUTES: readonly RouteEntry[] = [
...modelStatsRoutes,
...presentationImagesRoutes,
...runnersRoutes,
...testOAuthProviderAuthorizeRoutes,
...testOAuthProviderDeviceAuthRoutes,
...testOAuthProviderEchoRoutes,
...testOAuthProviderTokenRoutes,
...testOAuthProviderUserinfoRoutes,
...testSlackDispatchProbeRoutes,
...testSlackMockRoutes,
...testSlackStateRoutes,
...testSystemStoragePresignedUrlCacheStateRoutes,
...testWorkflowSkillStoragePresignedUrlCacheStateRoutes,
...testEmailStateRoutes,
...testBillingRedeemStateRoutes,
...testTelegramDispatchProbeRoutes,
...testTelegramMockRoutes,
...testTelegramStateRoutes,
...testGenerationStateRoutes,
...testOnboardingStatusStateRoutes,
...testMemoryStateRoutes,
...testRelationshipStateRoutes,
...testUsageInsightStateRoutes,
...testUsageStateRoutes,
...testUserExportStateRoutes,
...testWorkflowTriggerStateRoutes,
...testChatMessagesStateRoutes,
...testWebhooksStateRoutes,
...testZeroAgentStateRoutes,
...E2E_ROUTES,
];
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import { server } from "../../../mocks/server";
import { writeDb$ } from "../../external/db";
import { nowDate } from "../../external/time";
import {
seedZeroChatThread$,
type ZeroChatThreadFixture,
} from "../__tests__/helpers/zero-chat-threads";
seedLegacyChatThread,
type LegacyChatThreadFixture,
} from "../../../test-fixtures/chat-threads";
import { seedUserModelProvider$ } from "./helpers/zero-model-providers";
import { seedOrgMembership$ } from "../__tests__/helpers/zero-org-membership";
import { createZeroRouteMocks } from "../__tests__/helpers/zero-route-test";
Expand Down Expand Up @@ -301,7 +301,7 @@ async function seedBackgroundLoad(): Promise<void> {
}

async function seedTargetThreadRuns(
fixture: ZeroChatThreadFixture,
fixture: LegacyChatThreadFixture,
): Promise<void> {
const db = store.set(writeDb$);
const versionId = randomUUID();
Expand Down Expand Up @@ -391,7 +391,7 @@ async function seedTargetThreadRuns(
}

async function seedSideEffectFreeGetData(
fixture: ZeroChatThreadFixture,
fixture: LegacyChatThreadFixture,
): Promise<void> {
const db = store.set(writeDb$);

Expand Down Expand Up @@ -483,7 +483,7 @@ async function seedSideEffectFreeGetData(
}

async function logPlannerDiagnostic(
fixture: ZeroChatThreadFixture,
fixture: LegacyChatThreadFixture,
): Promise<void> {
const db = store.set(writeDb$);
await db.execute(sql`
Expand Down Expand Up @@ -513,16 +513,12 @@ async function logPlannerDiagnostic(
);
}

const ensureSeeded: () => Promise<ZeroChatThreadFixture> = (() => {
let cached: Promise<ZeroChatThreadFixture> | undefined;
const ensureSeeded: () => Promise<LegacyChatThreadFixture> = (() => {
let cached: Promise<LegacyChatThreadFixture> | undefined;
return () => {
cached ??= (async () => {
installR2ListMock();
const seeded = await store.set(
seedZeroChatThread$,
{ title: "bench" },
context.signal,
);
const seeded = await seedLegacyChatThread({ title: "bench" });
await seedBackgroundLoad();
await seedTargetThreadRuns(seeded);
await seedSideEffectFreeGetData(seeded);
Expand Down
Loading
Loading