mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix: preserve workspace recovery contracts
This commit is contained in:
@@ -130,6 +130,18 @@ test.describe("Worktree restore", () => {
|
||||
await expect(page.getByRole("button", { name: "Unarchive" })).toHaveCount(0);
|
||||
expect(await fetchAgentArchivedAt(client, agent.id)).toBeNull();
|
||||
expect(existsSync(worktree.workspaceDirectory)).toBe(true);
|
||||
|
||||
await openSessions(page);
|
||||
await expectSessionRowNotArchived(page, agent.title);
|
||||
await page.getByTestId(`agent-row-${serverId}-${agent.id}`).click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId(`workspace-tab-agent_${agent.id}`).filter({ visible: true }).first(),
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
await expect(
|
||||
page.getByTestId(`workspace-deck-entry-${serverId}:${worktree.workspaceId}`),
|
||||
).toHaveCount(1);
|
||||
expect(await fetchAgentArchivedAt(client, agent.id)).toBeNull();
|
||||
});
|
||||
|
||||
test("opening a recoverable archived workspace shows an explicit Restore action without mutating it", async ({
|
||||
|
||||
@@ -77,4 +77,32 @@ describe("navigateToHostWorkspaceRoute", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a workspace open intent in the POP_TO target", () => {
|
||||
const { navigationRef, dispatch } = createNavigationRef({
|
||||
key: "root-stack",
|
||||
routes: [{ key: "host-server-1", name: "h/[serverId]" }],
|
||||
});
|
||||
registerWorkspaceRouteNavigationRef(navigationRef);
|
||||
|
||||
navigateToHostWorkspaceRoute("/h/server-1/workspace/workspace-a?open=agent%3Aagent-1");
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: "POP_TO",
|
||||
target: "root-stack",
|
||||
payload: {
|
||||
name: "h/[serverId]",
|
||||
params: {
|
||||
serverId: "server-1",
|
||||
screen: "workspace/[workspaceId]/index",
|
||||
params: {
|
||||
serverId: "server-1",
|
||||
workspaceId: "workspace-a",
|
||||
open: "agent:agent-1",
|
||||
},
|
||||
pop: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NavigationAction, NavigationContainerRefWithCurrent } from "@react
|
||||
import { router, type Href } from "expo-router";
|
||||
import {
|
||||
encodeWorkspaceIdForPathSegment,
|
||||
getHostWorkspaceOpenParamFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
@@ -82,6 +83,7 @@ function dispatchHostWorkspacePopTo(route: string): boolean {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const open = getHostWorkspaceOpenParamFromPathname(route);
|
||||
|
||||
const action: NavigationAction = {
|
||||
type: "POP_TO",
|
||||
@@ -94,6 +96,7 @@ function dispatchHostWorkspacePopTo(route: string): boolean {
|
||||
params: {
|
||||
serverId: selection.serverId,
|
||||
workspaceId: encodeWorkspaceIdForPathSegment(selection.workspaceId),
|
||||
...(open ? { open } : {}),
|
||||
},
|
||||
// React Navigation consumes this nested hint when resolving the host child screen.
|
||||
// The browser-route canonicalizer strips the resulting ?pop=true URL artifact.
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
navigateToLastWorkspace as navigateToLastWorkspacePure,
|
||||
navigateToWorkspace as navigateToWorkspacePure,
|
||||
parseActiveWorkspaceSelection,
|
||||
type NavigateToWorkspaceInput,
|
||||
type NavigateToWorkspaceDeps,
|
||||
type NavigateToWorkspaceInput,
|
||||
} from "./navigation";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||
|
||||
@@ -174,11 +174,16 @@ export function parseWorkspaceOpenIntent(
|
||||
export function parseHostWorkspaceOpenIntentFromPathname(
|
||||
pathname: string,
|
||||
): WorkspaceOpenIntent | null {
|
||||
return parseWorkspaceOpenIntent(getHostWorkspaceOpenParamFromPathname(pathname));
|
||||
}
|
||||
|
||||
export function getHostWorkspaceOpenParamFromPathname(pathname: string): string | null {
|
||||
const search = extractSearch(pathname);
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
return parseWorkspaceOpenIntent(new URLSearchParams(search).get("open"));
|
||||
const open = new URLSearchParams(search).get("open");
|
||||
return parseWorkspaceOpenIntent(open) ? open : null;
|
||||
}
|
||||
|
||||
export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { router, type Href } from "expo-router";
|
||||
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { resolveNavigateToAgent, type NavigateToAgentInput } from "./resolve";
|
||||
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
||||
|
||||
export type { NavigateToAgentInput } from "./resolve";
|
||||
|
||||
|
||||
@@ -55,13 +55,8 @@ export function useWorkspaceRecovery(input: {
|
||||
throw new Error("The host disconnected before the workspace could be recovered.");
|
||||
}
|
||||
await waitForRecoveryLoadingPresentation();
|
||||
const [result] = await Promise.allSettled([
|
||||
client.restoreWorkspace(input.workspaceId),
|
||||
waitForMinimumRecoveryLoadingTime(),
|
||||
]);
|
||||
if (result.status === "rejected") {
|
||||
throw result.reason;
|
||||
}
|
||||
await waitForMinimumRecoveryLoadingTime();
|
||||
await client.restoreWorkspace(input.workspaceId);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1494,14 +1494,7 @@ export const WorkspaceRecoveryStateSchema = z.discriminatedUnion("kind", [
|
||||
z.object({
|
||||
kind: z.literal("unavailable"),
|
||||
workspaceId: z.string(),
|
||||
reason: z.enum([
|
||||
"workspace_not_found",
|
||||
"workspace_not_archived",
|
||||
"project_not_found",
|
||||
"project_directory_missing",
|
||||
"workspace_directory_missing",
|
||||
"worktree_branch_missing",
|
||||
]),
|
||||
reason: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -83,4 +83,28 @@ describe("workspace recovery protocol", () => {
|
||||
}).features?.workspaceRecovery,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("accepts unavailable reasons added by newer daemons", () => {
|
||||
expect(
|
||||
SessionOutboundMessageSchema.parse({
|
||||
type: "workspace.recovery.inspect.response",
|
||||
payload: {
|
||||
requestId: "inspect-future",
|
||||
state: {
|
||||
kind: "unavailable",
|
||||
workspaceId: "workspace-1",
|
||||
reason: "future_recovery_constraint",
|
||||
message: "This host cannot recover the workspace yet.",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
payload: {
|
||||
state: {
|
||||
kind: "unavailable",
|
||||
reason: "future_recovery_constraint",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user