445 lines
15 KiB
TypeScript
445 lines
15 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
|
|
import type { OpenworkServerClient } from "../src/app/lib/openwork-server";
|
|
import type { WorkspaceInfo } from "../src/app/lib/desktop";
|
|
import { getWorkspaceTaskLoadErrorDisplay } from "../src/app/utils";
|
|
import {
|
|
diagnoseRemoteWorkspaceTaskLoadFailure,
|
|
getRemoteWorkspaceConnectionKey,
|
|
redactRemoteDiagnosticText,
|
|
resolveRemoteWorkspaceConnectionTarget,
|
|
testRemoteWorkspaceConnection,
|
|
} from "../src/react-app/domains/workspace/remote-workspace-diagnostics";
|
|
|
|
function workspace(overrides: Partial<WorkspaceInfo> = {}): WorkspaceInfo {
|
|
return {
|
|
id: "ws_local",
|
|
name: "Remote worker",
|
|
path: "",
|
|
preset: "remote",
|
|
workspaceType: "remote",
|
|
remoteType: "openwork",
|
|
openworkHostUrl: "https://worker.example.com/w/ws_remote",
|
|
openworkToken: "ow-token",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function client(overrides: Partial<OpenworkServerClient> = {}): OpenworkServerClient {
|
|
return {
|
|
baseUrl: "https://worker.example.com/w/ws_remote",
|
|
token: "ow-token",
|
|
health: async () => ({ ok: true, version: "0.1.0", uptimeMs: 10 }),
|
|
status: async () => ({
|
|
ok: true,
|
|
version: "0.1.0",
|
|
uptimeMs: 10,
|
|
readOnly: false,
|
|
approval: { mode: "manual", timeoutMs: 30_000 },
|
|
corsOrigins: [],
|
|
workspaceCount: 1,
|
|
activeWorkspaceId: "ws_remote",
|
|
selectedWorkspaceId: "ws_remote",
|
|
workspace: {
|
|
id: "ws_remote",
|
|
name: "Worker project",
|
|
path: "/workspace",
|
|
preset: "starter",
|
|
workspaceType: "local",
|
|
},
|
|
authorizedRoots: ["/workspace"],
|
|
server: { host: "127.0.0.1", port: 8787 },
|
|
tokenSource: { client: "file", host: "file" },
|
|
}),
|
|
capabilities: async () => ({
|
|
skills: { read: true, write: true, source: "openwork" },
|
|
plugins: { read: true, write: true },
|
|
mcp: { read: true, write: true },
|
|
commands: { read: true, write: true },
|
|
config: { read: true, write: true },
|
|
}),
|
|
listWorkspaces: async () => ({
|
|
items: [
|
|
{
|
|
id: "ws_remote",
|
|
name: "Worker project",
|
|
path: "/workspace",
|
|
preset: "starter",
|
|
workspaceType: "local",
|
|
},
|
|
],
|
|
activeId: "ws_remote",
|
|
}),
|
|
...overrides,
|
|
} as OpenworkServerClient;
|
|
}
|
|
|
|
function serverError(status: number, code: string, message: string) {
|
|
return Object.assign(new Error(message), { status, code });
|
|
}
|
|
|
|
describe("resolveRemoteWorkspaceConnectionTarget", () => {
|
|
test("builds a host-scoped OpenWork target from saved worker credentials", () => {
|
|
const target = resolveRemoteWorkspaceConnectionTarget(
|
|
workspace({
|
|
openworkHostUrl: "https://worker.example.com",
|
|
openworkWorkspaceId: "ws_remote",
|
|
}),
|
|
);
|
|
|
|
expect(target.ok).toBe(true);
|
|
if (!target.ok) return;
|
|
expect(target.target.baseUrl).toBe("https://worker.example.com");
|
|
expect(target.target.workspaceId).toBe("ws_remote");
|
|
expect(target.target.token).toBe("ow-token");
|
|
});
|
|
|
|
test("parses workspace id from a workspace-scoped connect URL", () => {
|
|
const target = resolveRemoteWorkspaceConnectionTarget(workspace());
|
|
|
|
expect(target.ok).toBe(true);
|
|
if (!target.ok) return;
|
|
expect(target.target.workspaceId).toBe("ws_remote");
|
|
expect(target.target.baseUrl).toBe("https://worker.example.com");
|
|
});
|
|
|
|
test("fails fast when a remote worker has no endpoint", () => {
|
|
const target = resolveRemoteWorkspaceConnectionTarget(
|
|
workspace({
|
|
openworkHostUrl: "",
|
|
baseUrl: "",
|
|
}),
|
|
);
|
|
|
|
expect(target.ok).toBe(false);
|
|
if (target.ok) return;
|
|
expect(target.state.status).toBe("error");
|
|
expect(target.state.message).toContain("URL is missing");
|
|
});
|
|
|
|
test("fails fast when a remote worker endpoint is invalid", () => {
|
|
const target = resolveRemoteWorkspaceConnectionTarget(
|
|
workspace({
|
|
openworkHostUrl: "not a url",
|
|
}),
|
|
);
|
|
|
|
expect(target.ok).toBe(false);
|
|
if (target.ok) return;
|
|
expect(target.state.status).toBe("error");
|
|
expect(target.state.message).toContain("URL is invalid");
|
|
});
|
|
|
|
test("does not run OpenWork probes against non-OpenWork remote workspaces", () => {
|
|
const target = resolveRemoteWorkspaceConnectionTarget(
|
|
workspace({
|
|
remoteType: "opencode",
|
|
openworkHostUrl: "",
|
|
openworkToken: "",
|
|
baseUrl: "https://opencode.example.com",
|
|
}),
|
|
);
|
|
|
|
expect(target.ok).toBe(false);
|
|
if (target.ok) return;
|
|
expect(target.state.status).toBe("error");
|
|
expect(target.state.message).toContain("OpenWork remote workers");
|
|
});
|
|
|
|
test("does not run OpenWork probes against stale OpenWork fields on non-OpenWork remotes", () => {
|
|
const target = resolveRemoteWorkspaceConnectionTarget(
|
|
workspace({
|
|
remoteType: "opencode",
|
|
openworkHostUrl: "https://worker.example.com/w/ws_remote",
|
|
openworkToken: "owt_secret",
|
|
baseUrl: "https://opencode.example.com",
|
|
}),
|
|
);
|
|
|
|
expect(target.ok).toBe(false);
|
|
if (target.ok) return;
|
|
expect(target.state.message).toContain("OpenWork remote workers");
|
|
});
|
|
});
|
|
|
|
describe("testRemoteWorkspaceConnection", () => {
|
|
test("returns a connected state after health, token, capabilities, and workspace checks pass", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace(), {
|
|
now: () => 123,
|
|
createClient: () => client(),
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.state).toEqual({
|
|
status: "connected",
|
|
message: "Connected to Worker project.",
|
|
checkedAt: 123,
|
|
});
|
|
});
|
|
|
|
test("reports a missing token after proving the worker endpoint is reachable", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace({ openworkToken: "" }), {
|
|
createClient: () => client(),
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.status).toBe("error");
|
|
expect(result.state.message).toContain("Token is missing");
|
|
expect(result.state.message).toContain("Upgrade the OpenWork host");
|
|
expect(result.state.message).toContain("team@openworklabs.com");
|
|
});
|
|
|
|
test("reports unhealthy health responses as endpoint failures", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace(), {
|
|
createClient: () =>
|
|
client({
|
|
health: async () => ({ ok: false, version: "0.1.0", uptimeMs: 10 }),
|
|
}),
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.status).toBe("error");
|
|
expect(result.state.message).toContain("unhealthy response");
|
|
expect(result.state.message).toContain("Upgrade the OpenWork host");
|
|
expect(result.state.message).toContain("team@openworklabs.com");
|
|
});
|
|
|
|
test("uses fallback OpenWork tokens saved on older workspace records", async () => {
|
|
const result = await testRemoteWorkspaceConnection(
|
|
workspace({
|
|
openworkToken: "",
|
|
openworkClientToken: "legacy-client-token",
|
|
}),
|
|
{
|
|
createClient: (target) => {
|
|
expect(target.token).toBe("legacy-client-token");
|
|
return client();
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
test("reports rejected credentials without hiding the endpoint", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace(), {
|
|
createClient: () =>
|
|
client({
|
|
capabilities: async () => {
|
|
throw serverError(401, "invalid_token", "Invalid token");
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.status).toBe("error");
|
|
expect(result.state.message).toContain("Token was rejected by worker.example.com");
|
|
expect(result.state.message).toContain("Upgrade the OpenWork host");
|
|
expect(result.state.message).toContain("team@openworklabs.com");
|
|
});
|
|
|
|
test("reports a missing workspace separately from a dead worker", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace(), {
|
|
createClient: () =>
|
|
client({
|
|
listWorkspaces: async () => {
|
|
return { items: [], activeId: null };
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.status).toBe("error");
|
|
expect(result.state.message).toContain("Workspace ws_remote was not found");
|
|
expect(result.state.message).toContain("Upgrade the OpenWork host");
|
|
expect(result.state.message).toContain("team@openworklabs.com");
|
|
});
|
|
|
|
test("uses workspace list when the saved remote target is not workspace-scoped", async () => {
|
|
const result = await testRemoteWorkspaceConnection(
|
|
workspace({
|
|
openworkHostUrl: "https://worker.example.com",
|
|
openworkWorkspaceId: "",
|
|
baseUrl: "",
|
|
}),
|
|
{
|
|
createClient: (target) => {
|
|
expect(target.baseUrl).toBe("https://worker.example.com");
|
|
expect(target.workspaceId).toBe(null);
|
|
return client({
|
|
status: async () => {
|
|
throw new Error("status should not be called");
|
|
},
|
|
});
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.state.message).toBe("Connected to Worker project.");
|
|
});
|
|
|
|
test("reports rejected credentials from the workspace list fallback", async () => {
|
|
const result = await testRemoteWorkspaceConnection(
|
|
workspace({
|
|
openworkHostUrl: "https://worker.example.com",
|
|
openworkWorkspaceId: "",
|
|
baseUrl: "",
|
|
}),
|
|
{
|
|
createClient: () =>
|
|
client({
|
|
listWorkspaces: async () => {
|
|
throw serverError(401, "invalid_token", "Invalid token");
|
|
},
|
|
}),
|
|
},
|
|
);
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.status).toBe("error");
|
|
expect(result.state.message).toContain("Token was rejected by worker.example.com");
|
|
expect(result.state.message).toContain("Upgrade the OpenWork host");
|
|
expect(result.state.message).toContain("team@openworklabs.com");
|
|
});
|
|
|
|
test("reports unauthorized workspace status separately from bad credentials", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace(), {
|
|
createClient: () =>
|
|
client({
|
|
listWorkspaces: async () => {
|
|
throw serverError(403, "forbidden", "Forbidden");
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.status).toBe("error");
|
|
expect(result.state.message).toContain("is not authorized");
|
|
expect(result.state.message).toContain("Upgrade the OpenWork host");
|
|
expect(result.state.message).toContain("team@openworklabs.com");
|
|
});
|
|
|
|
test("reports endpoint reachability failures from the health probe", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace(), {
|
|
createClient: () =>
|
|
client({
|
|
health: async () => {
|
|
throw new Error("Failed to fetch");
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.status).toBe("error");
|
|
expect(result.state.message).toContain("Cannot reach worker.example.com");
|
|
expect(result.state.message).toContain("Upgrade the OpenWork host");
|
|
expect(result.state.message).toContain("team@openworklabs.com");
|
|
});
|
|
|
|
test("redacts token-like values from diagnostic error messages", async () => {
|
|
const result = await testRemoteWorkspaceConnection(workspace(), {
|
|
createClient: () =>
|
|
client({
|
|
health: async () => {
|
|
throw new Error("Failed with Bearer owt_live_secret and ?token=abc123");
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.state.message).toContain("Bearer [redacted]");
|
|
expect(result.state.message).toContain("?token=[redacted]");
|
|
expect(result.state.message).not.toContain("owt_live_secret");
|
|
expect(result.state.message).not.toContain("abc123");
|
|
});
|
|
});
|
|
|
|
describe("remote diagnostic identity", () => {
|
|
test("redacts common token shapes", () => {
|
|
const redacted = redactRemoteDiagnosticText(
|
|
"Authorization: Bearer abc.def and https://x.test/?access_token=secret&ok=1 and owt_live_secret",
|
|
);
|
|
|
|
expect(redacted).toContain("Authorization: Bearer [redacted]");
|
|
expect(redacted).toContain("?access_token=[redacted]&ok=1");
|
|
expect(redacted).toContain("owt_[redacted]");
|
|
expect(redacted).not.toContain("abc.def");
|
|
expect(redacted).not.toContain("secret");
|
|
expect(redacted).not.toContain("owt_live_secret");
|
|
});
|
|
|
|
test("changes when connection credentials change", () => {
|
|
const before = getRemoteWorkspaceConnectionKey(workspace({ openworkToken: "old-token" }));
|
|
const after = getRemoteWorkspaceConnectionKey(workspace({ openworkToken: "new-token" }));
|
|
|
|
expect(before).not.toBe(after);
|
|
});
|
|
});
|
|
|
|
describe("diagnoseRemoteWorkspaceTaskLoadFailure", () => {
|
|
test("keeps the task load error when the worker itself is reachable", async () => {
|
|
const state = await diagnoseRemoteWorkspaceTaskLoadFailure(
|
|
workspace(),
|
|
"Session list failed",
|
|
{
|
|
now: () => 456,
|
|
createClient: () => client(),
|
|
},
|
|
);
|
|
|
|
expect(state).toEqual({
|
|
status: "error",
|
|
message: "Worker is reachable, but tasks failed to load: Session list failed",
|
|
checkedAt: 456,
|
|
});
|
|
});
|
|
|
|
test("prefers the blocking connection diagnostic when the worker is unreachable", async () => {
|
|
const state = await diagnoseRemoteWorkspaceTaskLoadFailure(
|
|
workspace(),
|
|
"Session list failed",
|
|
{
|
|
createClient: () =>
|
|
client({
|
|
health: async () => {
|
|
throw new Error("Failed to fetch");
|
|
},
|
|
}),
|
|
},
|
|
);
|
|
|
|
expect(state.status).toBe("error");
|
|
expect(state.message).toContain("Cannot reach worker.example.com");
|
|
});
|
|
|
|
test("redacts token-like values from task load fallbacks", async () => {
|
|
const state = await diagnoseRemoteWorkspaceTaskLoadFailure(
|
|
workspace(),
|
|
"Session failed with bearer owt_live_secret and ?token=abc123",
|
|
{
|
|
createClient: () => client(),
|
|
},
|
|
);
|
|
|
|
expect(state.message).toContain("bearer [redacted]");
|
|
expect(state.message).toContain("?token=[redacted]");
|
|
expect(state.message).not.toContain("owt_live_secret");
|
|
expect(state.message).not.toContain("abc123");
|
|
});
|
|
});
|
|
|
|
describe("getWorkspaceTaskLoadErrorDisplay", () => {
|
|
test("redacts remote worker task load errors before rendering", () => {
|
|
const display = getWorkspaceTaskLoadErrorDisplay(
|
|
workspace(),
|
|
"failed with Authorization: Bearer owt_live_secret and ?token=abc123",
|
|
);
|
|
|
|
expect(display.message).toContain("Authorization: Bearer [redacted]");
|
|
expect(display.message).toContain("?token=[redacted]");
|
|
expect(display.message).not.toContain("owt_live_secret");
|
|
expect(display.message).not.toContain("abc123");
|
|
});
|
|
});
|