import type { AppEvent, CreateTaskInput, FoundryAppSnapshot, SandboxProcessesEvent, SessionEvent, TaskRecord, TaskSummary, TaskWorkspaceChangeModelInput, TaskWorkspaceCreateTaskInput, TaskWorkspaceCreateTaskResponse, TaskWorkspaceDiffInput, TaskWorkspaceRenameInput, TaskWorkspaceRenameSessionInput, TaskWorkspaceSelectInput, TaskWorkspaceSetSessionUnreadInput, TaskWorkspaceSendMessageInput, TaskWorkspaceSnapshot, TaskWorkspaceSessionInput, TaskWorkspaceUpdateDraftInput, TaskEvent, WorkspaceSessionDetail, WorkspaceModelGroup, WorkspaceTaskDetail, WorkspaceTaskSummary, OrganizationEvent, OrganizationSummarySnapshot, AuditLogEvent as HistoryEvent, HistoryQueryInput, SandboxProviderId, RepoOverview, RepoRecord, StarSandboxAgentRepoResult, SwitchResult, } from "@sandbox-agent/foundry-shared"; import { DEFAULT_WORKSPACE_MODEL_GROUPS } from "@sandbox-agent/foundry-shared"; import type { ProcessCreateRequest, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent"; import type { ActorConn, BackendClient, SandboxProcessRecord, SandboxSessionEventRecord, SandboxSessionRecord } from "../backend-client.js"; import { getSharedMockWorkspaceClient } from "./workspace-client.js"; interface MockProcessRecord extends SandboxProcessRecord { logText: string; } function notSupported(name: string): never { throw new Error(`${name} is not supported by the mock backend client.`); } function encodeBase64Utf8(value: string): string { if (typeof Buffer !== "undefined") { return Buffer.from(value, "utf8").toString("base64"); } return globalThis.btoa(unescape(encodeURIComponent(value))); } function nowMs(): number { return Date.now(); } function mockRepoRemote(label: string): string { return `https://example.test/${label}.git`; } function mockCwd(repoLabel: string, taskId: string): string { return `/mock/${repoLabel.replace(/\//g, "-")}/${taskId}`; } function unsupportedAppSnapshot(): FoundryAppSnapshot { return { auth: { status: "signed_out", currentUserId: null }, activeOrganizationId: null, onboarding: { starterRepo: { repoFullName: "rivet-dev/sandbox-agent", repoUrl: "https://github.com/rivet-dev/sandbox-agent", status: "pending", starredAt: null, skippedAt: null, }, }, users: [], organizations: [], }; } function toTaskStatus(status: TaskRecord["status"], archived: boolean): TaskRecord["status"] { if (archived) { return "archived"; } return status; } export function createMockBackendClient(defaultOrganizationId = "default"): BackendClient { const workspace = getSharedMockWorkspaceClient(); const listenersBySandboxId = new Map void>>(); const processesBySandboxId = new Map(); const connectionListeners = new Map void>>(); let nextPid = 4000; let nextProcessId = 1; const requireTask = (taskId: string) => { const task = workspace.getSnapshot().tasks.find((candidate) => candidate.id === taskId); if (!task) { throw new Error(`Unknown mock task ${taskId}`); } return task; }; const ensureProcessList = (sandboxId: string): MockProcessRecord[] => { const existing = processesBySandboxId.get(sandboxId); if (existing) { return existing; } const created: MockProcessRecord[] = []; processesBySandboxId.set(sandboxId, created); return created; }; const notifySandbox = (sandboxId: string): void => { const listeners = listenersBySandboxId.get(sandboxId); if (!listeners) { emitSandboxProcessesUpdate(sandboxId); return; } for (const listener of [...listeners]) { listener(); } emitSandboxProcessesUpdate(sandboxId); }; const connectionChannel = (scope: string, event: string): string => `${scope}:${event}`; const emitConnectionEvent = (scope: string, event: string, payload: any): void => { const listeners = connectionListeners.get(connectionChannel(scope, event)); if (!listeners) { return; } for (const listener of [...listeners]) { listener(payload); } }; const createConn = (scope: string): ActorConn => ({ on(event: string, listener: (payload: any) => void): () => void { const channel = connectionChannel(scope, event); let listeners = connectionListeners.get(channel); if (!listeners) { listeners = new Set(); connectionListeners.set(channel, listeners); } listeners.add(listener); return () => { const current = connectionListeners.get(channel); if (!current) { return; } current.delete(listener); if (current.size === 0) { connectionListeners.delete(channel); } }; }, onError(): () => void { return () => {}; }, async dispose(): Promise {}, }); const buildTaskSummary = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskSummary => ({ id: task.id, repoId: task.repoId, title: task.title, status: task.status, repoName: task.repoName, updatedAtMs: task.updatedAtMs, branch: task.branch, pullRequest: task.pullRequest, activeSessionId: task.activeSessionId ?? task.sessions[0]?.id ?? null, sessionsSummary: task.sessions.map((tab) => ({ id: tab.id, sessionId: tab.sessionId, sandboxSessionId: tab.sandboxSessionId ?? tab.sessionId, sessionName: tab.sessionName, agent: tab.agent, model: tab.model, status: tab.status, thinkingSinceMs: tab.thinkingSinceMs, unread: tab.unread, created: tab.created, })), }); const buildTaskDetail = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskDetail => ({ ...buildTaskSummary(task), task: task.title, fileChanges: task.fileChanges, diffs: task.diffs, fileTree: task.fileTree, minutesUsed: task.minutesUsed, sandboxes: [ { sandboxProviderId: "local", sandboxId: task.id, cwd: mockCwd(task.repoName, task.id), }, ], activeSandboxId: task.id, }); const buildSessionDetail = (task: TaskWorkspaceSnapshot["tasks"][number], sessionId: string): WorkspaceSessionDetail => { const tab = task.sessions.find((candidate) => candidate.id === sessionId); if (!tab) { throw new Error(`Unknown mock session ${sessionId} for task ${task.id}`); } return { sessionId: tab.id, sandboxSessionId: tab.sandboxSessionId ?? tab.sessionId, sessionName: tab.sessionName, agent: tab.agent, model: tab.model, status: tab.status, thinkingSinceMs: tab.thinkingSinceMs, unread: tab.unread, created: tab.created, draft: tab.draft, transcript: tab.transcript, }; }; const buildOrganizationSummary = (): OrganizationSummarySnapshot => { const snapshot = workspace.getSnapshot(); const taskSummaries = snapshot.tasks.map(buildTaskSummary); return { organizationId: defaultOrganizationId, github: { connectedAccount: "mock", installationStatus: "connected", syncStatus: "synced", importedRepoCount: snapshot.repos.length, lastSyncLabel: "Synced just now", lastSyncAt: nowMs(), lastWebhookAt: null, lastWebhookEvent: "", syncGeneration: 1, syncPhase: null, processedRepositoryCount: snapshot.repos.length, totalRepositoryCount: snapshot.repos.length, }, repos: snapshot.repos.map((repo) => { const repoTasks = taskSummaries.filter((task) => task.repoId === repo.id); return { id: repo.id, label: repo.label, taskCount: repoTasks.length, latestActivityMs: repoTasks.reduce((latest, task) => Math.max(latest, task.updatedAtMs), 0), }; }), taskSummaries, }; }; const organizationScope = (organizationId: string): string => `organization:${organizationId}`; const taskScope = (organizationId: string, repoId: string, taskId: string): string => `task:${organizationId}:${repoId}:${taskId}`; const sandboxScope = (organizationId: string, sandboxProviderId: string, sandboxId: string): string => `sandbox:${organizationId}:${sandboxProviderId}:${sandboxId}`; const emitOrganizationSnapshot = (): void => { emitConnectionEvent(organizationScope(defaultOrganizationId), "organizationUpdated", { type: "organizationUpdated", snapshot: buildOrganizationSummary(), } satisfies OrganizationEvent); }; const emitTaskUpdate = (taskId: string): void => { const task = requireTask(taskId); emitConnectionEvent(taskScope(defaultOrganizationId, task.repoId, task.id), "taskUpdated", { type: "taskUpdated", detail: buildTaskDetail(task), } satisfies TaskEvent); }; const emitSessionUpdate = (taskId: string, sessionId: string): void => { const task = requireTask(taskId); emitConnectionEvent(taskScope(defaultOrganizationId, task.repoId, task.id), "sessionUpdated", { type: "sessionUpdated", session: buildSessionDetail(task, sessionId), } satisfies SessionEvent); }; const emitSandboxProcessesUpdate = (sandboxId: string): void => { emitConnectionEvent(sandboxScope(defaultOrganizationId, "local", sandboxId), "processesUpdated", { type: "processesUpdated", processes: ensureProcessList(sandboxId).map((process) => cloneProcess(process)), } satisfies SandboxProcessesEvent); }; const buildTaskRecord = (taskId: string): TaskRecord => { const task = requireTask(taskId); const cwd = mockCwd(task.repoName, task.id); const archived = task.status === "archived"; return { organizationId: defaultOrganizationId, repoId: task.repoId, repoRemote: mockRepoRemote(task.repoName), taskId: task.id, branchName: task.branch, title: task.title, task: task.title, sandboxProviderId: "local", status: toTaskStatus(archived ? "archived" : "running", archived), pullRequest: null, activeSandboxId: task.id, sandboxes: [ { sandboxId: task.id, sandboxProviderId: "local", sandboxActorId: "mock-sandbox", switchTarget: `mock://${task.id}`, cwd, createdAt: task.updatedAtMs, updatedAt: task.updatedAtMs, }, ], createdAt: task.updatedAtMs, updatedAt: task.updatedAtMs, }; }; const cloneProcess = (process: MockProcessRecord): MockProcessRecord => ({ ...process }); const createProcessRecord = (sandboxId: string, cwd: string, request: ProcessCreateRequest): MockProcessRecord => { const processId = `proc_${nextProcessId++}`; const createdAtMs = nowMs(); const args = request.args ?? []; const interactive = request.interactive ?? false; const tty = request.tty ?? false; const statusLine = interactive && tty ? "Mock terminal session created.\nInteractive transport is unavailable in mock mode.\n" : "Mock process created.\n"; const commandLine = `$ ${[request.command, ...args].join(" ").trim()}\n`; return { id: processId, command: request.command, args, createdAtMs, cwd: request.cwd ?? cwd, exitCode: null, exitedAtMs: null, interactive, pid: nextPid++, status: "running", tty, logText: `${statusLine}${commandLine}`, }; }; return { async getAppSnapshot(): Promise { return unsupportedAppSnapshot(); }, async connectOrganization(organizationId: string): Promise { return createConn(organizationScope(organizationId)); }, async connectTask(organizationId: string, repoId: string, taskId: string): Promise { return createConn(taskScope(organizationId, repoId, taskId)); }, async connectSandbox(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise { return createConn(sandboxScope(organizationId, sandboxProviderId, sandboxId)); }, subscribeApp(): () => void { return () => {}; }, async signInWithGithub(): Promise { notSupported("signInWithGithub"); }, async signOutApp(): Promise { return unsupportedAppSnapshot(); }, async skipAppStarterRepo(): Promise { return unsupportedAppSnapshot(); }, async starAppStarterRepo(): Promise { return unsupportedAppSnapshot(); }, async selectAppOrganization(): Promise { return unsupportedAppSnapshot(); }, async setAppDefaultModel(): Promise { return unsupportedAppSnapshot(); }, async updateAppOrganizationProfile(): Promise { return unsupportedAppSnapshot(); }, async triggerAppRepoImport(): Promise { return unsupportedAppSnapshot(); }, async reconnectAppGithub(): Promise { notSupported("reconnectAppGithub"); }, async completeAppHostedCheckout(): Promise { notSupported("completeAppHostedCheckout"); }, async openAppBillingPortal(): Promise { notSupported("openAppBillingPortal"); }, async cancelAppScheduledRenewal(): Promise { return unsupportedAppSnapshot(); }, async resumeAppSubscription(): Promise { return unsupportedAppSnapshot(); }, async recordAppSeatUsage(): Promise { return unsupportedAppSnapshot(); }, async listRepos(_organizationId: string): Promise { return workspace.getSnapshot().repos.map((repo) => ({ organizationId: defaultOrganizationId, repoId: repo.id, remoteUrl: mockRepoRemote(repo.label), createdAt: nowMs(), updatedAt: nowMs(), })); }, async createTask(_input: CreateTaskInput): Promise { notSupported("createTask"); }, async listTasks(_organizationId: string, repoId?: string): Promise { return workspace .getSnapshot() .tasks.filter((task) => !repoId || task.repoId === repoId) .map((task) => ({ organizationId: defaultOrganizationId, repoId: task.repoId, taskId: task.id, branchName: task.branch, title: task.title, status: task.status === "archived" ? "archived" : "running", pullRequest: null, updatedAt: task.updatedAtMs, })); }, async getRepoOverview(_organizationId: string, _repoId: string): Promise { notSupported("getRepoOverview"); }, async getTask(_organizationId: string, _repoId: string, taskId: string): Promise { return buildTaskRecord(taskId); }, async listHistory(_input: HistoryQueryInput): Promise { return []; }, async switchTask(_organizationId: string, _repoId: string, taskId: string): Promise { return { organizationId: defaultOrganizationId, taskId, sandboxProviderId: "local", switchTarget: `mock://${taskId}`, }; }, async attachTask(_organizationId: string, _repoId: string, taskId: string): Promise<{ target: string; sessionId: string | null }> { return { target: `mock://${taskId}`, sessionId: requireTask(taskId).sessions[0]?.sessionId ?? null, }; }, async runAction(_organizationId: string, _repoId: string, _taskId: string): Promise { notSupported("runAction"); }, async createSandboxSession(): Promise<{ id: string; status: "running" | "idle" | "error" }> { notSupported("createSandboxSession"); }, async listSandboxSessions(): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }> { return { items: [] }; }, async listSandboxSessionEvents(): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }> { return { items: [] }; }, async createSandboxProcess(input: { organizationId: string; sandboxProviderId: SandboxProviderId; sandboxId: string; request: ProcessCreateRequest; }): Promise { const task = requireTask(input.sandboxId); const processes = ensureProcessList(input.sandboxId); const created = createProcessRecord(input.sandboxId, mockCwd(task.repoName, task.id), input.request); processes.unshift(created); notifySandbox(input.sandboxId); return cloneProcess(created); }, async listSandboxProcesses(_organizationId: string, _providerId: SandboxProviderId, sandboxId: string): Promise<{ processes: SandboxProcessRecord[] }> { return { processes: ensureProcessList(sandboxId).map((process) => cloneProcess(process)), }; }, async getSandboxProcessLogs( _organizationId: string, _providerId: SandboxProviderId, sandboxId: string, processId: string, query?: ProcessLogFollowQuery, ): Promise { const process = ensureProcessList(sandboxId).find((candidate) => candidate.id === processId); if (!process) { throw new Error(`Unknown mock process ${processId}`); } return { processId, stream: query?.stream ?? (process.tty ? "pty" : "combined"), entries: process.logText ? [ { data: encodeBase64Utf8(process.logText), encoding: "base64", sequence: 1, stream: query?.stream ?? (process.tty ? "pty" : "combined"), timestampMs: process.createdAtMs, }, ] : [], }; }, async stopSandboxProcess( _organizationId: string, _providerId: SandboxProviderId, sandboxId: string, processId: string, _query?: ProcessSignalQuery, ): Promise { const process = ensureProcessList(sandboxId).find((candidate) => candidate.id === processId); if (!process) { throw new Error(`Unknown mock process ${processId}`); } process.status = "exited"; process.exitCode = 0; process.exitedAtMs = nowMs(); process.logText += "\n[stopped]\n"; notifySandbox(sandboxId); return cloneProcess(process); }, async killSandboxProcess( _organizationId: string, _providerId: SandboxProviderId, sandboxId: string, processId: string, _query?: ProcessSignalQuery, ): Promise { const process = ensureProcessList(sandboxId).find((candidate) => candidate.id === processId); if (!process) { throw new Error(`Unknown mock process ${processId}`); } process.status = "exited"; process.exitCode = 137; process.exitedAtMs = nowMs(); process.logText += "\n[killed]\n"; notifySandbox(sandboxId); return cloneProcess(process); }, async deleteSandboxProcess(_organizationId: string, _providerId: SandboxProviderId, sandboxId: string, processId: string): Promise { processesBySandboxId.set( sandboxId, ensureProcessList(sandboxId).filter((candidate) => candidate.id !== processId), ); notifySandbox(sandboxId); }, subscribeSandboxProcesses(_organizationId: string, _providerId: SandboxProviderId, sandboxId: string, listener: () => void): () => void { let listeners = listenersBySandboxId.get(sandboxId); if (!listeners) { listeners = new Set(); listenersBySandboxId.set(sandboxId, listeners); } listeners.add(listener); return () => { const current = listenersBySandboxId.get(sandboxId); if (!current) { return; } current.delete(listener); if (current.size === 0) { listenersBySandboxId.delete(sandboxId); } }; }, async sendSandboxPrompt(): Promise { notSupported("sendSandboxPrompt"); }, async sandboxSessionStatus(sessionId: string): Promise<{ id: string; status: "running" | "idle" | "error" }> { return { id: sessionId, status: "idle" }; }, async sandboxProviderState( _organizationId: string, _providerId: SandboxProviderId, sandboxId: string, ): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }> { return { sandboxProviderId: "local", sandboxId, state: "running", at: nowMs() }; }, async getSandboxAgentConnection(): Promise<{ endpoint: string; token?: string }> { return { endpoint: "mock://terminal-unavailable" }; }, async getSandboxWorkspaceModelGroups(_organizationId: string, _sandboxProviderId: SandboxProviderId, _sandboxId: string): Promise { return DEFAULT_WORKSPACE_MODEL_GROUPS; }, async getOrganizationSummary(): Promise { return buildOrganizationSummary(); }, async getTaskDetail(_organizationId: string, _repoId: string, taskId: string): Promise { return buildTaskDetail(requireTask(taskId)); }, async getSessionDetail(_organizationId: string, _repoId: string, taskId: string, sessionId: string): Promise { return buildSessionDetail(requireTask(taskId), sessionId); }, async getWorkspace(): Promise { return workspace.getSnapshot(); }, subscribeWorkspace(_organizationId: string, listener: () => void): () => void { return workspace.subscribe(listener); }, async createWorkspaceTask(_organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise { const created = await workspace.createTask(input); emitOrganizationSnapshot(); emitTaskUpdate(created.taskId); if (created.sessionId) { emitSessionUpdate(created.taskId, created.sessionId); } return created; }, async markWorkspaceUnread(_organizationId: string, input: TaskWorkspaceSelectInput): Promise { await workspace.markTaskUnread(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); }, async renameWorkspaceTask(_organizationId: string, input: TaskWorkspaceRenameInput): Promise { await workspace.renameTask(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); }, async createWorkspaceSession(_organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> { const created = await workspace.addSession(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, created.sessionId); return created; }, async renameWorkspaceSession(_organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise { await workspace.renameSession(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, input.sessionId); }, async selectWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise { await workspace.selectSession(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, input.sessionId); }, async setWorkspaceSessionUnread(_organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise { await workspace.setSessionUnread(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, input.sessionId); }, async updateWorkspaceDraft(_organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise { await workspace.updateDraft(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, input.sessionId); }, async changeWorkspaceModel(_organizationId: string, input: TaskWorkspaceChangeModelInput): Promise { await workspace.changeModel(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, input.sessionId); }, async sendWorkspaceMessage(_organizationId: string, input: TaskWorkspaceSendMessageInput): Promise { await workspace.sendMessage(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, input.sessionId); }, async stopWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise { await workspace.stopAgent(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); emitSessionUpdate(input.taskId, input.sessionId); }, async closeWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise { await workspace.closeSession(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); }, async publishWorkspacePr(_organizationId: string, input: TaskWorkspaceSelectInput): Promise { await workspace.publishPr(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); }, async revertWorkspaceFile(_organizationId: string, input: TaskWorkspaceDiffInput): Promise { await workspace.revertFile(input); emitOrganizationSnapshot(); emitTaskUpdate(input.taskId); }, async adminReloadGithubOrganization(): Promise {}, async adminReloadGithubRepository(): Promise {}, async health(): Promise<{ ok: true }> { return { ok: true }; }, async useOrganization(organizationId: string): Promise<{ organizationId: string }> { return { organizationId }; }, async starSandboxAgentRepo(): Promise { return { repo: "rivet-dev/sandbox-agent", starredAt: nowMs(), }; }, }; }