fix: preserve github workspace context (#1487)
This commit is contained in:
@@ -633,6 +633,78 @@ func TestBootRouteDataIntegrationRouteUsesActiveWorkspaceCookie(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootRouteDataIntegrationRouteCookieWinsOverSettingsWorkspace(t *testing.T) {
|
||||
harness := newBootStateTestHarness(t)
|
||||
ctx := context.Background()
|
||||
workspaces, err := harness.taskSvc.ListWorkspaces(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkspaces: %v", err)
|
||||
}
|
||||
if len(workspaces) == 0 {
|
||||
t.Fatal("expected seeded default workspace")
|
||||
}
|
||||
settingsWorkspaceID := workspaces[0].ID
|
||||
cookieWorkspace, err := harness.taskSvc.CreateWorkspace(ctx, &taskservice.CreateWorkspaceRequest{
|
||||
Name: "Cookie Workspace",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorkspace: %v", err)
|
||||
}
|
||||
if _, err := harness.taskSvc.CreateWorkflow(ctx, &taskservice.CreateWorkflowRequest{
|
||||
WorkspaceID: cookieWorkspace.ID,
|
||||
Name: "Cookie Workflow",
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateWorkflow: %v", err)
|
||||
}
|
||||
if _, err := harness.userSvc.UpdateUserSettings(ctx, &userservice.UpdateUserSettingsRequest{
|
||||
WorkspaceID: &settingsWorkspaceID,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateUserSettings: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/app-state?path=%2Fgithub", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "kandev-active-workspace", Value: cookieWorkspace.ID})
|
||||
payload := bootPayload(ctx, req, routeParams{
|
||||
taskSvc: harness.taskSvc,
|
||||
userCtrl: harness.userCtrl,
|
||||
services: &Services{
|
||||
Workflow: harness.workflowSvc,
|
||||
},
|
||||
}, webapp.ClassifyRoute("/github"))
|
||||
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal payload: %v", err)
|
||||
}
|
||||
var decoded struct {
|
||||
InitialState struct {
|
||||
Workspaces struct {
|
||||
ActiveID string `json:"activeId"`
|
||||
} `json:"workspaces"`
|
||||
UserSettings struct {
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
} `json:"userSettings"`
|
||||
} `json:"initialState"`
|
||||
RouteData struct {
|
||||
RouteContext struct {
|
||||
ActiveWorkspaceID string `json:"activeWorkspaceId"`
|
||||
} `json:"routeContext"`
|
||||
} `json:"routeData"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal payload: %v", err)
|
||||
}
|
||||
if decoded.InitialState.Workspaces.ActiveID != cookieWorkspace.ID {
|
||||
t.Fatalf("initial active workspace = %q, want %q", decoded.InitialState.Workspaces.ActiveID, cookieWorkspace.ID)
|
||||
}
|
||||
if decoded.InitialState.UserSettings.WorkspaceID != cookieWorkspace.ID {
|
||||
t.Fatalf("user settings workspace = %q, want %q", decoded.InitialState.UserSettings.WorkspaceID, cookieWorkspace.ID)
|
||||
}
|
||||
if decoded.RouteData.RouteContext.ActiveWorkspaceID != cookieWorkspace.ID {
|
||||
t.Fatalf("route active workspace = %q, want %q", decoded.RouteData.RouteContext.ActiveWorkspaceID, cookieWorkspace.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func nonFallbackWorkspace(t *testing.T, ctx context.Context, taskSvc *taskservice.Service) *models.Workspace {
|
||||
t.Helper()
|
||||
workspaces, err := taskSvc.ListWorkspaces(ctx)
|
||||
|
||||
@@ -367,16 +367,28 @@ function useRouteData({
|
||||
|
||||
async function bootstrap() {
|
||||
const [workspacesResponse, settingsResponse] = await Promise.all([
|
||||
listWorkspaces({ cache: "no-store" }).catch(() => ({ workspaces: [] })),
|
||||
listWorkspaces({ cache: "no-store" }).catch(() => null),
|
||||
fetchUserSettings({ cache: "no-store" }).catch(() => null),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
const workspaces = workspacesResponse.workspaces;
|
||||
const workspaceId = settingsResponse?.settings?.workspace_id || workspaces[0]?.id || null;
|
||||
const settingsWorkspaceId = settingsResponse?.settings?.workspace_id || null;
|
||||
const settingsWorkflowId = settingsResponse?.settings?.workflow_filter_id || null;
|
||||
const storeWorkspaceId = store.getState().workspaces.activeId;
|
||||
const cookieWorkspaceId = readActiveWorkspaceCookie();
|
||||
const workspaceItems =
|
||||
workspacesResponse?.workspaces.map(mapWorkspaceItem) ?? store.getState().workspaces.items;
|
||||
const workspaceId =
|
||||
workspaceItems.length > 0
|
||||
? resolveActiveId(
|
||||
workspaceItems,
|
||||
storeWorkspaceId,
|
||||
cookieWorkspaceId,
|
||||
settingsWorkspaceId,
|
||||
)
|
||||
: firstKnownWorkspaceId(storeWorkspaceId, cookieWorkspaceId, settingsWorkspaceId);
|
||||
store.getState().hydrate({
|
||||
workspaces: { items: workspaces, activeId: workspaceId },
|
||||
workspaces: { items: workspaceItems, activeId: workspaceId },
|
||||
workflows: { items: store.getState().workflows.items, activeId: settingsWorkflowId },
|
||||
userSettings: { ...mapUserSettingsResponse(settingsResponse), workspaceId },
|
||||
});
|
||||
@@ -422,6 +434,14 @@ function listWorkspaceWorkflowSteps(workspaceId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function firstKnownWorkspaceId(...ids: (string | null | undefined)[]): string | null {
|
||||
for (const id of ids) {
|
||||
const value = id?.trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapWorkflowItem(workflow: Workflow) {
|
||||
return {
|
||||
id: workflow.id,
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { StateProvider } from "@/components/state-provider";
|
||||
import { SpaRoutes } from "./spa-routes";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listWorkspaces: vi.fn(),
|
||||
listRepositories: vi.fn(),
|
||||
listWorkflows: vi.fn(),
|
||||
fetchUserSettings: vi.fn(),
|
||||
fetchJson: vi.fn(),
|
||||
}));
|
||||
|
||||
const DEFAULT_WORKSPACE_ID = "ws-default";
|
||||
const SELECTED_WORKSPACE_ID = "ws-selected";
|
||||
const TEST_TIMESTAMP = "2026-06-24T00:00:00Z";
|
||||
|
||||
vi.mock("@/app/github/github-page-client", () => ({
|
||||
GitHubPageClient: ({ workspaceId }: { workspaceId?: string }) => (
|
||||
<div data-testid="github-page" data-workspace-id={workspaceId ?? ""} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/domains/workspace-api", () => ({
|
||||
listWorkspaces: mocks.listWorkspaces,
|
||||
listRepositories: mocks.listRepositories,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/domains/kanban-api", () => ({
|
||||
listWorkflows: mocks.listWorkflows,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/domains/settings-api", () => ({
|
||||
fetchUserSettings: mocks.fetchUserSettings,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/client", () => ({
|
||||
fetchJson: mocks.fetchJson,
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
document.cookie = "kandev-active-workspace=; path=/; max-age=0";
|
||||
document.cookie = "office-active-workspace=; path=/; max-age=0";
|
||||
window.history.replaceState({}, "", "/");
|
||||
});
|
||||
|
||||
describe("SpaRoutes data-backed workspace context", () => {
|
||||
it("keeps the currently active workspace when opening GitHub from another workspace", async () => {
|
||||
mockGitHubWorkspaceBootstrap();
|
||||
|
||||
render(
|
||||
<StateProvider
|
||||
initialState={{
|
||||
workspaces: {
|
||||
items: [workspaceState(DEFAULT_WORKSPACE_ID), workspaceState(SELECTED_WORKSPACE_ID)],
|
||||
activeId: SELECTED_WORKSPACE_ID,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SpaRoutes />
|
||||
</StateProvider>,
|
||||
);
|
||||
|
||||
await expectSelectedWorkspace();
|
||||
});
|
||||
|
||||
it("uses the active workspace cookie when the store has no active workspace yet", async () => {
|
||||
document.cookie = `kandev-active-workspace=${SELECTED_WORKSPACE_ID}; path=/`;
|
||||
mockGitHubWorkspaceBootstrap();
|
||||
|
||||
render(
|
||||
<StateProvider>
|
||||
<SpaRoutes />
|
||||
</StateProvider>,
|
||||
);
|
||||
|
||||
await expectSelectedWorkspace();
|
||||
});
|
||||
|
||||
it("keeps a known active workspace when the workspace list request fails", async () => {
|
||||
document.cookie = `kandev-active-workspace=${SELECTED_WORKSPACE_ID}; path=/`;
|
||||
mockGitHubWorkspaceBootstrap({ workspacesError: new Error("network down") });
|
||||
|
||||
render(
|
||||
<StateProvider>
|
||||
<SpaRoutes />
|
||||
</StateProvider>,
|
||||
);
|
||||
|
||||
await expectSelectedWorkspace();
|
||||
});
|
||||
});
|
||||
|
||||
function mockGitHubWorkspaceBootstrap({ workspacesError }: { workspacesError?: Error } = {}) {
|
||||
window.history.replaceState({}, "", "/github");
|
||||
if (workspacesError) {
|
||||
mocks.listWorkspaces.mockRejectedValue(workspacesError);
|
||||
} else {
|
||||
mocks.listWorkspaces.mockResolvedValue({
|
||||
workspaces: [workspace(DEFAULT_WORKSPACE_ID), workspace(SELECTED_WORKSPACE_ID)],
|
||||
});
|
||||
}
|
||||
mocks.fetchUserSettings.mockResolvedValue({
|
||||
settings: {
|
||||
workspace_id: DEFAULT_WORKSPACE_ID,
|
||||
workflow_filter_id: "",
|
||||
repository_ids: [],
|
||||
updated_at: TEST_TIMESTAMP,
|
||||
},
|
||||
});
|
||||
mocks.listWorkflows.mockResolvedValue({
|
||||
workflows: [workflow("wf-selected", SELECTED_WORKSPACE_ID)],
|
||||
});
|
||||
mocks.listRepositories.mockResolvedValue({ repositories: [] });
|
||||
mocks.fetchJson.mockResolvedValue({ steps: [], total: 0 });
|
||||
}
|
||||
|
||||
async function expectSelectedWorkspace() {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("github-page").getAttribute("data-workspace-id")).toBe(
|
||||
SELECTED_WORKSPACE_ID,
|
||||
);
|
||||
});
|
||||
expect(mocks.listWorkflows).toHaveBeenCalledWith(SELECTED_WORKSPACE_ID, {
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
function workspace(id: string) {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
description: null,
|
||||
owner_id: "owner-1",
|
||||
default_executor_id: null,
|
||||
default_environment_id: null,
|
||||
default_agent_profile_id: null,
|
||||
default_config_agent_profile_id: null,
|
||||
office_workflow_id: null,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
updated_at: TEST_TIMESTAMP,
|
||||
};
|
||||
}
|
||||
|
||||
function workspaceState(id: string) {
|
||||
return workspace(id);
|
||||
}
|
||||
|
||||
function workflow(id: string, workspaceId: string) {
|
||||
return {
|
||||
id,
|
||||
workspace_id: workspaceId,
|
||||
name: id,
|
||||
description: null,
|
||||
sort_order: 0,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
updated_at: TEST_TIMESTAMP,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user