Files
openwork/evals/specs/cloud-provider-sync-contract.e2e.test.ts
T

458 lines
23 KiB
TypeScript

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<string, unknown>;
}
interface ConnectLogEntry {
timestamp: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function auth(session: DenSession): Record<string, string> {
return { authorization: `Bearer ${session.token}` };
}
async function organizationId(session: DenSession): Promise<string> {
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<string, unknown>): Promise<string> {
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<void> {
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<string[]> {
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<string, unknown>): 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<typeof evalIn>[0]): Promise<Record<string, unknown>> {
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);
});