源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { expect } from "vitest";
|
||||
import { control, evalIn, go } from "@openwork/behaviors";
|
||||
import {
|
||||
checkedExec,
|
||||
daytonaSandbox,
|
||||
defaultDaytonaExec,
|
||||
deleteSandboxes,
|
||||
desktop,
|
||||
localHost,
|
||||
provisionDesktopSandbox,
|
||||
} from "@openwork/hosts";
|
||||
import type { DesktopHandle } from "@openwork/hosts";
|
||||
import { eventually, needs, sleep, test } from "@openwork/testkit";
|
||||
|
||||
const e2eTestsEnabled = process.env.OPENWORK_EVAL_E2E_TESTS === "1";
|
||||
const daytonaEnabled = process.env.OPENWORK_EVAL_DAYTONA === "1";
|
||||
const title = e2eTestsEnabled
|
||||
? "workspace sidebar order stays stable, remains draggable, and appends new workspaces"
|
||||
: "workspace sidebar order skipped — needs: set OPENWORK_EVAL_E2E_TESTS=1";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
async function seedWorkspaces(desktopApp: DesktopHandle, profileDir: string): Promise<string[]> {
|
||||
const value = await evalIn(desktopApp, `(async () => {
|
||||
const plans = ${JSON.stringify(["Alpha", "Beta", "Gamma"])};
|
||||
let state = null;
|
||||
for (const label of plans) {
|
||||
state = await window.__OPENWORK_ELECTRON__.invokeDesktop("workspaceCreate", {
|
||||
folderPath: ${JSON.stringify(profileDir)} + "/" + label.toLowerCase(),
|
||||
name: label,
|
||||
});
|
||||
}
|
||||
return (state?.workspaces ?? []).map((workspace) => workspace.id);
|
||||
})()`, { awaitPromise: true, timeoutMs: 60_000 });
|
||||
|
||||
if (!Array.isArray(value) || value.some((id) => typeof id !== "string")) {
|
||||
throw new Error(`Workspace seeding returned an invalid list: ${JSON.stringify(value)}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function sidebarOrder(desktopApp: DesktopHandle): Promise<string[]> {
|
||||
const value = await evalIn(
|
||||
desktopApp,
|
||||
`Array.from(document.querySelectorAll("[data-sidebar-workspace-id]"))
|
||||
.map((element) => element.getAttribute("data-sidebar-workspace-id"))
|
||||
.filter(Boolean)`,
|
||||
);
|
||||
if (!Array.isArray(value) || value.some((id) => typeof id !== "string")) {
|
||||
throw new Error(`Sidebar returned an invalid workspace order: ${JSON.stringify(value)}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function storedOrder(desktopApp: DesktopHandle): Promise<string[]> {
|
||||
const value = await evalIn(desktopApp, `(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem("openwork.react.workspaceOrder") ?? "[]");
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})()`);
|
||||
if (!Array.isArray(value) || value.some((id) => typeof id !== "string")) {
|
||||
throw new Error(`Stored workspace order is invalid: ${JSON.stringify(value)}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function listServerWorkspaces(desktopApp: DesktopHandle): Promise<{ activeId: string | null; ids: string[] }> {
|
||||
const value = await evalIn(desktopApp, `(async () => {
|
||||
const info = await window.__OPENWORK_ELECTRON__?.invokeDesktop?.("openworkServerInfo");
|
||||
if (!info?.running || !info.baseUrl) return { error: "local_server_unavailable" };
|
||||
const response = await fetch(String(info.baseUrl).replace(/\\/+$/, "") + "/workspaces", {
|
||||
headers: { Authorization: "Bearer " + String(info.ownerToken ?? info.clientToken ?? "") },
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
const body = await response.json();
|
||||
const items = Array.isArray(body?.items) ? body.items : [];
|
||||
return {
|
||||
ok: response.ok,
|
||||
activeId: typeof body?.activeId === "string" ? body.activeId : null,
|
||||
ids: items.map((item) => String(item?.id ?? "")).filter(Boolean),
|
||||
};
|
||||
})()`, { awaitPromise: true, timeoutMs: 20_000 });
|
||||
|
||||
if (!isRecord(value) || value.ok !== true || !Array.isArray(value.ids)) {
|
||||
throw new Error(`Listing server workspaces failed: ${JSON.stringify(value)}`);
|
||||
}
|
||||
return {
|
||||
activeId: typeof value.activeId === "string" ? value.activeId : null,
|
||||
ids: value.ids.filter((id): id is string => typeof id === "string"),
|
||||
};
|
||||
}
|
||||
|
||||
async function reloadAndReadSidebar(desktopApp: DesktopHandle, expectedCount: number): Promise<string[]> {
|
||||
await evalIn(desktopApp, "window.location.reload(); true");
|
||||
return eventually(() => sidebarOrder(desktopApp), {
|
||||
within: 90_000,
|
||||
intervalMs: 250,
|
||||
label: `sidebar with ${expectedCount} workspaces after reload`,
|
||||
until: (order) => order.length === expectedCount,
|
||||
});
|
||||
}
|
||||
|
||||
async function dragWorkspaceAfter(desktopApp: DesktopHandle, sourceId: string, targetId: string): Promise<void> {
|
||||
const value = await evalIn(desktopApp, `(() => {
|
||||
const items = Array.from(document.querySelectorAll("[data-sidebar-workspace-id]"));
|
||||
const source = items.find((item) => item.getAttribute("data-sidebar-workspace-id") === ${JSON.stringify(sourceId)});
|
||||
const target = items.find((item) => item.getAttribute("data-sidebar-workspace-id") === ${JSON.stringify(targetId)});
|
||||
const handle = source?.querySelector("[data-sidebar-workspace-drag-handle]");
|
||||
if (!handle || !target) return null;
|
||||
const from = handle.getBoundingClientRect();
|
||||
const to = target.getBoundingClientRect();
|
||||
return {
|
||||
fromX: from.left + from.width / 2,
|
||||
fromY: from.top + from.height / 2,
|
||||
toX: to.left + to.width / 2,
|
||||
toY: to.bottom - 2,
|
||||
};
|
||||
})()`);
|
||||
|
||||
if (!isRecord(value)) throw new Error("Workspace drag handles were not available.");
|
||||
const fromX = value.fromX;
|
||||
const fromY = value.fromY;
|
||||
const toX = value.toX;
|
||||
const toY = value.toY;
|
||||
if (
|
||||
typeof fromX !== "number"
|
||||
|| typeof fromY !== "number"
|
||||
|| typeof toX !== "number"
|
||||
|| typeof toY !== "number"
|
||||
) {
|
||||
throw new Error(`Workspace drag coordinates were invalid: ${JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
await desktopApp.client.send("Input.dispatchMouseEvent", {
|
||||
type: "mouseMoved",
|
||||
x: fromX,
|
||||
y: fromY,
|
||||
});
|
||||
await desktopApp.client.send("Input.dispatchMouseEvent", {
|
||||
type: "mousePressed",
|
||||
x: fromX,
|
||||
y: fromY,
|
||||
button: "left",
|
||||
buttons: 1,
|
||||
clickCount: 1,
|
||||
});
|
||||
for (let step = 1; step <= 16; step += 1) {
|
||||
const progress = step / 16;
|
||||
await desktopApp.client.send("Input.dispatchMouseEvent", {
|
||||
type: "mouseMoved",
|
||||
x: fromX + (toX - fromX) * progress,
|
||||
y: fromY + (toY - fromY) * progress,
|
||||
button: "left",
|
||||
buttons: 1,
|
||||
});
|
||||
await sleep(16);
|
||||
}
|
||||
await desktopApp.client.send("Input.dispatchMouseEvent", {
|
||||
type: "mouseReleased",
|
||||
x: toX,
|
||||
y: toY,
|
||||
button: "left",
|
||||
buttons: 0,
|
||||
clickCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async function createWorkspace(desktopApp: DesktopHandle, path: string): Promise<string> {
|
||||
const before = await listServerWorkspaces(desktopApp);
|
||||
await control(desktopApp, "workspace.create", { path }, { timeoutMs: 90_000 });
|
||||
const after = await eventually(() => listServerWorkspaces(desktopApp), {
|
||||
within: 90_000,
|
||||
intervalMs: 500,
|
||||
label: "new workspace registered",
|
||||
until: (listing) => listing.ids.length === before.ids.length + 1,
|
||||
});
|
||||
const createdId = after.ids.find((id) => !before.ids.includes(id));
|
||||
if (!createdId) throw new Error("Workspace creation produced no new workspace id.");
|
||||
return createdId;
|
||||
}
|
||||
|
||||
test.skipIf(!e2eTestsEnabled)(title, { timeout: 20 * 60_000 }, async ({ evidence }) => {
|
||||
needs({ optIn: ["OPENWORK_EVAL_E2E_TESTS"] });
|
||||
const profileDir = `/tmp/openwork-workspace-sidebar-order-${process.pid}-${Date.now()}`;
|
||||
const provisioned = daytonaEnabled
|
||||
? await provisionDesktopSandbox({
|
||||
ref: process.env.OPENWORK_EVAL_REF?.trim() || process.env.GITHUB_SHA?.trim() || "dev",
|
||||
name: "workspace-sidebar-order",
|
||||
reuse: process.env.OPENWORK_EVAL_DAYTONA_SANDBOX?.trim(),
|
||||
log: (line) => console.error(`[openwork/testkit] ${line}`),
|
||||
})
|
||||
: null;
|
||||
const host = provisioned ? daytonaSandbox(provisioned.sandbox) : localHost();
|
||||
|
||||
try {
|
||||
let seededWorkspaceIds: string[] = [];
|
||||
{
|
||||
await using seededApp = await desktop({ name: "workspace-order-seed", host, profileDir });
|
||||
seededWorkspaceIds = await seedWorkspaces(seededApp, profileDir);
|
||||
}
|
||||
|
||||
await using desktopApp = await desktop({ name: "workspace-sidebar-order", host, profileDir });
|
||||
const initialOrder = await eventually(() => sidebarOrder(desktopApp), {
|
||||
within: 90_000,
|
||||
intervalMs: 250,
|
||||
label: "seeded workspace sidebar",
|
||||
until: (order) => order.length === seededWorkspaceIds.length,
|
||||
});
|
||||
expect(new Set(initialOrder)).toEqual(new Set(seededWorkspaceIds));
|
||||
expect(await storedOrder(desktopApp)).toEqual(initialOrder);
|
||||
|
||||
const nestedWorkspaceButtons = await evalIn(
|
||||
desktopApp,
|
||||
`document.querySelectorAll("[data-sidebar-workspace-id] button button").length`,
|
||||
);
|
||||
expect(nestedWorkspaceButtons).toBe(0);
|
||||
evidence.recordAssertionEvidence(
|
||||
"Workspace controls use valid, non-nested buttons",
|
||||
`All ${initialOrder.length} workspace rows rendered without one button containing another button.`,
|
||||
true,
|
||||
);
|
||||
|
||||
const initialServerOrder = await listServerWorkspaces(desktopApp);
|
||||
const activatedWorkspaceId = initialOrder.find((id) => id !== initialServerOrder.activeId)
|
||||
?? initialOrder[0];
|
||||
await go(desktopApp, `/workspace/${encodeURIComponent(activatedWorkspaceId)}/session`);
|
||||
const activeServerOrder = await eventually(() => listServerWorkspaces(desktopApp), {
|
||||
within: 90_000,
|
||||
intervalMs: 250,
|
||||
label: "server active workspace order",
|
||||
until: (listing) => listing.activeId === activatedWorkspaceId && listing.ids[0] === activatedWorkspaceId,
|
||||
});
|
||||
expect(activeServerOrder.ids[0]).toBe(activatedWorkspaceId);
|
||||
|
||||
const afterActivationReload = await reloadAndReadSidebar(desktopApp, initialOrder.length);
|
||||
expect(afterActivationReload).toEqual(initialOrder);
|
||||
evidence.recordAssertionEvidence(
|
||||
"Selecting a workspace does not reshuffle the sidebar",
|
||||
`The server returned the active workspace first, while the sidebar retained ${JSON.stringify(initialOrder)} across reload.`,
|
||||
true,
|
||||
);
|
||||
|
||||
const draggedWorkspaceId = afterActivationReload[0];
|
||||
const dragTargetId = afterActivationReload[afterActivationReload.length - 1];
|
||||
await dragWorkspaceAfter(desktopApp, draggedWorkspaceId, dragTargetId);
|
||||
const draggedOrder = await eventually(() => sidebarOrder(desktopApp), {
|
||||
within: 30_000,
|
||||
intervalMs: 100,
|
||||
label: "workspace drag reorder",
|
||||
until: (order) => order.length === afterActivationReload.length
|
||||
&& order[order.length - 1] === draggedWorkspaceId,
|
||||
});
|
||||
expect(draggedOrder).not.toEqual(afterActivationReload);
|
||||
expect(await storedOrder(desktopApp)).toEqual(draggedOrder);
|
||||
expect(await reloadAndReadSidebar(desktopApp, draggedOrder.length)).toEqual(draggedOrder);
|
||||
evidence.recordAssertionEvidence(
|
||||
"Dragging a workspace changes and persists its position",
|
||||
`The drag changed the sidebar to ${JSON.stringify(draggedOrder)}, local storage matched it, and reload preserved it.`,
|
||||
true,
|
||||
);
|
||||
|
||||
const newWorkspaceId = await createWorkspace(desktopApp, `${profileDir}/delta`);
|
||||
const withNewWorkspace = await eventually(() => sidebarOrder(desktopApp), {
|
||||
within: 90_000,
|
||||
intervalMs: 250,
|
||||
label: "new workspace appended to saved order",
|
||||
until: (order) => order.length === draggedOrder.length + 1 && order.includes(newWorkspaceId),
|
||||
});
|
||||
expect(withNewWorkspace.slice(0, -1)).toEqual(draggedOrder);
|
||||
expect(withNewWorkspace[withNewWorkspace.length - 1]).toBe(newWorkspaceId);
|
||||
evidence.recordAssertionEvidence(
|
||||
"A newly discovered workspace is appended without disturbing the saved order",
|
||||
`The existing order stayed ${JSON.stringify(draggedOrder)} and the new workspace was appended.`,
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await host[Symbol.asyncDispose]();
|
||||
} finally {
|
||||
if (provisioned) {
|
||||
try {
|
||||
await checkedExec(
|
||||
defaultDaytonaExec,
|
||||
["exec", provisioned.sandbox, "--", "rm", "-rf", profileDir],
|
||||
`remove caller-owned workspace-order profile ${profileDir}`,
|
||||
{ timeoutMs: 30_000 },
|
||||
);
|
||||
} finally {
|
||||
if (provisioned.created) await deleteSandboxes([provisioned.sandbox]);
|
||||
}
|
||||
} else {
|
||||
await rm(profileDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user