Fix sidebar project removal
Remove projects through a daemon RPC so the sidebar can delete the project record after archiving its active workspaces. Reopening a removed directory recreates the missing project record so local projects keep their derived basename, and History skips orphaned archived workspace rows. Fixes #1583.
This commit is contained in:
@@ -197,7 +197,10 @@ import {
|
||||
type ProjectConfigRpcError,
|
||||
} from "../utils/paseo-config-file.js";
|
||||
import { buildMetadataPrompt } from "../utils/build-metadata-prompt.js";
|
||||
import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
|
||||
import {
|
||||
archivePersistedWorkspaceRecord,
|
||||
archiveWorkspaceContents,
|
||||
} from "./workspace-archive-service.js";
|
||||
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
||||
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
||||
import {
|
||||
@@ -1703,6 +1706,17 @@ export class Session {
|
||||
return this.buildProjectPlacementForWorkspace(workspace);
|
||||
}
|
||||
|
||||
private async buildProjectPlacementForExistingWorkspaceProject(
|
||||
workspaceId: string,
|
||||
): Promise<ProjectPlacementPayload | null> {
|
||||
const workspace = await this.workspaceRegistry.get(workspaceId);
|
||||
if (!workspace) return null;
|
||||
|
||||
const project = await this.projectRegistry.get(workspace.projectId);
|
||||
if (!project) return null;
|
||||
return this.buildProjectPlacementForWorkspace(workspace, project);
|
||||
}
|
||||
|
||||
private async forwardAgentUpdate(agent: ManagedAgent): Promise<void> {
|
||||
try {
|
||||
const subscription = this.agentUpdatesSubscription;
|
||||
@@ -2184,6 +2198,8 @@ export class Session {
|
||||
return this.handleOpenProjectRequest(msg);
|
||||
case "archive_workspace_request":
|
||||
return this.handleArchiveWorkspaceRequest(msg);
|
||||
case "project.remove.request":
|
||||
return this.handleProjectRemoveRequest(msg);
|
||||
case "workspace.create.request":
|
||||
return this.handleWorkspaceCreateRequest(msg);
|
||||
case "workspace.clear_attention.request":
|
||||
@@ -2685,6 +2701,97 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleProjectRemoveRequest(
|
||||
request: Extract<SessionInboundMessage, { type: "project.remove.request" }>,
|
||||
): Promise<void> {
|
||||
const { projectId, requestId } = request;
|
||||
this.sessionLogger.info({ projectId, requestId }, "session: project.remove.request");
|
||||
|
||||
try {
|
||||
const projectWorkspaces = (await this.workspaceRegistry.list()).filter(
|
||||
(workspace) => workspace.projectId === projectId,
|
||||
);
|
||||
const activeWorkspaceIds = projectWorkspaces
|
||||
.filter((workspace) => !workspace.archivedAt)
|
||||
.map((workspace) => workspace.workspaceId);
|
||||
|
||||
if (activeWorkspaceIds.length > 0) {
|
||||
this.markWorkspaceArchiving(activeWorkspaceIds, new Date().toISOString());
|
||||
await this.emitWorkspaceUpdatesForWorkspaceIds(activeWorkspaceIds, {
|
||||
skipReconcile: true,
|
||||
});
|
||||
}
|
||||
|
||||
const removedWorkspaceIds: string[] = [];
|
||||
try {
|
||||
for (const workspaceId of activeWorkspaceIds) {
|
||||
await archiveWorkspaceContents(
|
||||
{
|
||||
agentManager: this.agentManager,
|
||||
agentStorage: this.agentStorage,
|
||||
killTerminalsForWorkspace: (id) =>
|
||||
this.terminalController.killTerminalsForWorkspace(id),
|
||||
sessionLogger: this.sessionLogger,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
await this.archiveWorkspaceRecord(workspaceId);
|
||||
removedWorkspaceIds.push(workspaceId);
|
||||
}
|
||||
|
||||
await this.projectRegistry.remove(projectId);
|
||||
} finally {
|
||||
if (activeWorkspaceIds.length > 0) {
|
||||
this.clearWorkspaceArchiving(activeWorkspaceIds);
|
||||
}
|
||||
}
|
||||
|
||||
const updateIds =
|
||||
removedWorkspaceIds.length > 0
|
||||
? removedWorkspaceIds
|
||||
: [projectWorkspaces[0]?.workspaceId ?? projectId];
|
||||
await this.emitWorkspaceUpdatesForWorkspaceIds(updateIds, {
|
||||
skipReconcile: true,
|
||||
removedProjectId: projectId,
|
||||
});
|
||||
|
||||
this.emit({
|
||||
type: "project.remove.response",
|
||||
payload: {
|
||||
requestId,
|
||||
projectId,
|
||||
accepted: true,
|
||||
removedWorkspaceIds,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.sessionLogger.error(
|
||||
{ err: error, projectId, requestId },
|
||||
"session: project.remove.request error",
|
||||
);
|
||||
this.emit({
|
||||
type: "activity_log",
|
||||
payload: {
|
||||
id: uuidv4(),
|
||||
timestamp: new Date(),
|
||||
type: "error",
|
||||
content: `Failed to remove project: ${getErrorMessage(error)}`,
|
||||
},
|
||||
});
|
||||
this.emit({
|
||||
type: "project.remove.response",
|
||||
payload: {
|
||||
requestId,
|
||||
projectId,
|
||||
accepted: false,
|
||||
removedWorkspaceIds: [],
|
||||
error: getErrorMessageOr(error, "Failed to remove project"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleWorkspaceTitleSetRequest(
|
||||
workspaceId: string,
|
||||
title: string | null,
|
||||
@@ -6569,7 +6676,10 @@ export class Session {
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
|
||||
const placementPromise =
|
||||
request.type === "fetch_agent_history_request"
|
||||
? this.buildProjectPlacementForExistingWorkspaceProject(workspaceId)
|
||||
: this.buildProjectPlacementForWorkspaceId(workspaceId);
|
||||
placementByWorkspaceId.set(workspaceId, placementPromise);
|
||||
return placementPromise;
|
||||
};
|
||||
@@ -7023,6 +7133,9 @@ export class Session {
|
||||
input.workspace.kind === kind &&
|
||||
input.workspace.displayName === displayName
|
||||
) {
|
||||
if (!input.project) {
|
||||
await this.projectRegistry.upsert(projectRecord);
|
||||
}
|
||||
return this.ensureWorkspaceRecordUnarchived(input.workspace);
|
||||
}
|
||||
|
||||
@@ -7327,7 +7440,7 @@ export class Session {
|
||||
|
||||
private async emitWorkspaceUpdatesForWorkspaceIds(
|
||||
workspaceIds: Iterable<string>,
|
||||
options?: { skipReconcile?: boolean; dedupeGitState?: boolean },
|
||||
options?: { skipReconcile?: boolean; dedupeGitState?: boolean; removedProjectId?: string },
|
||||
): Promise<void> {
|
||||
const subscription = this.workspaceUpdatesSubscription;
|
||||
if (!subscription) {
|
||||
@@ -7356,22 +7469,14 @@ export class Session {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
||||
if (watchTarget && this.onBranchChanged) {
|
||||
const newBranchName = nextWorkspace?.name ?? null;
|
||||
if (newBranchName !== watchTarget.lastBranchName) {
|
||||
this.onBranchChanged(workspaceId, watchTarget.lastBranchName, newBranchName);
|
||||
}
|
||||
}
|
||||
this.rememberWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
|
||||
this.recordWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
|
||||
|
||||
if (!nextWorkspace) {
|
||||
subscription.lastEmittedByWorkspaceId.delete(workspaceId);
|
||||
this.bufferOrEmitWorkspaceUpdate(subscription, {
|
||||
kind: "remove",
|
||||
id: workspaceId,
|
||||
...(await this.resolveEmptyProjectForArchivedWorkspace(workspaceId)),
|
||||
});
|
||||
this.bufferOrEmitWorkspaceUpdate(
|
||||
subscription,
|
||||
await this.buildWorkspaceRemoveUpdatePayload(workspaceId, options?.removedProjectId),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -7397,6 +7502,34 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private recordWorkspaceGitDescriptorState(
|
||||
workspaceId: string,
|
||||
nextWorkspace: WorkspaceDescriptorPayload | null,
|
||||
): void {
|
||||
const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
||||
if (watchTarget && this.onBranchChanged) {
|
||||
const newBranchName = nextWorkspace?.name ?? null;
|
||||
if (newBranchName !== watchTarget.lastBranchName) {
|
||||
this.onBranchChanged(workspaceId, watchTarget.lastBranchName, newBranchName);
|
||||
}
|
||||
}
|
||||
this.rememberWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
|
||||
}
|
||||
|
||||
private async buildWorkspaceRemoveUpdatePayload(
|
||||
workspaceId: string,
|
||||
removedProjectId?: string,
|
||||
): Promise<WorkspaceUpdatePayload> {
|
||||
if (removedProjectId) {
|
||||
return { kind: "remove", id: workspaceId, removedProjectId };
|
||||
}
|
||||
return {
|
||||
kind: "remove",
|
||||
id: workspaceId,
|
||||
...(await this.resolveEmptyProjectForArchivedWorkspace(workspaceId)),
|
||||
};
|
||||
}
|
||||
|
||||
// When a workspace is archived its project may become empty. Resolve the
|
||||
// now-empty project parent so the `remove` update can carry it, keeping the
|
||||
// sidebar's empty project row in sync without a full re-hydration.
|
||||
|
||||
@@ -84,6 +84,7 @@ interface SessionTestAccess {
|
||||
archive(projectId: string, archivedAt: string): Promise<void>;
|
||||
get(id: string): Promise<unknown>;
|
||||
upsert(record: unknown): Promise<unknown>;
|
||||
remove(projectId: string): Promise<void>;
|
||||
};
|
||||
agentStorage: {
|
||||
list(...args: unknown[]): Promise<unknown[]>;
|
||||
@@ -2391,6 +2392,94 @@ test("fetch_agent_history_request pages archived historical rows separately", as
|
||||
expect(session.agentUpdatesSubscription).toBeNull();
|
||||
});
|
||||
|
||||
test("fetch_agent_history_request skips rows whose workspace project record is missing", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForWorkspaceTests();
|
||||
const stableCwd = path.resolve("/tmp/stable-history");
|
||||
const orphanCwd = path.resolve("/tmp/orphan-history");
|
||||
const stableProject = createPersistedProjectRecord({
|
||||
projectId: "proj-stable-history",
|
||||
rootPath: stableCwd,
|
||||
kind: "non_git",
|
||||
displayName: "stable history",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
const stableWorkspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-stable-history",
|
||||
projectId: stableProject.projectId,
|
||||
cwd: stableCwd,
|
||||
kind: "directory",
|
||||
displayName: "stable history",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
const orphanWorkspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-orphan-history",
|
||||
projectId: orphanCwd,
|
||||
cwd: orphanCwd,
|
||||
kind: "local_checkout",
|
||||
displayName: "main",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
|
||||
session.emit = (message) => {
|
||||
if (isSessionOutboundMessage(message)) emitted.push(message);
|
||||
};
|
||||
session.projectRegistry.get = async (projectId: string) =>
|
||||
projectId === stableProject.projectId ? stableProject : null;
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
[stableWorkspace, orphanWorkspace].find((workspace) => workspace.workspaceId === workspaceId) ??
|
||||
null;
|
||||
session.listAgentPayloads = async () => [
|
||||
makeAgent({
|
||||
id: "history-orphan",
|
||||
cwd: orphanCwd,
|
||||
workspaceId: orphanWorkspace.workspaceId,
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:01:00.000Z",
|
||||
}),
|
||||
makeAgent({
|
||||
id: "history-stable",
|
||||
cwd: stableCwd,
|
||||
workspaceId: stableWorkspace.workspaceId,
|
||||
status: "closed",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
];
|
||||
|
||||
await session.handleMessage({
|
||||
type: "fetch_agent_history_request",
|
||||
requestId: "req-history-orphan",
|
||||
page: { limit: 25 },
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "fetch_agent_history_response",
|
||||
payload: expect.objectContaining({
|
||||
requestId: "req-history-orphan",
|
||||
entries: [
|
||||
expect.objectContaining({
|
||||
agent: expect.objectContaining({ id: "history-stable" }),
|
||||
project: expect.objectContaining({
|
||||
projectKey: stableProject.projectId,
|
||||
projectName: "stable history",
|
||||
workspaceName: "stable history",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("fetch_recent_provider_sessions_request lists importable provider sessions by handle", async () => {
|
||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||
const session = createSessionForWorkspaceTests();
|
||||
@@ -3035,6 +3124,182 @@ test("archiving the last workspace emits a remove carrying the now-empty project
|
||||
});
|
||||
});
|
||||
|
||||
test("project.remove.request archives active workspaces and removes the project record", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForWorkspaceTests({
|
||||
onMessage: (message) => emitted.push(message),
|
||||
});
|
||||
const project = createPersistedProjectRecord({
|
||||
projectId: "proj-remove-with-workspace",
|
||||
rootPath: REPO_CWD,
|
||||
kind: "git",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
const workspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-project-remove",
|
||||
projectId: project.projectId,
|
||||
cwd: REPO_CWD,
|
||||
kind: "local_checkout",
|
||||
displayName: "main",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
const projects = new Map<string, PersistedProjectRecord>([[project.projectId, project]]);
|
||||
const workspaces = new Map<string, PersistedWorkspaceRecord>([
|
||||
[workspace.workspaceId, workspace],
|
||||
]);
|
||||
|
||||
session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null;
|
||||
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||
session.projectRegistry.remove = async (projectId: string) => {
|
||||
projects.delete(projectId);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||
session.workspaceRegistry.archive = async (workspaceId: string, archivedAt: string) => {
|
||||
const existing = workspaces.get(workspaceId);
|
||||
if (!existing) return;
|
||||
workspaces.set(workspaceId, { ...existing, updatedAt: archivedAt, archivedAt });
|
||||
};
|
||||
session.workspaceUpdatesSubscription = {
|
||||
subscriptionId: "sub-project-remove",
|
||||
filter: undefined,
|
||||
isBootstrapping: false,
|
||||
pendingUpdatesByWorkspaceId: new Map(),
|
||||
lastEmittedByWorkspaceId: new Map(),
|
||||
};
|
||||
session.reconcileActiveWorkspaceRecords = async () => new Set();
|
||||
session.listAgentPayloads = async () => [];
|
||||
session.buildWorkspaceDescriptorMap = async (options: { workspaceIds?: Iterable<string> }) => {
|
||||
const workspaceIds = Array.from(options.workspaceIds ?? workspaces.keys());
|
||||
const descriptors = new Map<string, unknown>();
|
||||
for (const workspaceId of workspaceIds) {
|
||||
const record = workspaces.get(workspaceId);
|
||||
if (!record || record.archivedAt) continue;
|
||||
descriptors.set(workspaceId, {
|
||||
id: record.workspaceId,
|
||||
projectId: record.projectId,
|
||||
projectDisplayName: "repo",
|
||||
projectRootPath: REPO_CWD,
|
||||
projectKind: "git",
|
||||
workspaceKind: record.kind,
|
||||
name: record.displayName,
|
||||
status: "idle",
|
||||
activityAt: null,
|
||||
});
|
||||
}
|
||||
return descriptors;
|
||||
};
|
||||
|
||||
await session.handleMessage({
|
||||
type: "project.remove.request",
|
||||
projectId: project.projectId,
|
||||
requestId: "req-remove-project",
|
||||
});
|
||||
|
||||
expect(projects.has(project.projectId)).toBe(false);
|
||||
expect(workspaces.get(workspace.workspaceId)).toEqual({
|
||||
...workspace,
|
||||
updatedAt: expect.any(String),
|
||||
archivedAt: expect.any(String),
|
||||
});
|
||||
expect(findByType(emitted, "project.remove.response")?.payload).toEqual({
|
||||
requestId: "req-remove-project",
|
||||
projectId: project.projectId,
|
||||
accepted: true,
|
||||
removedWorkspaceIds: [workspace.workspaceId],
|
||||
error: null,
|
||||
});
|
||||
const workspaceUpdates = filterByType(emitted, "workspace_update");
|
||||
expect(workspaceUpdates.at(-1)?.payload).toEqual({
|
||||
kind: "remove",
|
||||
id: workspace.workspaceId,
|
||||
removedProjectId: project.projectId,
|
||||
});
|
||||
});
|
||||
|
||||
test("project.remove.request removes an already-empty project", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForWorkspaceTests({
|
||||
onMessage: (message) => emitted.push(message),
|
||||
});
|
||||
const project = createPersistedProjectRecord({
|
||||
projectId: "proj-remove-empty",
|
||||
rootPath: REPO_CWD,
|
||||
kind: "git",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
const archivedWorkspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-project-remove-empty",
|
||||
projectId: project.projectId,
|
||||
cwd: REPO_CWD,
|
||||
kind: "local_checkout",
|
||||
displayName: "main",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
archivedAt: "2026-03-02T12:00:00.000Z",
|
||||
});
|
||||
const projects = new Map<string, PersistedProjectRecord>([[project.projectId, project]]);
|
||||
const workspaces = new Map<string, PersistedWorkspaceRecord>([
|
||||
[archivedWorkspace.workspaceId, archivedWorkspace],
|
||||
]);
|
||||
|
||||
session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null;
|
||||
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||
session.projectRegistry.remove = async (projectId: string) => {
|
||||
projects.delete(projectId);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||
session.workspaceRegistry.archive = async (workspaceId: string, archivedAt: string) => {
|
||||
const existing = workspaces.get(workspaceId);
|
||||
if (!existing) return;
|
||||
workspaces.set(workspaceId, { ...existing, updatedAt: archivedAt, archivedAt });
|
||||
};
|
||||
session.workspaceUpdatesSubscription = {
|
||||
subscriptionId: "sub-empty-project-remove",
|
||||
filter: undefined,
|
||||
isBootstrapping: false,
|
||||
pendingUpdatesByWorkspaceId: new Map(),
|
||||
lastEmittedByWorkspaceId: new Map(),
|
||||
};
|
||||
session.reconcileActiveWorkspaceRecords = async () => new Set();
|
||||
session.listAgentPayloads = async () => [];
|
||||
session.buildWorkspaceDescriptorMap = async () => new Map();
|
||||
|
||||
await session.handleMessage({
|
||||
type: "project.remove.request",
|
||||
projectId: project.projectId,
|
||||
requestId: "req-remove-empty-project",
|
||||
});
|
||||
|
||||
expect(projects.has(project.projectId)).toBe(false);
|
||||
expect(workspaces.get(archivedWorkspace.workspaceId)).toEqual(archivedWorkspace);
|
||||
expect(findByType(emitted, "project.remove.response")?.payload).toEqual({
|
||||
requestId: "req-remove-empty-project",
|
||||
projectId: project.projectId,
|
||||
accepted: true,
|
||||
removedWorkspaceIds: [],
|
||||
error: null,
|
||||
});
|
||||
expect(filterByType(emitted, "workspace_update")).toEqual([
|
||||
{
|
||||
type: "workspace_update",
|
||||
payload: {
|
||||
kind: "remove",
|
||||
id: archivedWorkspace.workspaceId,
|
||||
removedProjectId: project.projectId,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("create paseo worktree request returns a registered workspace descriptor", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const createdAt = "2026-05-12T12:00:00.000Z";
|
||||
@@ -4058,6 +4323,67 @@ test("open_project_request unarchives an existing archived workspace and project
|
||||
expect(response?.payload.workspace?.id).toBe(workspaceId);
|
||||
});
|
||||
|
||||
test("open_project_request recreates a missing project record when unarchiving its workspace", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForWorkspaceTests();
|
||||
const projects = new Map<string, ReturnType<typeof createPersistedProjectRecord>>();
|
||||
const workspaces = new Map<string, ReturnType<typeof createPersistedWorkspaceRecord>>();
|
||||
|
||||
const cwd = REPO_CWD;
|
||||
const workspaceId = "ws-repo-project-removed";
|
||||
workspaces.set(
|
||||
workspaceId,
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId,
|
||||
projectId: cwd,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-10T00:00:00.000Z",
|
||||
archivedAt: "2026-03-10T00:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
session.emit = (message) => {
|
||||
if (isSessionOutboundMessage(message)) emitted.push(message);
|
||||
};
|
||||
session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null;
|
||||
session.projectRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedProjectRecord>,
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
workspaces.set(record.workspaceId, record);
|
||||
};
|
||||
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||
|
||||
await session.handleMessage({
|
||||
type: "open_project_request",
|
||||
cwd,
|
||||
requestId: "req-open-removed-project",
|
||||
});
|
||||
|
||||
expect(projects.get(cwd)).toEqual(
|
||||
expect.objectContaining({
|
||||
projectId: cwd,
|
||||
displayName: "repo",
|
||||
archivedAt: null,
|
||||
}),
|
||||
);
|
||||
expect(workspaces.get(workspaceId)?.archivedAt).toBeNull();
|
||||
const response = findByType(emitted, "open_project_response");
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.id).toBe(workspaceId);
|
||||
expect(response?.payload.workspace?.projectDisplayName).toBe("repo");
|
||||
});
|
||||
|
||||
test("refresh_agent_request unarchives the owning workspace when its directory exists", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForWorkspaceTests({
|
||||
|
||||
@@ -1157,6 +1157,8 @@ export class VoiceAssistantWebSocketServer {
|
||||
checkoutRefresh: true,
|
||||
// COMPAT(workspaceMultiplicity): added in v0.1.97, drop the gate when floor >= v0.1.97
|
||||
workspaceMultiplicity: true,
|
||||
// COMPAT(projectRemove): added in v0.1.97, drop the gate when floor >= v0.1.97.
|
||||
projectRemove: true,
|
||||
// COMPAT(worktreeRestore): added in v0.1.98, drop the gate when floor >= v0.1.98
|
||||
worktreeRestore: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user