Files
openwork/apps/app/tests/cloud-mcp-submit-readiness.test.ts

508 lines
18 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type {
OpenworkCloudMcpFailure,
OpenworkCloudMcpHealth,
} from "../src/app/lib/openwork-server";
import {
createCloudMcpSubmissionCoordinator,
clearCloudMcpSubmissionFailure,
decideCloudMcpSubmissionGate,
ensureCloudMcpSubmissionReadiness,
IDLE_CLOUD_MCP_SUBMISSION_GATE_STATE,
resolveCloudMcpSubmissionAuth,
type CloudMcpSubmissionGateDecision,
type CloudMcpSubmissionPreparationResult,
} from "../src/react-app/domains/connections/cloud-mcp-submit-readiness";
const PROVIDER_MODEL = { provider: "openwork", model: "gpt-5" };
function failure(input?: Partial<OpenworkCloudMcpFailure>): OpenworkCloudMcpFailure {
return {
code: input?.code ?? "cloud_registration_failed",
stage: input?.stage ?? "engine_delivery",
retryable: input?.retryable ?? true,
recommendedAction: input?.recommendedAction ?? "Retry",
message: input?.message ?? "Cloud tools are not ready.",
};
}
function health(input?: {
usable?: boolean;
usableByCurrentModel?: boolean | null;
firstFailure?: OpenworkCloudMcpFailure | null;
projectionSource?: "experimental_tool" | "provider_capability";
projectionChecked?: boolean;
modelExists?: boolean;
toolCalling?: boolean | null;
}): OpenworkCloudMcpHealth {
const usable = input?.usable ?? true;
const projectionSource = input?.projectionSource ?? "experimental_tool";
const projected = usable
? ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"]
: [];
const projectionPresent = projectionSource === "experimental_tool" ? projected : [];
const direct = usable ? ["search_capabilities", "execute_capability"] : [];
return {
schemaVersion: 1,
phase: usable ? "ready" : "registration_failed",
usable,
usableByCurrentModel: input?.usableByCurrentModel === undefined ? usable : input.usableByCurrentModel,
connectCatalogEnabled: true,
workspace: { id: "workspace_1", type: "local", directory: "/workspace", path: "/workspace" },
desired: {
present: true,
name: "openwork-cloud",
revision: "rev_1",
config: { type: "remote", enabled: true },
token: { present: true, metadata: {} },
},
delivery: {
state: usable ? "ready" : "failed",
desiredRevision: "rev_1",
appliedRevision: usable ? "rev_1" : null,
updatedAt: 1,
appliedAt: usable ? 1 : null,
lastAttemptAt: 1,
},
engine: { status: usable ? "connected" : "missing" },
tools: {
expected: ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"],
present: projected,
missing: usable ? [] : ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"],
direct: {
checked: true,
source: "mcp_tools_list",
expected: ["search_capabilities", "execute_capability"],
present: direct,
missing: usable ? [] : ["search_capabilities", "execute_capability"],
},
providerProjection: {
checked: input?.projectionChecked ?? true,
provider: PROVIDER_MODEL.provider,
model: PROVIDER_MODEL.model,
source: projectionSource,
...(projectionSource === "provider_capability"
? {
limitation: "Only generic model tool-call capability is available.",
modelExists: input?.modelExists ?? true,
toolCalling: input?.toolCalling === undefined ? true : input.toolCalling,
}
: {}),
present: projectionPresent,
missing: projectionPresent.length ? [] : ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"],
},
},
pluginCanaries: { expected: ["openwork_docs_search"], present: usable ? ["openwork_docs_search"] : [], missing: usable ? [] : ["openwork_docs_search"] },
compatibility: {
openwork: { serverVersion: "test", app: null },
opencode: { expectedVersion: "test", actualVersion: "test", probe: "ok" },
pluginFileHashes: [],
supportedFeatures: { dynamicMcp: true, directoryScoping: true, toolIds: true, providerToolProjection: projectionSource === "experimental_tool", pluginCanaries: true },
experimentalToolIds: {
checked: true,
expected: ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"],
present: projected,
missing: usable ? [] : ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"],
includesMcpTools: usable,
},
experimentalProviderTools: {
checked: true,
provider: PROVIDER_MODEL.provider,
model: PROVIDER_MODEL.model,
expected: ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"],
present: projected,
missing: usable ? [] : ["openwork-cloud_search_capabilities", "openwork-cloud_execute_capability"],
includesMcpTools: projectionSource === "experimental_tool" && usable,
},
},
toolDenies: [],
firstFailure: usable ? input?.firstFailure ?? null : input?.firstFailure ?? failure(),
checkedAt: "2026-07-15T00:00:00.000Z",
};
}
function requiredDecision(input?: {
authStatus?: "checking" | "signed_in" | "unavailable" | "signed_out";
hasSessionToken?: boolean;
userState?: "disabled" | "removed" | null;
workspaceId?: string;
model?: string;
}) {
return decideCloudMcpSubmissionGate({
cloudAuthStatus: input?.authStatus ?? "signed_in",
cloudHasSessionToken: input?.hasSessionToken ?? true,
denBaseUrl: "https://app.openwork.test",
serverBaseUrl: "https://worker.openwork.test",
orgId: "org_1",
workspaceId: input?.workspaceId ?? "workspace_1",
providerModel: { ...PROVIDER_MODEL, model: input?.model ?? PROVIDER_MODEL.model },
userState: input?.userState ?? null,
});
}
function preparation(input: {
check: () => Promise<OpenworkCloudMcpHealth | null>;
repair: () => Promise<OpenworkCloudMcpHealth | null>;
}): () => Promise<CloudMcpSubmissionPreparationResult> {
return async () => {
const result = await ensureCloudMcpSubmissionReadiness({
providerModel: PROVIDER_MODEL,
check: input.check,
repair: input.repair,
retryDelaysMs: [0],
attemptTimeoutMs: 0,
});
if (result.outcome === "ready") return { outcome: "ready" };
if (result.outcome === "bypass") return { outcome: "bypass" };
return { outcome: "failed", issue: result.issue };
};
}
describe("Cloud MCP pre-send readiness", () => {
test("a successful provider refresh clears only a stale failure", () => {
const failed = {
status: "failed" as const,
issue: failure({ code: "provider_tool_projection_missing" }),
attempt: 3,
maxAttempts: 3,
};
const checking = { ...failed, status: "checking" as const };
expect(clearCloudMcpSubmissionFailure(failed)).toEqual(IDLE_CLOUD_MCP_SUBMISSION_GATE_STATE);
expect(clearCloudMcpSubmissionFailure(checking)).toBe(checking);
});
test("ready projected tools send immediately", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const decision = requiredDecision();
let checks = 0;
let repairs = 0;
let runs = 0;
const result = await coordinator.submit({
scopeKey: decision.scopeKey,
prepare: preparation({
check: async () => {
checks += 1;
return health();
},
repair: async () => {
repairs += 1;
return health();
},
}),
send: async () => {
runs += 1;
},
});
expect(result).toEqual({ outcome: "sent", bypassed: false });
expect({ checks, repairs, runs }).toEqual({ checks: 1, repairs: 0, runs: 1 });
});
test("a transient startup failure repairs and sends exactly once", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const decision = requiredDecision();
let repairs = 0;
let runs = 0;
const result = await coordinator.submit({
scopeKey: decision.scopeKey,
prepare: preparation({
check: async () => health({ usable: false, firstFailure: failure({ retryable: true }) }),
repair: async () => {
repairs += 1;
return health();
},
}),
send: async () => {
runs += 1;
},
});
expect(result).toEqual({ outcome: "sent", bypassed: false });
expect(repairs).toBe(1);
expect(runs).toBe(1);
});
test("a cached Cloud session waits for auth restoration and sends exactly once", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const checking = requiredDecision({ authStatus: "checking", hasSessionToken: true });
const signedIn = requiredDecision();
expect(checking.mode).toBe("waiting_for_auth");
expect(checking.scopeKey).toBe(signedIn.scopeKey);
let resolveAuth: ((decision: CloudMcpSubmissionGateDecision) => void) | null = null;
const authResolution = new Promise<CloudMcpSubmissionGateDecision>((resolve) => {
resolveAuth = resolve;
});
let checks = 0;
let runs = 0;
const input = {
scopeKey: checking.scopeKey,
prepare: async (): Promise<CloudMcpSubmissionPreparationResult> => {
const resolution = await resolveCloudMcpSubmissionAuth({
decision: checking,
waitForResolution: () => authResolution,
timeoutMs: 0,
});
if (resolution.outcome === "failed") {
return { outcome: "failed", issue: resolution.issue };
}
if (resolution.decision.mode === "bypass") return { outcome: "bypass" };
if (resolution.decision.mode !== "required") {
return { outcome: "cancelled", reason: "context_changed" };
}
return preparation({
check: async () => {
checks += 1;
return health();
},
repair: async () => health(),
})();
},
send: async () => {
runs += 1;
},
};
const first = coordinator.submit(input);
const second = coordinator.submit(input);
expect(second).toBe(first);
expect({ checks, runs }).toEqual({ checks: 0, runs: 0 });
resolveAuth?.(signedIn);
await expect(first).resolves.toEqual({ outcome: "sent", bypassed: false });
await expect(second).resolves.toEqual({ outcome: "sent", bypassed: false });
expect({ checks, runs }).toEqual({ checks: 1, runs: 1 });
});
test("an auth restoration timeout creates no run", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const checking = requiredDecision({ authStatus: "checking", hasSessionToken: true });
let runs = 0;
const result = await coordinator.submit({
scopeKey: checking.scopeKey,
prepare: async () => {
const resolution = await resolveCloudMcpSubmissionAuth({
decision: checking,
waitForResolution: () => new Promise<CloudMcpSubmissionGateDecision>(() => undefined),
timeoutMs: 1,
});
if (resolution.outcome === "failed") {
return { outcome: "failed", issue: resolution.issue };
}
return { outcome: "ready" };
},
send: async () => {
runs += 1;
},
});
expect(result).toMatchObject({
outcome: "blocked",
issue: { code: "cloud_mcp_auth_resolution_timeout" },
});
expect(runs).toBe(0);
});
test("a permanent injection failure creates no run and preserves the exact draft", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const decision = requiredDecision();
const originalDraft = {
text: "Search my connected services",
attachments: [{ id: "attachment_1", name: "brief.pdf" }],
};
let storedDraft = originalDraft;
let runs = 0;
const result = await coordinator.submit({
scopeKey: decision.scopeKey,
prepare: preparation({
check: async () => health({
usable: false,
firstFailure: failure({
code: "provider_tool_projection_missing",
stage: "provider_projection",
retryable: false,
message: "The selected model is missing an injected Cloud tool.",
}),
}),
repair: async () => health(),
}),
send: async () => {
runs += 1;
storedDraft = { text: "", attachments: [] };
},
});
expect(result).toMatchObject({ outcome: "blocked", issue: { code: "provider_tool_projection_missing" } });
expect(runs).toBe(0);
expect(storedDraft).toEqual(originalDraft);
});
test("provider tool-call capability plus verified direct tools is accepted as projection proof", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const decision = requiredDecision();
let checks = 0;
let repairs = 0;
let runs = 0;
const result = await coordinator.submit({
scopeKey: decision.scopeKey,
prepare: preparation({
check: async () => {
checks += 1;
return health({ projectionSource: "provider_capability", usableByCurrentModel: true });
},
repair: async () => {
repairs += 1;
return health();
},
}),
send: async () => {
runs += 1;
},
});
expect(result).toEqual({ outcome: "sent", bypassed: false });
expect({ checks, repairs, runs }).toEqual({ checks: 1, repairs: 0, runs: 1 });
});
test("provider capability without model tool calling is blocked without retry", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const decision = requiredDecision();
let runs = 0;
const result = await coordinator.submit({
scopeKey: decision.scopeKey,
prepare: preparation({
check: async () => health({
projectionSource: "provider_capability",
usableByCurrentModel: false,
modelExists: true,
toolCalling: false,
}),
repair: async () => health(),
}),
send: async () => {
runs += 1;
},
});
expect(result).toMatchObject({
outcome: "blocked",
issue: {
code: "provider_tool_calling_unavailable",
retryable: false,
message: "The selected model does not support tool calling.",
},
});
expect(runs).toBe(0);
});
test("an unchecked provider projection is blocked", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const decision = requiredDecision();
let runs = 0;
const unchecked = health({ projectionChecked: false });
const result = await coordinator.submit({
scopeKey: decision.scopeKey,
prepare: preparation({
check: async () => unchecked,
repair: async () => unchecked,
}),
send: async () => {
runs += 1;
},
});
expect(result).toMatchObject({
outcome: "blocked",
issue: { code: "provider_tool_projection_unverified", retryable: true },
});
expect(runs).toBe(0);
});
test("repeated clicks share one queued submission and cannot duplicate the send", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const decision = requiredDecision();
let release: ((result: CloudMcpSubmissionPreparationResult) => void) | null = null;
const pending = new Promise<CloudMcpSubmissionPreparationResult>((resolve) => {
release = resolve;
});
let runs = 0;
const input = {
scopeKey: decision.scopeKey,
prepare: () => pending,
send: async () => {
runs += 1;
},
};
const first = coordinator.submit(input);
const second = coordinator.submit(input);
expect(second).toBe(first);
release?.({ outcome: "ready" });
await Promise.all([first, second]);
expect(runs).toBe(1);
});
test("signed-out, tokenless startup, and explicitly disabled or removed Cloud bypass readiness entirely", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
let preparations = 0;
let runs = 0;
for (const decision of [
requiredDecision({ authStatus: "signed_out", hasSessionToken: false }),
requiredDecision({ authStatus: "checking", hasSessionToken: false }),
requiredDecision({ userState: "disabled" }),
requiredDecision({ userState: "removed" }),
]) {
await coordinator.submit({
scopeKey: decision.scopeKey,
...(decision.mode === "required"
? {
prepare: async () => {
preparations += 1;
return { outcome: "ready" };
},
}
: {}),
send: async () => {
runs += 1;
},
});
}
expect(preparations).toBe(0);
expect(runs).toBe(4);
});
test("workspace or model changes cannot release an old queued message", async () => {
const coordinator = createCloudMcpSubmissionCoordinator();
const original = requiredDecision({ workspaceId: "workspace_a", model: "gpt-5" });
const replacement = requiredDecision({ workspaceId: "workspace_b", model: "gpt-5-mini" });
let releaseOriginal: ((result: CloudMcpSubmissionPreparationResult) => void) | null = null;
const originalPreparation = new Promise<CloudMcpSubmissionPreparationResult>((resolve) => {
releaseOriginal = resolve;
});
const sentContexts: string[] = [];
const originalResult = coordinator.submit({
scopeKey: original.scopeKey,
prepare: () => originalPreparation,
send: async () => {
sentContexts.push("workspace_a/gpt-5");
},
});
const replacementResult = coordinator.submit({
scopeKey: replacement.scopeKey,
prepare: async () => ({ outcome: "ready" }),
send: async () => {
sentContexts.push("workspace_b/gpt-5-mini");
},
});
releaseOriginal?.({ outcome: "ready" });
await expect(originalResult).resolves.toEqual({ outcome: "cancelled", reason: "context_changed" });
await expect(replacementResult).resolves.toEqual({ outcome: "sent", bypassed: false });
expect(sentContexts).toEqual(["workspace_b/gpt-5-mini"]);
});
});