WIP: workspace execution refactor + notification routing + setup store updates
This commit is contained in:
@@ -28,6 +28,20 @@ describe("resolveNotificationTarget", () => {
|
||||
workspaceId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat cwd as a workspace id alias", () => {
|
||||
expect(
|
||||
resolveNotificationTarget({
|
||||
serverId: "srv-1",
|
||||
agentId: "agent-1",
|
||||
cwd: "/tmp/repo",
|
||||
}),
|
||||
).toEqual({
|
||||
serverId: "srv-1",
|
||||
agentId: "agent-1",
|
||||
workspaceId: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildNotificationRoute", () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export function resolveNotificationTarget(data: NotificationData): {
|
||||
return {
|
||||
serverId: readNonEmptyString(data, "serverId"),
|
||||
agentId: readNonEmptyString(data, "agentId"),
|
||||
workspaceId: readNonEmptyString(data, "workspaceId") ?? readNonEmptyString(data, "cwd"),
|
||||
workspaceId: readNonEmptyString(data, "workspaceId"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||
|
||||
export function resolveHydratedWorkspaceId(input: {
|
||||
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
||||
path: string | null | undefined;
|
||||
}): string | null {
|
||||
const normalizedPath = normalizeWorkspaceIdentity(input.path);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const workspace of input.workspaces ?? []) {
|
||||
if (normalizeWorkspaceIdentity(workspace.id) === normalizedPath) {
|
||||
return workspace.id;
|
||||
}
|
||||
if (normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedPath) {
|
||||
return workspace.id;
|
||||
}
|
||||
if (normalizeWorkspaceIdentity(workspace.projectRootPath) === normalizedPath) {
|
||||
return workspace.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ function workspace(
|
||||
projectKind: input.projectKind ?? "git",
|
||||
workspaceKind: input.workspaceKind ?? "worktree",
|
||||
name: input.name ?? input.id,
|
||||
status: input.status ?? "done",
|
||||
status: input.status ?? "running",
|
||||
activityAt: input.activityAt ?? null,
|
||||
diffStat: input.diffStat ?? null,
|
||||
};
|
||||
@@ -38,7 +38,7 @@ describe("resolveWorkspaceArchiveRedirectWorkspaceId", () => {
|
||||
).toBe("/repo");
|
||||
});
|
||||
|
||||
it("falls back to the project root path when the root checkout is not in the visible workspace list", () => {
|
||||
it("falls back to the host root route when no sibling workspace target exists", () => {
|
||||
const workspaces = [
|
||||
workspace({
|
||||
id: "/repo/.paseo/worktrees/feature",
|
||||
@@ -48,11 +48,12 @@ describe("resolveWorkspaceArchiveRedirectWorkspaceId", () => {
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveWorkspaceArchiveRedirectWorkspaceId({
|
||||
buildWorkspaceArchiveRedirectRoute({
|
||||
serverId: "server-1",
|
||||
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
|
||||
workspaces,
|
||||
}),
|
||||
).toBe("/repo");
|
||||
).toBe("/h/server-1");
|
||||
});
|
||||
|
||||
it("falls back to the host root route when no alternate workspace target exists", () => {
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { buildHostRootRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
|
||||
function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
import { resolveWorkspaceRouteId } from "@/utils/workspace-execution";
|
||||
|
||||
export function resolveWorkspaceArchiveRedirectWorkspaceId(input: {
|
||||
archivedWorkspaceId: string;
|
||||
workspaces: Iterable<WorkspaceDescriptor>;
|
||||
}): string | null {
|
||||
const archivedWorkspaceId = trimNonEmpty(input.archivedWorkspaceId);
|
||||
const archivedWorkspaceId = resolveWorkspaceRouteId({
|
||||
routeWorkspaceId: input.archivedWorkspaceId,
|
||||
});
|
||||
if (!archivedWorkspaceId) {
|
||||
return null;
|
||||
}
|
||||
@@ -38,11 +32,6 @@ export function resolveWorkspaceArchiveRedirectWorkspaceId(input: {
|
||||
return rootCheckoutWorkspace.id;
|
||||
}
|
||||
|
||||
const fallbackProjectRootPath = trimNonEmpty(archivedWorkspace.projectRootPath);
|
||||
if (fallbackProjectRootPath && fallbackProjectRootPath !== archivedWorkspace.id) {
|
||||
return fallbackProjectRootPath;
|
||||
}
|
||||
|
||||
const siblingWorkspace =
|
||||
sameProjectWorkspaces.find((workspace) => workspace.id !== archivedWorkspace.id) ?? null;
|
||||
return siblingWorkspace?.id ?? null;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import {
|
||||
getWorkspaceExecutionAuthority,
|
||||
requireWorkspaceExecutionAuthority,
|
||||
resolveWorkspaceIdByExecutionDirectory,
|
||||
resolveWorkspaceRouteId,
|
||||
} from "./workspace-execution";
|
||||
|
||||
function createWorkspace(
|
||||
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
|
||||
): WorkspaceDescriptor {
|
||||
return {
|
||||
id: input.id,
|
||||
projectId: input.projectId ?? "project-1",
|
||||
projectDisplayName: input.projectDisplayName ?? "Project",
|
||||
projectRootPath: input.projectRootPath ?? "/repo",
|
||||
workspaceDirectory: input.workspaceDirectory ?? "/repo",
|
||||
projectKind: input.projectKind ?? "git",
|
||||
workspaceKind: input.workspaceKind ?? "checkout",
|
||||
name: input.name ?? "main",
|
||||
status: input.status ?? "running",
|
||||
activityAt: input.activityAt ?? null,
|
||||
diffStat: input.diffStat ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveWorkspaceRouteId", () => {
|
||||
it("normalizes route workspace ids", () => {
|
||||
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " /tmp/repo/ " })).toBe("/tmp/repo");
|
||||
});
|
||||
|
||||
it("returns null for empty values", () => {
|
||||
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " " })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWorkspaceIdByExecutionDirectory", () => {
|
||||
it("matches workspace directories", () => {
|
||||
const workspaces = [
|
||||
createWorkspace({
|
||||
id: "workspace-1",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveWorkspaceIdByExecutionDirectory({
|
||||
workspaces,
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
}),
|
||||
).toBe("workspace-1");
|
||||
});
|
||||
|
||||
it("does not match project root metadata", () => {
|
||||
const workspaces = [
|
||||
createWorkspace({
|
||||
id: "workspace-1",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveWorkspaceIdByExecutionDirectory({
|
||||
workspaces,
|
||||
workspaceDirectory: "/repo",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workspace execution authority", () => {
|
||||
it("returns an explicit failure when workspace id is missing", () => {
|
||||
expect(
|
||||
getWorkspaceExecutionAuthority({
|
||||
workspaces: new Map(),
|
||||
workspaceId: null,
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
reason: "workspace_id_missing",
|
||||
message: "Workspace id is required.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an explicit failure when workspace directory is missing", () => {
|
||||
const workspaces = new Map<string, WorkspaceDescriptor>([
|
||||
[
|
||||
"workspace-1",
|
||||
createWorkspace({
|
||||
id: "workspace-1",
|
||||
workspaceDirectory: " ",
|
||||
projectRootPath: "/repo",
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
getWorkspaceExecutionAuthority({
|
||||
workspaces,
|
||||
workspaceId: "workspace-1",
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
reason: "workspace_directory_missing",
|
||||
message: "Workspace directory is missing for workspace workspace-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("never falls back to project root metadata", () => {
|
||||
const workspaces = new Map<string, WorkspaceDescriptor>([
|
||||
[
|
||||
"workspace-1",
|
||||
createWorkspace({
|
||||
id: "workspace-1",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
requireWorkspaceExecutionAuthority({
|
||||
workspaces,
|
||||
workspaceId: "workspace-1",
|
||||
}),
|
||||
).toEqual({
|
||||
workspaceId: "workspace-1",
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
workspace: workspaces.get("workspace-1"),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||
|
||||
export type WorkspaceAuthorityResult = {
|
||||
workspaceId: string;
|
||||
workspaceDirectory: string;
|
||||
workspace: WorkspaceDescriptor;
|
||||
};
|
||||
|
||||
export type WorkspaceExecutionAuthorityFailureReason =
|
||||
| "workspace_id_missing"
|
||||
| "workspace_missing"
|
||||
| "workspace_directory_missing";
|
||||
|
||||
export type WorkspaceExecutionAuthorityResult =
|
||||
| { ok: true; authority: WorkspaceAuthorityResult }
|
||||
| {
|
||||
ok: false;
|
||||
reason: WorkspaceExecutionAuthorityFailureReason;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function resolveWorkspaceRouteId(input: {
|
||||
routeWorkspaceId: string | null | undefined;
|
||||
}): string | null {
|
||||
return normalizeWorkspaceIdentity(input.routeWorkspaceId);
|
||||
}
|
||||
|
||||
export function resolveWorkspaceIdByExecutionDirectory(input: {
|
||||
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
||||
workspaceDirectory: string | null | undefined;
|
||||
}): string | null {
|
||||
const normalizedWorkspaceDirectory = normalizeWorkspaceIdentity(input.workspaceDirectory);
|
||||
if (!normalizedWorkspaceDirectory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const workspace of input.workspaces ?? []) {
|
||||
if (normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedWorkspaceDirectory) {
|
||||
return workspace.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getWorkspaceExecutionAuthority(
|
||||
input:
|
||||
| {
|
||||
workspace: WorkspaceDescriptor | null | undefined;
|
||||
}
|
||||
| {
|
||||
workspaces: Map<string, WorkspaceDescriptor> | undefined;
|
||||
workspaceId: string | null | undefined;
|
||||
},
|
||||
): WorkspaceExecutionAuthorityResult {
|
||||
const workspace =
|
||||
"workspace" in input
|
||||
? input.workspace
|
||||
: (() => {
|
||||
const normalizedWorkspaceId = normalizeWorkspaceIdentity(input.workspaceId);
|
||||
if (!normalizedWorkspaceId) {
|
||||
return null;
|
||||
}
|
||||
return input.workspaces?.get(normalizedWorkspaceId) ?? null;
|
||||
})();
|
||||
|
||||
if ("workspaces" in input) {
|
||||
const normalizedWorkspaceId = normalizeWorkspaceIdentity(input.workspaceId);
|
||||
if (!normalizedWorkspaceId) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "workspace_id_missing",
|
||||
message: "Workspace id is required.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "workspace_missing",
|
||||
message:
|
||||
"workspaces" in input
|
||||
? `Workspace not found: ${String(input.workspaceId ?? "")}`
|
||||
: "Workspace not found.",
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceDirectory = normalizeWorkspaceIdentity(workspace.workspaceDirectory);
|
||||
if (!workspaceDirectory) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "workspace_directory_missing",
|
||||
message: `Workspace directory is missing for workspace ${workspace.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
authority: {
|
||||
workspaceId: workspace.id,
|
||||
workspaceDirectory,
|
||||
workspace,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function requireWorkspaceExecutionAuthority(
|
||||
input:
|
||||
| {
|
||||
workspace: WorkspaceDescriptor | null | undefined;
|
||||
}
|
||||
| {
|
||||
workspaces: Map<string, WorkspaceDescriptor> | undefined;
|
||||
workspaceId: string | null | undefined;
|
||||
},
|
||||
): WorkspaceAuthorityResult {
|
||||
const result = getWorkspaceExecutionAuthority(input);
|
||||
if (!result.ok) {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
return result.authority;
|
||||
}
|
||||
|
||||
export function requireWorkspaceRecordId(workspaceId: string): number {
|
||||
const normalizedWorkspaceId = normalizeWorkspaceIdentity(workspaceId);
|
||||
if (!normalizedWorkspaceId) {
|
||||
throw new Error("Workspace ID is required");
|
||||
}
|
||||
|
||||
const parsedWorkspaceId = Number(normalizedWorkspaceId);
|
||||
if (!Number.isInteger(parsedWorkspaceId)) {
|
||||
throw new Error(`Workspace ID is not a persisted record ID: ${workspaceId}`);
|
||||
}
|
||||
|
||||
return parsedWorkspaceId;
|
||||
}
|
||||
|
||||
export function resolveWorkspaceExecutionDirectory(input: {
|
||||
workspaceDirectory: string | null | undefined;
|
||||
}): string | null {
|
||||
return normalizeWorkspaceIdentity(input.workspaceDirectory);
|
||||
}
|
||||
|
||||
export function requireWorkspaceExecutionDirectory(input: {
|
||||
workspaceId?: string;
|
||||
workspaceDirectory: string | null | undefined;
|
||||
}): string {
|
||||
const workspaceDirectory = resolveWorkspaceExecutionDirectory({
|
||||
workspaceDirectory: input.workspaceDirectory,
|
||||
});
|
||||
if (!workspaceDirectory) {
|
||||
throw new Error(
|
||||
input.workspaceId
|
||||
? `Workspace directory is missing for workspace ${input.workspaceId}`
|
||||
: "Workspace directory is missing.",
|
||||
);
|
||||
}
|
||||
return workspaceDirectory;
|
||||
}
|
||||
|
||||
export function resolveWorkspaceExecutionAuthority(
|
||||
input:
|
||||
| {
|
||||
workspace: WorkspaceDescriptor | null | undefined;
|
||||
}
|
||||
| {
|
||||
workspaces: Map<string, WorkspaceDescriptor> | undefined;
|
||||
workspaceId: string | null | undefined;
|
||||
},
|
||||
): WorkspaceAuthorityResult | null {
|
||||
const result = getWorkspaceExecutionAuthority(input);
|
||||
return result.ok ? result.authority : null;
|
||||
}
|
||||
|
||||
export const parseWorkspaceRecordId = requireWorkspaceRecordId;
|
||||
Reference in New Issue
Block a user