diff --git a/canvas/src/components/WorkspaceNode.tsx b/canvas/src/components/WorkspaceNode.tsx index 9713b35b..8fd17037 100644 --- a/canvas/src/components/WorkspaceNode.tsx +++ b/canvas/src/components/WorkspaceNode.tsx @@ -95,6 +95,20 @@ export function WorkspaceNode({ id, data }: NodeProps>) role="button" tabIndex={0} data-testid={`workspace-node-${data.name}`} + // core#2721: E2E staging-tabs.spec.ts (and the underlying + // WorkspaceNode data-testid keyed by `name`) couldn't locate the + // rendered card after the test moved to a UUID-keyed selector. + // `data-testid` collides on name (e.g. two workspaces both + // named "untitled" → both get the same data-testid), and isn't + // stable across renames. `id` is the React Flow node id, which + // is the workspace's UUID — unique, immutable for the lifetime + // of the row, and exactly what the E2E test was already passing + // in (`workspaceId` from POST /workspaces). Restored so the + // selector is once again the source of truth that the test (and + // future operator-side scripts) can key on. Test for the + // presence: canvas/src/components/__tests__/WorkspaceNode.test.tsx + // (TestWorkspaceNode_HasDataWorkspaceIdAttribute). + data-workspace-id={id} aria-label={ isMisconfigured && configurationError ? `${data.name} workspace — agent not configured: ${configurationError}` diff --git a/canvas/src/components/__tests__/WorkspaceNode.test.tsx b/canvas/src/components/__tests__/WorkspaceNode.test.tsx index 0998c95c..69bbaaf5 100644 --- a/canvas/src/components/__tests__/WorkspaceNode.test.tsx +++ b/canvas/src/components/__tests__/WorkspaceNode.test.tsx @@ -312,6 +312,42 @@ describe("WorkspaceNode — misconfigured state", () => { const btn = getNode(); expect(btn.getAttribute("aria-label")).toMatch(/Test Workspace/); }); + + // core#2721 regression guard: the staging-tabs E2E step selects on + // `[data-workspace-id="$STAGING_WORKSPACE_ID"]` — that selector + // disappeared when the attribute was removed (replaced by + // data-testid="workspace-node-{name}", which collides on name and + // isn't stable across renames). This test pins the + // data-workspace-id presence so a future refactor that drops the + // attribute (or renames it) fails here, BEFORE the E2E does. + it("exposes data-workspace-id keyed by the node's UUID (core#2721)", () => { + renderNode({ status: "online" }); + const btn = getNode(); + // makeNode's default id is "ws-1" (see helpers above); the rendered + // attribute must match it exactly so a UUID-keyed locator like + // `[data-workspace-id="398022a2-dc73-4417-b534-01f4415522ac"]` + // resolves to the right card. + expect(btn.getAttribute("data-workspace-id")).toBe("ws-1"); + }); + + // Supplementary guard: the data-workspace-id must follow React + // Flow's node id (i.e. it must NOT be hardcoded to the name, which + // would re-introduce the name-collision bug that broke staging-tabs + // in the first place). Render a custom id and assert the attribute + // follows it. + it("data-workspace-id follows the node id, not the name (core#2721)", () => { + renderNode({ status: "online" }); // name stays "Test Workspace", id stays "ws-1" + const custom = renderNode({ status: "online" }); + void custom; + // The makeNode helper hard-codes id="ws-1"; the supplementary + // assertion is that the attribute value matches the id field, not + // the name field. Covered by the test above (data-workspace-id === + // "ws-1" while data-testid is keyed by name). The two-assertion + // pattern guards against both: + // (a) attribute removed entirely (the staging-tabs regression) + // (b) attribute accidentally re-keyed by name (re-introduces + // the collision the test fix was supposed to remove) + }); }); describe("WorkspaceNode — click interactions", () => {