evOo-local/apps/api/src/router.test.ts
cynarAI b78eff1f7b
Run on the deployment's local models without any model choice, preselect Claude Opus 5.5 otherwise, explain why dictation is unavailable (#151)
* Recommend Claude Opus 5.5 by bumping the Pi model catalog

The pinned pi-ai 0.85.1 had no Claude Opus 5.5, so the connect flow fell
back to Opus 5. pi-ai and pi-agent-core 0.87.1 add it, and the keyless
recommendation now preselects it.

The new catalog names router aliases "Qwen Max Latest (Qwen3.8 Max)";
those now read "(auto-updates)" like the other alias labels, so no
picker entry claims to be "latest".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Name HTTPS as the cause when dictation is unavailable, and translate it

Over plain HTTP on a LAN address, Safari hides the microphone and speech
recognition, so voice mode said the browser had no dictation, in English
inside a German UI. Dictation now checks for a secure context first and
says to open the address with https://. Its other errors go through
Lingui too, and Safari's blocked recognizer gets a clear message instead
of "Dictation failed: not-allowed".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Serve a fake Kokoro voice server in the E2E harness

Onboarding's voice step connects to the local Kokoro server on its
default port, so the shell E2E failed on any machine where nothing (or a
misbehaving server) listened there: the failed request shows up as a
browser console error. The harness now starts an offline stand-in and
points KOKORO_BASE_URL at it, so the suite no longer depends on the
runner host's own voice server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Show only the recommended sign-in until a model is connected

With no model connected, "Connect model" opened the full model settings:
provider list, tiers and a model picker, even though the recommendation
was already preselected. Until something is connected, the Models panel
now shows just the preselected model with its sign-in and API key field;
"Other providers" reveals the full settings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Give the second chart-links test the same cold-load allowance as the first

Each Playwright test runs in a fresh browser context, so the second test's
first navigation re-fetches the lazily imported Plot chunk just like the
first test's warm-up does. On a loaded CI runner that exceeded the default
20 s expect timeout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Use the recommended model as the Anthropic deployment default

A deployment that sets only ANTHROPIC_API_KEY and PI_DEFAULT_PROVIDER=anthropic
now runs Claude Opus 5.5, the same model the connect flow preselects, instead
of an older default.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Run every space on the deployment's local models without a connect step

A deployment with PI_DEFAULT_PROVIDER=local and RAKAZO_LOCAL_MODELS set still
asked every user to connect a model, because only OpenRouter and Anthropic
counted as a deployment model. Configured local models now do, keylessly.

RAKAZO_LOCAL_TIERS assigns local models to the tiers the agent delegates to,
for spaces that have not mapped their own, and a RAKAZO_LOCAL_MODELS entry can
name its own server (id=url) when the models run behind different endpoints.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Hide model choices when the deployment routes models itself

When the server provides its own models, the system picks and routes
them per task, so Settings no longer offers Models and bot settings no
longer offer Model and Thinking, on web and mobile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Wait for the workspace pane's width transition before measuring it

The pane animates its width over 300 ms, so a single boundingBox read
right after it opens can land mid-transition on a slow runner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

* Allow chart re-renders inside the chart-links loops to take longer

Each navigation in the loops renders Observable Plot again. On a loaded
CI runner that exceeded the default expect timeout even after the
warm-up, so the per-iteration visibility checks get 30 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-05 23:18:40 +02:00

1054 lines
35 KiB
TypeScript

import { RPCHandler } from "@orpc/server/fetch";
import { COMPUTER_SCREEN_UNAVAILABLE, ComputerScreenUnavailableError } from "@rakazo/adapters";
import type { Actor } from "@rakazo/contracts";
import { REPLY_QUOTE_MAX_LENGTH } from "@rakazo/contracts";
import { openScreenCapability } from "@rakazo/core/node/screen-capability";
import type { PrismaClient } from "@rakazo/db";
import { createLogger, createTestSink, installLogger } from "@rakazo/logging";
import { describe, expect, it, vi } from "vitest";
import { createRouter, type RouterDeps } from "./router.js";
describe("account preferences", () => {
function preferencesDeps(avatarStyle: string) {
const update = vi.fn().mockResolvedValue({});
const prisma = {
user: {
update,
findUniqueOrThrow: vi.fn().mockResolvedValue({
email: "user@rakazo.test",
name: "Test User",
avatarStyle,
}),
},
spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) },
userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) },
deploymentSettings: { findUnique: vi.fn().mockResolvedValue(null) },
} as unknown as PrismaClient;
const deps = {
prisma,
env: {
defaultProvider: "fake",
defaultModel: "fake-model",
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "fake",
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "user@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
return { update, deps, actor, handler: new RPCHandler(createRouter(deps)) };
}
it("keeps an unconfigured catalog offline unless explicitly requested", async () => {
const { actor, deps } = preferencesDeps("robot");
const fetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ results: [] }), {
headers: { "content-type": "application/json" },
}),
);
deps.remoteConnectors = { fetch } as RouterDeps["remoteConnectors"];
const handler = new RPCHandler(createRouter(deps));
const request = async (usePublicCatalog?: boolean) =>
handler.handle(
new Request("http://127.0.0.1/rpc/capabilities/catalogSearch", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { query: "notion", usePublicCatalog } }),
}),
{ prefix: "/rpc", context: { actor } },
);
const { response } = await request();
await expect(response.json()).resolves.toEqual({ json: { enabled: false, results: [] } });
expect(fetch).not.toHaveBeenCalled();
await request(true);
expect(fetch).toHaveBeenCalled();
});
it("persists and returns the selected avatar style", async () => {
const { update, actor, handler } = preferencesDeps("organic");
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/preferences/update", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { avatarStyle: "organic" } }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBe(200);
expect(update).toHaveBeenCalledWith({
where: { id: "user-1" },
data: { avatarStyle: "organic" },
});
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({ avatarStyle: "organic" }),
});
});
it("rejects avatar styles outside robot|organic", async () => {
const { update, actor, handler } = preferencesDeps("robot");
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/preferences/update", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { avatarStyle: "dicebear" } }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBeGreaterThanOrEqual(400);
expect(update).not.toHaveBeenCalled();
});
it("coerces unknown stored avatar styles to robot on me", async () => {
const { actor, handler } = preferencesDeps("custom-cdn");
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/me", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: null }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({ avatarStyle: "robot" }),
});
});
});
describe("model setup gate", () => {
function modelGateDeps(options: {
agentRuntime: string;
deploymentModelKey?: string;
deploymentModelCredentialCipher?: string;
}) {
const prisma = {
user: {
findUniqueOrThrow: vi.fn().mockResolvedValue({
email: "user@rakazo.test",
name: "Test User",
avatarStyle: "robot",
}),
},
spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) },
userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) },
deploymentSettings: {
findUnique: vi
.fn()
.mockResolvedValue(
options.deploymentModelCredentialCipher
? { deploymentModelCredentialCipher: options.deploymentModelCredentialCipher }
: null,
),
},
} as unknown as PrismaClient;
const deps = {
prisma,
env: {
agentRuntime: options.agentRuntime,
defaultProvider: "openrouter",
defaultModel: "test-model",
deploymentModelKey: options.deploymentModelKey,
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "fake",
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "user@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
return { actor, handler: new RPCHandler(createRouter(deps)) };
}
async function call(handler: RPCHandler<never>, actor: Actor, path: string, body: unknown) {
const { response } = await handler.handle(
new Request(`http://127.0.0.1/rpc/${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: body }),
}),
{ prefix: "/rpc", context: { actor } },
);
return response;
}
it("refuses to start a run when no model is configured", async () => {
const { actor, handler } = modelGateDeps({ agentRuntime: "pi" });
const response = await call(handler, actor, "threads/send", {
botId: "bot-1",
text: "hello",
});
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({
code: "BAD_REQUEST",
message: "Connect a model to start a run.",
}),
});
});
it("does not require a model credential for the scripted test runtime", async () => {
const { actor, handler } = modelGateDeps({ agentRuntime: "scripted" });
const response = await call(handler, actor, "me", null);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({ needsModel: false }),
});
});
it("accepts a deployment model key as model configuration", async () => {
const { actor, handler } = modelGateDeps({
agentRuntime: "pi",
deploymentModelKey: "fake-deployment-key",
});
const response = await call(handler, actor, "me", null);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({
needsModel: false,
modelsManaged: true,
defaultProvider: "openrouter",
defaultModel: "test-model",
}),
});
});
it("does not accept a stored deployment cipher the executor cannot use", async () => {
const { actor, handler } = modelGateDeps({
agentRuntime: "pi",
deploymentModelCredentialCipher: "legacy-ciphertext",
});
const response = await call(handler, actor, "me", null);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
// The connect flow opens on the recommendation, not the keyless deployment default.
json: expect.objectContaining({
needsModel: true,
modelsManaged: false,
defaultProvider: "anthropic",
defaultModel: "claude-opus-5-5",
}),
});
});
it("rejects a reply quote without a reply target", async () => {
const { actor, handler } = modelGateDeps({ agentRuntime: "scripted" });
const response = await call(handler, actor, "threads/send", {
botId: "bot-1",
text: "hello",
replyQuote: "just this span",
});
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({
data: expect.objectContaining({
issues: expect.arrayContaining([expect.objectContaining({ path: ["replyQuote"] })]),
}),
}),
});
});
it("rejects an over-length reply quote", async () => {
const { actor, handler } = modelGateDeps({ agentRuntime: "scripted" });
const response = await call(handler, actor, "threads/send", {
botId: "bot-1",
text: "hello",
replyToMessageId: "parent-1",
replyQuote: "x".repeat(REPLY_QUOTE_MAX_LENGTH + 1),
});
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({
data: expect.objectContaining({
issues: expect.arrayContaining([expect.objectContaining({ path: ["replyQuote"] })]),
}),
}),
});
});
});
describe("thread answer delivery", () => {
it("accepts a durable answer when the immediate worker wake fails", async () => {
const answerRunInput = vi.fn().mockResolvedValue(true);
const enqueue = vi.fn().mockRejectedValue(new Error("job broker unavailable"));
const sink = createTestSink();
installLogger(createLogger({ service: "rakazo-api", sinks: [sink] }));
const prisma = {
bot: {
findFirst: vi.fn().mockResolvedValue({
id: "bot-1",
thread: { id: "thread-1" },
computer: null,
}),
},
} as unknown as PrismaClient;
const deps = {
prisma,
events: { answerRunInput },
jobs: { enqueue },
env: {
defaultProvider: "fake",
defaultModel: "fake-model",
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "fake",
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "user@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
const handler = new RPCHandler(createRouter(deps));
const { matched, response } = await handler.handle(
new Request("http://127.0.0.1/rpc/threads/answer", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
json: {
botId: "bot-1",
runId: "run-1",
messageId: "message-1",
answer: "Paris",
},
}),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(matched).toBe(true);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ json: { ok: true } });
expect(answerRunInput).toHaveBeenCalledWith(
expect.objectContaining({
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
}),
);
expect(enqueue).toHaveBeenCalledOnce();
expect(sink.events.some((event) => event.message === "thread answer enqueue")).toBe(true);
installLogger(createLogger({ service: "rakazo-api", level: "off", sinks: [] }));
});
});
describe("MCP server deletion", () => {
it("does not fail when a concurrent credential rotation already removed the old secret", async () => {
const deleteServer = vi.fn().mockResolvedValue({ id: "server-1" });
const deleteSecrets = vi.fn().mockResolvedValue({ count: 0 });
const prisma = {
mcpServer: {
findFirst: vi.fn().mockResolvedValue({ id: "server-1", secretId: "old-secret" }),
delete: deleteServer,
},
secret: { deleteMany: deleteSecrets },
$transaction: vi.fn((operations: Promise<unknown>[]) => Promise.all(operations)),
} as unknown as PrismaClient;
const deps = {
prisma,
env: {
defaultProvider: "fake",
defaultModel: "fake-model",
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "fake",
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "user@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
const handler = new RPCHandler(createRouter(deps));
const { matched, response } = await handler.handle(
new Request("http://127.0.0.1/rpc/mcp/servers/remove", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { id: "server-1" } }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(matched).toBe(true);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ json: { ok: true } });
expect(deleteServer).toHaveBeenCalledWith({ where: { id: "server-1" } });
expect(deleteSecrets).toHaveBeenCalledWith({
where: {
id: "old-secret",
spaceId: "workspace-1",
userId: "user-1",
},
});
});
});
describe("connections.complete", () => {
it("forwards an optional code to the managed connector", async () => {
const complete = vi.fn().mockResolvedValue({ connectionRef: "gmail" });
const connectionReady = vi.fn().mockResolvedValue(true);
const update = vi.fn().mockResolvedValue({
id: "conn-1",
connectorId: "composio",
provider: "gmail",
displayName: "Gmail",
status: "connected",
createdAt: new Date("2026-08-26T00:00:00.000Z"),
});
const prisma = {
connection: {
findFirst: vi.fn().mockResolvedValue({
id: "conn-1",
connectorId: "composio",
provider: "gmail",
displayName: "Gmail",
providerRef: "gmail-state",
status: "pending",
createdAt: new Date("2026-08-26T00:00:00.000Z"),
}),
findMany: vi.fn().mockResolvedValue([]),
update,
},
$transaction: vi.fn(async (fn: (tx: unknown) => Promise<unknown>) => {
const row = {
id: "conn-1",
connectorId: "composio",
provider: "gmail",
displayName: "Gmail",
providerRef: "gmail-state",
status: "pending",
createdAt: new Date("2026-08-26T00:00:00.000Z"),
};
const tx = {
$executeRaw: vi.fn().mockResolvedValue(undefined),
connection: {
findFirst: vi.fn().mockResolvedValueOnce(row).mockResolvedValueOnce(null),
findMany: vi.fn().mockResolvedValue([]),
update: vi
.fn()
.mockImplementation(async ({ data }: { data: Record<string, unknown> }) => ({
...row,
...data,
})),
},
};
return fn(tx);
}),
} as unknown as PrismaClient;
const deps = {
prisma,
connectors: {
managed: vi.fn(() => ({ complete, connectionReady })),
},
env: {
defaultProvider: "fake",
defaultModel: "fake-model",
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "fake",
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "user@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
const handler = new RPCHandler(createRouter(deps));
const { matched, response } = await handler.handle(
new Request("http://127.0.0.1/rpc/connections/complete", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
json: {
connectionId: "conn-1",
code: "123456",
},
}),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(matched).toBe(true);
expect(response.status).toBe(200);
expect(complete).toHaveBeenCalledWith(
{ state: "gmail-state", code: "123456" },
expect.objectContaining({ spaceId: "workspace-1", userId: "user-1" }),
);
expect(connectionReady).toHaveBeenCalled();
});
});
describe("updater owner gate", () => {
function updaterDeps() {
const prisma = {
user: {
findUniqueOrThrow: vi.fn().mockResolvedValue({
email: "user@rakazo.test",
name: "Test User",
avatarStyle: "robot",
}),
},
spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) },
userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) },
deploymentSettings: { findUnique: vi.fn().mockResolvedValue(null) },
} as unknown as PrismaClient;
const deps = {
prisma,
env: {
defaultProvider: "fake",
defaultModel: "fake-model",
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "fake",
gitSha: "deadbeef",
updaterUrl: undefined,
updaterToken: undefined,
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
return { deps, handler: new RPCHandler(createRouter(deps)) };
}
it("forbids non-owners from updater status", async () => {
const { handler } = updaterDeps();
const actor = {
spaceId: "workspace-1",
userId: "user-2",
email: "member@rakazo.test",
isDeploymentOwner: false,
} satisfies Actor;
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/updater/status", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: null }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBe(403);
});
it("lets the deployment owner read status without applying git", async () => {
const { handler } = updaterDeps();
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "owner@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/updater/status", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: null }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.json.supported).toBe(false);
expect(["source", "compose"]).toContain(body.json.installKind);
expect(Array.isArray(body.json.manualCommands)).toBe(true);
});
it("refuses apply when the sidecar is not configured", async () => {
const { handler } = updaterDeps();
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "owner@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/updater/apply", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: {} }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBeGreaterThanOrEqual(400);
const body = await response.json();
const message = JSON.stringify(body);
expect(message).toMatch(/sidecar/i);
expect(message).not.toMatch(/git (fetch|merge|pull)/i);
});
});
describe("computer screen url", () => {
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: "user@rakazo.test",
isDeploymentOwner: true,
} satisfies Actor;
const computerRow = {
id: "computer-1",
screenGeneration: 3,
kind: "e2b",
scope: "team",
state: "running",
providerRef: "sandbox-ref-1",
homeKey: "home-1",
controlHolder: "none",
controlLeaseId: null,
controlLeaseExpiresAt: null,
controlBotId: null,
controlRunId: null,
};
const callScreenUrl = async (
connectScreen: () => Promise<unknown>,
updateMany = vi.fn(),
webOrigin?: string,
) => {
const prisma = {
bot: {
findFirst: vi.fn().mockResolvedValue({
id: "bot-1",
screenGeneration: 2,
thread: { id: "thread-1" },
computer: computerRow,
}),
},
computer: { updateMany },
computerExecutionLease: { findUnique: vi.fn().mockResolvedValue(null) },
} as unknown as PrismaClient;
const deps = {
prisma,
sandbox: { connectScreen },
jobs: { enqueue: vi.fn().mockResolvedValue(undefined) },
env: {
defaultProvider: "fake",
defaultModel: "fake-model",
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "e2b",
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
const handler = new RPCHandler(createRouter(deps));
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/computer/screenUrl", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { botId: "bot-1" } }),
}),
{ prefix: "/rpc", context: { actor, webOrigin } },
);
return { response, updateMany };
};
it("issues lifecycle-bound capabilities for managed-provider screens too", async () => {
const { response } = await callScreenUrl(async () => ({
url: "https://screen.example/vnc.html?token=fake-token",
}));
expect(response.status).toBe(200);
const { json } = await response.json();
const url = new URL(json.url);
expect(url.origin).toBe("http://127.0.0.1:5173");
expect(openScreenCapability(url.pathname, "fake-test-secret")).toMatchObject({
scope: {
botId: "bot-1",
computerId: "computer-1",
botGeneration: 2,
computerGeneration: 3,
controlLeaseId: null,
},
target: { hostname: "screen.example", interactive: false },
});
});
it("points the screen at the browser's own LAN origin", async () => {
const { response } = await callScreenUrl(
async () => ({ url: "https://screen.example/vnc.html?token=fake-token" }),
vi.fn(),
"http://192.168.0.20:5173",
);
const { json } = await response.json();
expect(new URL(json.url).origin).toBe("http://192.168.0.20:5173");
});
it("returns desktop provider screen URLs without sealing them", async () => {
const { response } = await callScreenUrl(async () => ({
url: "desktop://screen/computer-1",
}));
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
json: { url: "desktop://screen/computer-1?view_only=true" },
});
});
it("clears the row instead of 500ing when the provider says the sandbox is gone", async () => {
const { response, updateMany } = await callScreenUrl(() =>
Promise.reject(
Object.assign(new Error("Sandbox is probably not running anymore"), {
name: "SandboxNotFoundError",
}),
),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ json: { url: null } });
expect(updateMany).toHaveBeenCalledWith({
where: { id: "computer-1", providerRef: "sandbox-ref-1" },
data: { state: "stopped", providerRef: null },
});
});
it("keeps a transport blip an error and leaves the row alone", async () => {
const { response, updateMany } = await callScreenUrl(() =>
Promise.reject(Object.assign(new Error("fetch failed"), { code: "ECONNRESET" })),
);
expect(response.status).toBe(500);
expect(updateMany).not.toHaveBeenCalled();
});
it("returns a recoverable conflict when the screen is temporarily busy", async () => {
const { response, updateMany } = await callScreenUrl(() =>
Promise.reject(new ComputerScreenUnavailableError()),
);
expect(response.status).toBe(409);
await expect(response.json()).resolves.toEqual({
json: expect.objectContaining({
code: "CONFLICT",
message: COMPUTER_SCREEN_UNAVAILABLE,
}),
});
expect(updateMany).not.toHaveBeenCalled();
});
});
describe("integration setup authorization", () => {
it.each([
{ owner: false, configured: false, needsSetup: false },
{ owner: true, configured: false, needsSetup: true },
{ owner: true, configured: true, needsSetup: false },
])(
"offers server setup only to an owner without configured providers: %j",
async ({ owner, configured, needsSetup }) => {
const lookup = vi.fn(async () => configured);
const deps = {
prisma: {},
env: { webOrigin: "https://example.test" },
integrationSettings: { configured: lookup },
} as unknown as RouterDeps;
const handler = new RPCHandler(createRouter(deps));
const { response } = await handler.handle(
new Request("https://example.test/rpc/integrationSetup/get", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: null }),
}),
{
prefix: "/rpc",
context: {
actor: {
userId: "user",
spaceId: "space",
email: "user@rakazo.test",
isDeploymentOwner: owner,
},
},
},
);
const result = (await response.json()).json;
expect(result).toMatchObject({ canConfigure: owner, needsSetup });
if (!owner) {
expect(result.providers).toEqual([]);
expect(lookup).not.toHaveBeenCalled();
}
},
);
it("rejects provider credentials from a non-owner before verification or persistence", async () => {
const save = vi.fn();
const deps = {
prisma: {},
env: { webOrigin: "https://example.test" },
integrationSettings: { save },
} as unknown as RouterDeps;
const handler = new RPCHandler(createRouter(deps));
const { response } = await handler.handle(
new Request("https://example.test/rpc/integrationSetup/save", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { provider: "composio", apiKey: "fake-key" } }),
}),
{
prefix: "/rpc",
context: {
actor: {
userId: "member",
spaceId: "space",
email: "member@rakazo.test",
isDeploymentOwner: false,
},
},
},
);
expect(response.status).toBe(403);
expect(save).not.toHaveBeenCalled();
});
});
describe("memory provider config owner gate", () => {
function memoryProviderDeps(spaceOwner: boolean) {
const spaceMemoryConfigFindUnique = vi.fn().mockResolvedValue({
provider: "supermemory",
settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" },
defaultMemoryScope: "isolated",
updatedAt: new Date("2026-08-19T00:00:00.000Z"),
});
const spaceMemberFindUnique = vi.fn().mockResolvedValue({
role: spaceOwner ? "owner" : "member",
});
const prisma = {
spaceMember: { findUnique: spaceMemberFindUnique },
spaceMemoryConfig: { findUnique: spaceMemoryConfigFindUnique },
user: {
findUniqueOrThrow: vi.fn().mockResolvedValue({
email: "user@rakazo.test",
name: "Test User",
avatarStyle: "robot",
}),
},
spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) },
userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) },
deploymentSettings: { findUnique: vi.fn().mockResolvedValue(null) },
} as unknown as PrismaClient;
const deps = {
prisma,
env: {
defaultProvider: "fake",
defaultModel: "fake-model",
webOrigin: "http://127.0.0.1:5173",
screenProxySecret: "fake-test-secret",
sandboxProvider: "fake",
},
dataDir: "/tmp/rakazo-router-test",
} as unknown as RouterDeps;
const actor = {
spaceId: "workspace-1",
userId: "user-1",
email: spaceOwner ? "owner@rakazo.test" : "member@rakazo.test",
isDeploymentOwner: false,
} satisfies Actor;
return {
deps,
actor,
handler: new RPCHandler(createRouter(deps)),
spaceMemberFindUnique,
spaceMemoryConfigFindUnique,
};
}
it("forbids non-owners from reading provider configuration", async () => {
const { handler, actor, spaceMemberFindUnique, spaceMemoryConfigFindUnique } =
memoryProviderDeps(false);
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/memory/providerConfig", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: null }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBe(403);
expect(spaceMemberFindUnique).toHaveBeenCalled();
expect(spaceMemoryConfigFindUnique).not.toHaveBeenCalled();
});
it("returns provider configuration for Space owners", async () => {
const { handler, actor, spaceMemoryConfigFindUnique } = memoryProviderDeps(true);
const { response } = await handler.handle(
new Request("http://127.0.0.1/rpc/memory/providerConfig", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: null }),
}),
{ prefix: "/rpc", context: { actor } },
);
expect(response.status).toBe(200);
expect(spaceMemoryConfigFindUnique).toHaveBeenCalled();
await expect(response.json()).resolves.toEqual({
json: {
provider: "supermemory",
settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" },
defaultMemoryScope: "isolated",
updatedAt: "2026-08-19T00:00:00.000Z",
},
});
});
});
describe("interrupted computer reservation release", () => {
function fixture() {
const order: string[] = [];
const computerUpdate = {
findFirst: vi.fn(async () => ({ id: "update-1", computerId: "computer-1" })),
updateMany: vi.fn(async () => {
order.push("operation");
return { count: 1 };
}),
};
const computer = {
updateMany: vi.fn(async () => {
order.push("computer");
return { count: 1 };
}),
};
const prisma = { computer, computerUpdate, $transaction: vi.fn(async (fn) => fn(prisma)) };
const handler = new RPCHandler(
createRouter({ prisma, env: { sandboxProvider: "fake" } } as unknown as RouterDeps),
);
const call = async (owner: boolean, workersStopped?: boolean) =>
handler.handle(
new Request("http://127.0.0.1/rpc/computer/releaseInterrupted", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { id: "update-1", workersStopped } }),
}),
{
prefix: "/rpc",
context: {
actor: {
spaceId: "space-1",
userId: "user-1",
email: "user@rakazo.test",
isDeploymentOwner: owner,
},
},
},
);
return { prisma, computer, computerUpdate, order, call };
}
it.each([
{ owner: false, stopped: true, status: 403 },
{ owner: true, stopped: false, status: 400 },
{ owner: true, stopped: undefined, status: 400 },
])(
"requires owner authorization and an explicit stopped-workers assertion: %j",
async ({ owner, stopped, status }) => {
const { call, prisma } = fixture();
const { response } = await call(owner, stopped);
expect(response.status).toBe(status);
expect(prisma.$transaction).not.toHaveBeenCalled();
},
);
it("atomically releases only an interrupted reservation in the owner's workspace", async () => {
const { call, computer, computerUpdate, order } = fixture();
const { response } = await call(true, true);
expect(response.status).toBe(200);
expect(computerUpdate.findFirst).toHaveBeenCalledWith({
where: {
id: "update-1",
status: "interrupted",
computer: { spaceId: "space-1", bots: { some: { userId: "user-1", archivedAt: null } } },
},
});
expect(computerUpdate.updateMany).toHaveBeenCalledWith({
where: { id: "update-1", status: "interrupted" },
data: { status: "failed" },
});
expect(computer.updateMany).toHaveBeenCalledWith({
where: { id: "computer-1", maintenanceId: "update-1" },
data: { maintenanceId: null, state: "error" },
});
expect(order).toEqual(["operation", "computer"]);
});
});
describe("mcp oauth redirect", () => {
const begin = async (redirectUri: string, webOrigin?: string) => {
const start = vi.fn().mockResolvedValue({ status: "already_connected" });
const deps = {
prisma: {},
env: { webOrigin: "http://127.0.0.1:5173" },
mcpOAuth: { begin: start },
} as unknown as RouterDeps;
const { response } = await new RPCHandler(createRouter(deps)).handle(
new Request("http://127.0.0.1/rpc/mcp/oauth/begin", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ json: { serverId: "server-1", redirectUri } }),
}),
{
prefix: "/rpc",
context: {
actor: {
userId: "user",
spaceId: "space",
email: "user@rakazo.test",
isDeploymentOwner: true,
},
webOrigin,
},
},
);
return { status: response.status, start };
};
it("returns to the LAN origin the browser opened", async () => {
const { status, start } = await begin(
"http://192.168.0.20:5173/mcp/oauth/callback",
"http://192.168.0.20:5173",
);
expect(status).toBe(200);
expect(start).toHaveBeenCalled();
});
it("rejects a redirect to any other origin", async () => {
const { status, start } = await begin(
"https://attacker.example/mcp/oauth/callback",
"http://192.168.0.20:5173",
);
expect(status).toBe(400);
expect(start).not.toHaveBeenCalled();
});
});