mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix(app): require explicit workspace recovery
This commit is contained in:
@@ -838,6 +838,18 @@ export const WorkspacePinSetRequestSchema = z.object({
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const WorkspaceRecoveryInspectRequestSchema = z.object({
|
||||
type: z.literal("workspace.recovery.inspect.request"),
|
||||
workspaceId: z.string(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const WorkspaceRecoveryRestoreRequestSchema = z.object({
|
||||
type: z.literal("workspace.recovery.restore.request"),
|
||||
workspaceId: z.string(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const SetVoiceModeMessageSchema = z.object({
|
||||
type: z.literal("set_voice_mode"),
|
||||
enabled: z.boolean(),
|
||||
@@ -1471,6 +1483,47 @@ export const WorkspacePinSetResponseSchema = z.object({
|
||||
payload: WorkspacePinSetResponsePayloadSchema,
|
||||
});
|
||||
|
||||
export const WorkspaceRecoveryStateSchema = z.discriminatedUnion("kind", [
|
||||
z.object({
|
||||
kind: z.literal("recoverable"),
|
||||
workspaceId: z.string(),
|
||||
workspaceName: z.string(),
|
||||
action: z.enum(["unarchive", "restore"]),
|
||||
branch: z.string().nullable(),
|
||||
}),
|
||||
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",
|
||||
]),
|
||||
message: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const WorkspaceRecoveryInspectResponseSchema = z.object({
|
||||
type: z.literal("workspace.recovery.inspect.response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
state: WorkspaceRecoveryStateSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
export const WorkspaceRecoveryRestoreResponseSchema = z.object({
|
||||
type: z.literal("workspace.recovery.restore.response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
accepted: z.boolean(),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const SetVoiceModeResponseMessageSchema = z.object({
|
||||
type: z.literal("set_voice_mode_response"),
|
||||
payload: z.object({
|
||||
@@ -2143,6 +2196,8 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
ProjectRemoveRequestSchema,
|
||||
WorkspaceTitleSetRequestSchema,
|
||||
WorkspacePinSetRequestSchema,
|
||||
WorkspaceRecoveryInspectRequestSchema,
|
||||
WorkspaceRecoveryRestoreRequestSchema,
|
||||
SetVoiceModeMessageSchema,
|
||||
SendAgentMessageRequestSchema,
|
||||
WaitForFinishRequestSchema,
|
||||
@@ -2449,6 +2504,8 @@ export const ServerInfoStatusPayloadSchema = z
|
||||
projectAdd: z.boolean().optional(),
|
||||
// COMPAT(worktreeRestore): added in v0.1.97, drop the gate when floor >= v0.1.97
|
||||
worktreeRestore: z.boolean().optional(),
|
||||
// COMPAT(workspaceRecovery): added in v0.1.105, remove after 2027-01-11 once daemon floor >= v0.1.105.
|
||||
workspaceRecovery: z.boolean().optional(),
|
||||
// COMPAT(providerUsageList): added in v0.1.98, drop the gate when daemon floor >= v0.1.98.
|
||||
providerUsageList: z.boolean().optional(),
|
||||
// COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
|
||||
@@ -4496,6 +4553,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
ProjectRemoveResponseSchema,
|
||||
WorkspaceTitleSetResponseSchema,
|
||||
WorkspacePinSetResponseSchema,
|
||||
WorkspaceRecoveryInspectResponseSchema,
|
||||
WorkspaceRecoveryRestoreResponseSchema,
|
||||
WaitForFinishResponseMessageSchema,
|
||||
AgentPermissionRequestMessageSchema,
|
||||
AgentPermissionResolvedMessageSchema,
|
||||
@@ -4660,6 +4719,13 @@ export type WorkspaceTitleSetResponsePayload = z.infer<
|
||||
>;
|
||||
export type WorkspacePinSetResponse = z.infer<typeof WorkspacePinSetResponseSchema>;
|
||||
export type WorkspacePinSetResponsePayload = z.infer<typeof WorkspacePinSetResponsePayloadSchema>;
|
||||
export type WorkspaceRecoveryState = z.infer<typeof WorkspaceRecoveryStateSchema>;
|
||||
export type WorkspaceRecoveryInspectResponse = z.infer<
|
||||
typeof WorkspaceRecoveryInspectResponseSchema
|
||||
>;
|
||||
export type WorkspaceRecoveryRestoreResponse = z.infer<
|
||||
typeof WorkspaceRecoveryRestoreResponseSchema
|
||||
>;
|
||||
export type WorkspaceCreateRequest = z.infer<typeof WorkspaceCreateRequestSchema>;
|
||||
export type WorkspaceCreateResponse = z.infer<typeof WorkspaceCreateResponseSchema>;
|
||||
export type ProjectRenameResponsePayload = z.infer<typeof ProjectRenameResponsePayloadSchema>;
|
||||
@@ -4793,6 +4859,8 @@ export type ProjectRenameRequest = z.infer<typeof ProjectRenameRequestSchema>;
|
||||
export type ProjectRemoveRequest = z.infer<typeof ProjectRemoveRequestSchema>;
|
||||
export type WorkspaceTitleSetRequest = z.infer<typeof WorkspaceTitleSetRequestSchema>;
|
||||
export type WorkspacePinSetRequest = z.infer<typeof WorkspacePinSetRequestSchema>;
|
||||
export type WorkspaceRecoveryInspectRequest = z.infer<typeof WorkspaceRecoveryInspectRequestSchema>;
|
||||
export type WorkspaceRecoveryRestoreRequest = z.infer<typeof WorkspaceRecoveryRestoreRequestSchema>;
|
||||
export type SetAgentModeRequestMessage = z.infer<typeof SetAgentModeRequestMessageSchema>;
|
||||
export type SetAgentModelRequestMessage = z.infer<typeof SetAgentModelRequestMessageSchema>;
|
||||
export type SetAgentThinkingRequestMessage = z.infer<typeof SetAgentThinkingRequestMessageSchema>;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
SessionInboundMessageSchema,
|
||||
SessionOutboundMessageSchema,
|
||||
ServerInfoStatusPayloadSchema,
|
||||
} from "./messages.js";
|
||||
|
||||
describe("workspace recovery protocol", () => {
|
||||
test("parses the read-only recovery inspection exchange", () => {
|
||||
expect(
|
||||
SessionInboundMessageSchema.parse({
|
||||
type: "workspace.recovery.inspect.request",
|
||||
requestId: "inspect-1",
|
||||
workspaceId: "wks_15a1b5630ebaab33",
|
||||
}),
|
||||
).toMatchObject({ type: "workspace.recovery.inspect.request" });
|
||||
|
||||
expect(
|
||||
SessionOutboundMessageSchema.parse({
|
||||
type: "workspace.recovery.inspect.response",
|
||||
payload: {
|
||||
requestId: "inspect-1",
|
||||
state: {
|
||||
kind: "recoverable",
|
||||
workspaceId: "wks_15a1b5630ebaab33",
|
||||
workspaceName: "Codex TDD reproduction",
|
||||
action: "restore",
|
||||
branch: "diagnose-repro-tdd",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
payload: { state: { kind: "recoverable", action: "restore" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("parses explicit restore success and retryable failure responses", () => {
|
||||
expect(
|
||||
SessionInboundMessageSchema.parse({
|
||||
type: "workspace.recovery.restore.request",
|
||||
requestId: "restore-1",
|
||||
workspaceId: "wks_15a1b5630ebaab33",
|
||||
}),
|
||||
).toMatchObject({ type: "workspace.recovery.restore.request" });
|
||||
|
||||
for (const payload of [
|
||||
{
|
||||
requestId: "restore-1",
|
||||
workspaceId: "wks_15a1b5630ebaab33",
|
||||
accepted: true,
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
requestId: "restore-2",
|
||||
workspaceId: "wks_15a1b5630ebaab33",
|
||||
accepted: false,
|
||||
error: "Project root is missing: /repo",
|
||||
},
|
||||
]) {
|
||||
expect(
|
||||
SessionOutboundMessageSchema.parse({
|
||||
type: "workspace.recovery.restore.response",
|
||||
payload,
|
||||
}),
|
||||
).toMatchObject({ payload });
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps the capability optional for older daemons", () => {
|
||||
expect(
|
||||
ServerInfoStatusPayloadSchema.parse({
|
||||
status: "server_info",
|
||||
serverId: "old-host",
|
||||
features: {},
|
||||
}).features?.workspaceRecovery,
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
ServerInfoStatusPayloadSchema.parse({
|
||||
status: "server_info",
|
||||
serverId: "new-host",
|
||||
features: { workspaceRecovery: true },
|
||||
}).features?.workspaceRecovery,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user