import { expect, onTestFinished } from "vitest"; import { denFetch, evalIn, go, readAvailableModels, waitFor } from "@openwork/behaviors"; import type { DenSession, ModelFacts } from "@openwork/behaviors"; import { screenshot, validate } from "@openwork/test-evidence"; import { app, eventually, needs, server, sleep, test, unmetNeeds } from "@openwork/testkit"; import type { TestNeeds } from "@openwork/testkit"; /** * ACCEPTANCE TEST for #3671: org-published LLM providers never finish syncing * to member desktops — the settings row sits on "Syncing" forever (suspect: * commit 316d364d0's engine-reload guard deferring reloads indefinitely). * * EXPECTED RED on dev as of 2026-08-11, at claim 1 or claim 2. Every failure * message embeds the per-provider /cloud-provider-sync/status payload so a red * run discriminates the three failure modes on its own: * - stuck-syncing -> claim 1 times out; the payload shows which * provider never reached a terminal outcome. * - applied-but-engine-empty -> claim 1 green, claim 2 red; status lists the * provider (fingerprint recorded as done) but * the engine never served its model — the * settings row derives "Syncing" from exactly * this gap (entitled && !available). * - silent-skip -> a published provider is absent from status * with no failure message anywhere (claims 1/3). * * Terminal per-provider outcome, derived from the real schema * (apps/server/src/cloud-provider-sync.ts:33-69): CloudProviderSyncStatus has * hasSession, lastRun { at, status: "applied"|"noop"|"failed", message?, * detail? } and providers[] (CloudProviderSyncStatusProvider: cloudProviderId, * providerId, sourceProviderId, name, source, updatedAt, modelIds, importedAt). * A provider entry only materializes after a successful pass * (updateProviderStatus, cloud-provider-sync.ts:781-801), so "terminal" here * means: the provider's cloudProviderId is listed AND lastRun settled as * applied|noop. Repeated "failed" (or absence) is permanently non-terminal. */ const ORGANIZATION_NAME = "Cloud Provider Sync Contract"; const CUSTOM_PROVIDER_NAME = "Sync Contract Custom Models"; const CUSTOM_PROVIDER_KEY = "sync-contract-custom-models"; const CUSTOM_MODEL_ID = "sync-contract-custom-model"; const CATALOG_PROVIDER_NAME = "Sync Contract Catalog Models"; // Same real models.dev catalog entry models-available.e2e.test.ts:37 publishes. const CATALOG_MODEL_ID = "gpt-5.4"; const REQUEST_TIMEOUT_MS = 10_000; const TERMINAL_BUDGET_MS = 120_000; const MODEL_BUDGET_MS = 30_000; const QUIET_DELAY_MS = 30_000; const OBSERVATION_WINDOW_MS = 60_000; // Intended poll cadence: one sync pass per 5 minutes (defaultIntervalMs = // 5 * 60 * 1_000, apps/server/src/cloud-provider-sync.ts:132; the testkit // desktop sets no OPENWORK_CLOUD_PROVIDER_SYNC_INTERVAL_MS override, so the // env branch at cloud-provider-sync.ts:481-484 stays on the default). Each // pass issues exactly ONE GET /v1/llm-providers/:llmProviderId/connect per // provider (fetchProviders, cloud-provider-sync.ts:294-306), so a 60s window // holds at most one intended pass -> <=1 connect per provider. 3x headroom: const CONNECT_BOUND_PER_PROVIDER = 3; // den-api's access log normalizes the path to the parameterized route // (ee/apps/den-api/src/observability/hono.ts:66-72), so connect calls cannot // be split per provider id; every pass touches every provider exactly once, // which makes per-provider rate = total / provider count. const CONNECT_ROUTE = "/v1/llm-providers/:llmProviderId/connect"; const requirements: TestNeeds = { optIn: ["OPENWORK_EVAL_E2E_TESTS"] }; const missingRequirements = unmetNeeds(requirements, process.env); const title = missingRequirements.length > 0 ? `cloud provider sync contract skipped — needs: ${missingRequirements.join(", ")}` : "an org-published provider's desktop sync reaches a terminal, truthful status"; interface PublishedProvider { cloudId: string; name: string; modelId: string; } interface ProviderStatusEntry { cloudProviderId: string; providerId: string; sourceProviderId: string; name: string; source: string; modelIds: string[]; } interface SyncStatusFacts { hasSession: boolean; lastRunStatus: "applied" | "noop" | "failed" | "none"; lastRunAt: string; lastRunMessage: string; reloadDeferred: boolean; providers: ProviderStatusEntry[]; raw: Record; } interface ConnectLogEntry { timestamp: string; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function auth(session: DenSession): Record { return { authorization: `Bearer ${session.token}` }; } async function organizationId(session: DenSession): Promise { const result = await denFetch(session, "/v1/me/orgs", { headers: auth(session), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); const organizations = isRecord(result.body) && Array.isArray(result.body.orgs) ? result.body.orgs.filter(isRecord) : []; const organization = organizations.find((entry) => entry.name === ORGANIZATION_NAME); const id = organization && typeof organization.id === "string" ? organization.id : ""; if (!result.response.ok || !id) { throw new Error(`Finding the test organization failed: HTTP ${result.response.status} ${result.text.slice(0, 500)}`); } return id; } async function createProvider(admin: DenSession, orgId: string, body: Record): Promise { const result = await denFetch(admin, "/v1/llm-providers", { method: "POST", headers: { ...auth(admin), "x-openwork-org-id": orgId }, body: JSON.stringify(body), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); const provider = isRecord(result.body) && isRecord(result.body.llmProvider) ? result.body.llmProvider : null; const id = provider && typeof provider.id === "string" ? provider.id : ""; if (result.response.status !== 201 || !id) { throw new Error(`Creating provider ${JSON.stringify(body.name)} failed: HTTP ${result.response.status} ${result.text.slice(0, 500)}`); } return id; } async function deleteProvider(admin: DenSession, orgId: string, providerId: string): Promise { await denFetch(admin, `/v1/llm-providers/${encodeURIComponent(providerId)}`, { method: "DELETE", headers: { ...auth(admin), "x-openwork-org-id": orgId }, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); } async function memberVisibleProviderIds(member: DenSession, orgId: string): Promise { const result = await denFetch(member, "/v1/llm-providers", { headers: { ...auth(member), "x-openwork-org-id": orgId }, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); if (!result.response.ok) { throw new Error(`Listing member-visible providers failed: HTTP ${result.response.status} ${result.text.slice(0, 500)}`); } const providers = isRecord(result.body) && Array.isArray(result.body.llmProviders) ? result.body.llmProviders.filter(isRecord) : []; return providers.flatMap((entry) => (typeof entry.id === "string" ? [entry.id] : [])); } function parseSyncStatus(payload: Record): SyncStatusFacts { const lastRun = isRecord(payload.lastRun) ? payload.lastRun : {}; const detail = isRecord(lastRun.detail) ? lastRun.detail : {}; const lastRunStatus = lastRun.status === "applied" || lastRun.status === "noop" || lastRun.status === "failed" ? lastRun.status : "none"; const providers = Array.isArray(payload.providers) ? payload.providers.filter(isRecord) : []; return { hasSession: payload.hasSession === true, lastRunStatus, lastRunAt: typeof lastRun.at === "string" ? lastRun.at : "", lastRunMessage: typeof lastRun.message === "string" ? lastRun.message : "", reloadDeferred: detail.reloadDeferred === true, providers: providers.map((entry) => ({ cloudProviderId: typeof entry.cloudProviderId === "string" ? entry.cloudProviderId : "", providerId: typeof entry.providerId === "string" ? entry.providerId : "", sourceProviderId: typeof entry.sourceProviderId === "string" ? entry.sourceProviderId : "", name: typeof entry.name === "string" ? entry.name : "", source: typeof entry.source === "string" ? entry.source : "", modelIds: Array.isArray(entry.modelIds) ? entry.modelIds.filter((id): id is string => typeof id === "string") : [], })), raw: payload, }; } // The desktop local server's GET /cloud-provider-sync/status is registered // with "client" auth (apps/server/src/server.ts:2108), so the renderer's own // persisted credentials (localStorage openwork.server.port/openwork.server.token) // reach it with a plain Bearer fetch to 127.0.0.1 — the same access pattern // subagent-run-survives-provider-sync-storm.e2e.test.ts uses. async function readSyncStatusPayload(surface: Parameters[0]): Promise> { const value = await evalIn(surface, `(async () => { const port = localStorage.getItem("openwork.server.port"); const token = localStorage.getItem("openwork.server.token"); if (!port || !token) return { specProbeError: "missing local server credentials" }; const response = await fetch("http://127.0.0.1:" + port + "/cloud-provider-sync/status", { headers: { Authorization: "Bearer " + token }, }); if (!response.ok) return { specProbeError: "HTTP " + response.status + " " + (await response.text()).slice(0, 200) }; return await response.json(); })()`, { awaitPromise: true, timeoutMs: 30_000 }); if (!isRecord(value)) { throw new Error(`GET /cloud-provider-sync/status returned a non-object: ${JSON.stringify(value)}`); } if (typeof value.specProbeError === "string") { throw new Error(`GET /cloud-provider-sync/status unreachable: ${value.specProbeError}`); } return value; } function connectLogEntries(logText: string): ConnectLogEntry[] { const entries: ConnectLogEntry[] = []; for (const line of logText.split(/\r?\n/)) { // Local-lane api.log interleaves pnpm/tsx chatter with the JSON lines, so // parse from the first brace and skip anything that is not a JSON object. const start = line.indexOf("{"); if (start === -1) continue; let parsed: unknown; try { parsed = JSON.parse(line.slice(start)); } catch { continue; } if (!isRecord(parsed) || parsed.http_route !== CONNECT_ROUTE) continue; entries.push({ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : "" }); } return entries; } test.skipIf(missingRequirements.length > 0)(title, { timeout: 30 * 60_000 }, async ({ evidence, place }) => { needs(requirements); await using den = await server({ place, org: { name: ORGANIZATION_NAME, admin: { name: "Sync Admin" }, members: { member: { name: "Sync Member" } }, }, }); const member = den.members.member; if (!member) throw new Error("The testkit did not provision the organization member."); const orgId = await organizationId(den.admin); // Both org-wide providers are published BEFORE the desktop boots, so the // very first sign-in sync owes the member both of them. const customProviderId = await createProvider(den.admin, orgId, { name: CUSTOM_PROVIDER_NAME, source: "custom", customConfig: { id: CUSTOM_PROVIDER_KEY, name: CUSTOM_PROVIDER_NAME, npm: "@ai-sdk/openai-compatible", env: ["SYNC_CONTRACT_PROVIDER_API_KEY"], models: [{ id: CUSTOM_MODEL_ID, name: "Sync Contract Custom Model" }], }, apiKey: "sk-openwork-sync-contract-eval-only", allMembers: true, memberIds: [], teamIds: [], }); onTestFinished(async () => { await deleteProvider(den.admin, orgId, customProviderId).catch(() => undefined); }); const catalogProviderId = await createProvider(den.admin, orgId, { name: CATALOG_PROVIDER_NAME, source: "models_dev", providerId: "openai", modelIds: [CATALOG_MODEL_ID], apiKey: "sk-openwork-sync-contract-eval-only", allMembers: true, memberIds: [], teamIds: [], }); onTestFinished(async () => { await deleteProvider(den.admin, orgId, catalogProviderId).catch(() => undefined); }); const published: PublishedProvider[] = [ { cloudId: customProviderId, name: CUSTOM_PROVIDER_NAME, modelId: CUSTOM_MODEL_ID }, { cloudId: catalogProviderId, name: CATALOG_PROVIDER_NAME, modelId: CATALOG_MODEL_ID }, ]; // Access-wiring gate, so a red claim 1 can only mean sync: Den itself must // already list both providers for this member. const grantedIds = await memberVisibleProviderIds(member, orgId); for (const provider of published) { if (!grantedIds.includes(provider.cloudId)) { throw new Error( `Setup failed before any sync claim: Den does not list ${provider.name} (${provider.cloudId}) for the member. ` + `Member-visible provider ids: ${JSON.stringify(grantedIds)}`, ); } } await using desktopApp = await app({ den, as: "member", place }); // ── Claim 1: terminal status ──────────────────────────────────────────── const isTerminal = (status: SyncStatusFacts): boolean => published.every((provider) => status.providers.some((entry) => entry.cloudProviderId === provider.cloudId)) && (status.lastRunStatus === "applied" || status.lastRunStatus === "noop"); const terminalDeadline = Date.now() + TERMINAL_BUDGET_MS; let latestStatus: SyncStatusFacts | null = null; let terminalReached = false; let lastStatusReadError = ""; while (Date.now() < terminalDeadline) { try { latestStatus = parseSyncStatus(await readSyncStatusPayload(desktopApp)); if (isTerminal(latestStatus)) { terminalReached = true; break; } } catch (error) { lastStatusReadError = error instanceof Error ? error.message : String(error); } await sleep(3_000); } const terminalSettledAtMs = Date.now(); const settledStatus = latestStatus; if (!settledStatus) { throw new Error( `GET /cloud-provider-sync/status never returned a readable payload within ${TERMINAL_BUDGET_MS / 1000}s of desktop sign-in` + `${lastStatusReadError ? ` (last error: ${lastStatusReadError})` : ""}; no sync claim was reached.`, ); } const absentFromStatus = published .filter((provider) => !settledStatus.providers.some((entry) => entry.cloudProviderId === provider.cloudId)) .map((provider) => `${provider.name} (${provider.cloudId})`); evidence.recordAssertionEvidence( "Both org-published providers reach a terminal per-provider sync outcome within 120s", `Published ${JSON.stringify(published.map((provider) => provider.cloudId))}; latest /cloud-provider-sync/status: ${JSON.stringify(settledStatus.raw)}`, terminalReached, ); expect(terminalReached, [ "Claim 1 (terminal status): every Den-granted provider must appear in /cloud-provider-sync/status providers[] with a settled run (lastRun applied|noop) within 120s; a provider may not stay absent or permanently non-terminal.", `Still absent from status: ${JSON.stringify(absentFromStatus)}; lastRun status: ${settledStatus.lastRunStatus}${settledStatus.lastRunMessage ? ` (${settledStatus.lastRunMessage})` : ""}.`, `Latest status payload: ${JSON.stringify(settledStatus.raw)}`, ].join("\n")).toBe(true); // ── Claim 2: truthfulness — a status entry means the engine serves it ─── const syncedProviders = published.filter((provider) => settledStatus.providers.some((entry) => entry.cloudProviderId === provider.cloudId)); const expectedModelIds = syncedProviders.map((provider) => provider.modelId); let lastModels: ModelFacts[] = []; const modelsArrived = await eventually(async () => { lastModels = await readAvailableModels(desktopApp); return lastModels; }, { within: MODEL_BUDGET_MS, intervalMs: 3_000, label: "engine models for status-synced providers", until: (models) => expectedModelIds.every((id) => models.some((model) => model.id === id)), }).then(() => true, () => false); const missingModelIds = expectedModelIds.filter((id) => !lastModels.some((model) => model.id === id)); evidence.recordAssertionEvidence( "Every provider the sync status lists actually contributes its model to the engine within 30s", `Status-synced providers claim ${JSON.stringify(expectedModelIds)}; engine picker ids: ${JSON.stringify(lastModels.map((model) => model.id))}`, modelsArrived, ); expect(modelsArrived, [ "Claim 2 (truthfulness): the sync status lists these providers, but the engine never served their models within 30s — the fingerprint was recorded as done while the engine was never reloaded (the stuck-'Syncing' settings row).", `Missing model ids: ${JSON.stringify(missingModelIds)}.`, `Status entries for the published providers: ${JSON.stringify(settledStatus.providers.filter((entry) => published.some((provider) => provider.cloudId === entry.cloudProviderId)))}.`, `Latest status payload: ${JSON.stringify(settledStatus.raw)}`, `Engine picker models: ${JSON.stringify(lastModels)}`, ].join("\n")).toBe(true); // readAvailableModels leaves the picker dialog open; close it through its // own close control so later claims observe a calm, unobstructed app. await evalIn(desktopApp, `(() => { const close = document.querySelector('[data-slot="dialog-content"] [data-slot="dialog-close"]'); if (close instanceof HTMLElement) close.click(); return true; })()`); await waitFor(desktopApp, `!document.querySelector('[data-slot="dialog-content"]')`, { timeoutMs: 15_000, label: "model picker dialog closed", }); // ── Claim 3: loud skips — a dropped provider must name itself ─────────── // The schema (apps/server/src/cloud-provider-sync.ts:38-69) carries no // per-provider skipped/errored state: its only error carrier is // lastRun.status="failed" plus lastRun.message, and a skipped provider // manifests as absence from providers[] (prepareMaterialization silently // drops entries without env matches, cloud-provider-sync.ts:414-426). The // loud-skip contract at this schema is therefore: a failed run must carry a // non-empty message, and a published provider absent from status is only // acceptable when such an explaining failure exists. const currentStatus = parseSyncStatus(await readSyncStatusPayload(desktopApp)); const droppedNow = published .filter((provider) => !currentStatus.providers.some((entry) => entry.cloudProviderId === provider.cloudId)) .map((provider) => `${provider.name} (${provider.cloudId})`); const failureExplained = currentStatus.lastRunStatus !== "failed" || currentStatus.lastRunMessage.trim().length > 0; const dropsExplained = droppedNow.length === 0 || (currentStatus.lastRunStatus === "failed" && currentStatus.lastRunMessage.trim().length > 0); const loudSkips = failureExplained && dropsExplained; evidence.recordAssertionEvidence( "No provider is skipped or errored silently: failures carry a reason and drops are explained", `Dropped from status: ${JSON.stringify(droppedNow)}; lastRun: ${currentStatus.lastRunStatus}${currentStatus.lastRunMessage ? ` (${currentStatus.lastRunMessage})` : " (no message)"}; payload: ${JSON.stringify(currentStatus.raw)}`, loudSkips, ); expect(loudSkips, [ "Claim 3 (loud skips): a failed sync run must carry a non-empty message, and a published provider missing from status must be accompanied by such an explained failure — never silently dropped.", `Dropped without explanation: ${JSON.stringify(droppedNow)}; lastRun status: ${currentStatus.lastRunStatus}; message: ${JSON.stringify(currentStatus.lastRunMessage)}.`, `Latest status payload: ${JSON.stringify(currentStatus.raw)}`, ].join("\n")).toBe(true); // ── Claim 4: calm loop — the settled sync must not hammer den-api ─────── // Observation window: >=30s after claim 1 settled, then 60s wide. The // window is bounded by two den.apiLog() snapshots (count diffing), so the // sandbox/driver clock skew cannot bend it; the JSON lines' own timestamps // travel into the failure message to pin any ~500ms hammering bursts. const sinceSettleMs = Date.now() - terminalSettledAtMs; if (sinceSettleMs < QUIET_DELAY_MS) await sleep(QUIET_DELAY_MS - sinceSettleMs); const connectsBefore = connectLogEntries(await den.apiLog()); await sleep(OBSERVATION_WINDOW_MS); const connectsAfter = connectLogEntries(await den.apiLog()); const windowConnectCount = connectsAfter.length - connectsBefore.length; const windowTimestamps = connectsAfter.slice(connectsBefore.length).map((entry) => entry.timestamp); const connectBound = CONNECT_BOUND_PER_PROVIDER * published.length; const calmLoop = windowConnectCount >= 0 && windowConnectCount <= connectBound; const statusAfterWindow = parseSyncStatus(await readSyncStatusPayload(desktopApp)); evidence.recordAssertionEvidence( "The settled sync loop polls at its intended cadence instead of hammering /connect", `${windowConnectCount} ${CONNECT_ROUTE} calls in a ${OBSERVATION_WINDOW_MS / 1000}s window across ${published.length} providers ` + `(bound ${connectBound} = ${CONNECT_BOUND_PER_PROVIDER}/provider from the 5-minute cadence at apps/server/src/cloud-provider-sync.ts:132); ` + `window call timestamps: ${JSON.stringify(windowTimestamps)}`, calmLoop, ); expect(calmLoop, [ `Claim 4 (calm loop): a settled sync intends <=1 pass (one /connect per provider) per 60s window at the 5-minute cadence (apps/server/src/cloud-provider-sync.ts:132); observed ${windowConnectCount} connect calls against a 3x-headroom bound of ${connectBound} (log counts ${connectsBefore.length} -> ${connectsAfter.length}).`, `Window call timestamps (pins list+connect rounds landing within ~500ms): ${JSON.stringify(windowTimestamps)}.`, `Latest status payload: ${JSON.stringify(statusAfterWindow.raw)}`, ].join("\n")).toBe(true); // ── Claim 5: the settings surface stands, bug present or fixed ────────── const settingsRoute = `/workspace/${desktopApp.workspaceId}/settings/cloud-providers`; await go(desktopApp, settingsRoute); await waitFor(desktopApp, `(() => { if (!window.location.hash.includes('/settings/cloud-providers')) { window.location.hash = ${JSON.stringify(`#${settingsRoute}`)}; return false; } return /provider/i.test(document.body.innerText); })()`, { timeoutMs: 60_000, label: "cloud providers settings surface" }); const shot = await screenshot(desktopApp); const seen = await validate(shot, [ "The workspace settings cloud providers surface is visible", "No crash text such as 'Something went wrong' is visible", ]); evidence.recordAssertionEvidence( "The cloud providers settings surface renders without crashing", `Validated at ${settingsRoute}: ${seen.ok ? "ok" : seen.why}`, seen.ok, ); expect(seen.ok, `${seen.why}\nLatest status payload: ${JSON.stringify(statusAfterWindow.raw)}`).toBe(true); });