Keep archived workspaces recent in Done
This commit is contained in:
@@ -4356,6 +4356,62 @@ test("buildWorkspaceDescriptorMap computes statusEnteredAt from runtime agent fi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("buildWorkspaceDescriptorMap keeps a done workspace recent after its agents are archived", async () => {
|
||||||
|
const session = createSessionForWorkspaceTests();
|
||||||
|
const project = createPersistedProjectRecord({
|
||||||
|
projectId: "proj-archive-status-entered",
|
||||||
|
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-archive-status-entered",
|
||||||
|
projectId: project.projectId,
|
||||||
|
cwd: "/tmp/repo/archive-status-entered",
|
||||||
|
kind: "worktree",
|
||||||
|
displayName: "feature",
|
||||||
|
createdAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
});
|
||||||
|
const doneEnteredAt = "2026-05-12T09:30:00.000Z";
|
||||||
|
const archivedAt = "2026-05-12T09:45:00.000Z";
|
||||||
|
|
||||||
|
session.projectRegistry.list = async () => [project];
|
||||||
|
session.workspaceRegistry.list = async () => [workspace];
|
||||||
|
session.listAgentPayloads = async () => [
|
||||||
|
makeAgent({
|
||||||
|
id: "agent-done",
|
||||||
|
cwd: workspace.cwd,
|
||||||
|
status: "idle",
|
||||||
|
updatedAt: doneEnteredAt,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const first = await session.buildWorkspaceDescriptorMap({ includeGitData: false });
|
||||||
|
expect(first.get(workspace.workspaceId)?.status).toBe("done");
|
||||||
|
expect(first.get(workspace.workspaceId)?.statusEnteredAt).toBe(doneEnteredAt);
|
||||||
|
|
||||||
|
session.listAgentPayloads = async () => [
|
||||||
|
{
|
||||||
|
...makeAgent({
|
||||||
|
id: "agent-done",
|
||||||
|
cwd: workspace.cwd,
|
||||||
|
status: "idle",
|
||||||
|
updatedAt: doneEnteredAt,
|
||||||
|
}),
|
||||||
|
archivedAt,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const second = await session.buildWorkspaceDescriptorMap({ includeGitData: false });
|
||||||
|
expect(second.get(workspace.workspaceId)).toMatchObject({
|
||||||
|
status: "done",
|
||||||
|
statusEnteredAt: doneEnteredAt,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("buildWorkspaceDescriptorMap stamps workspace archiving state", async () => {
|
test("buildWorkspaceDescriptorMap stamps workspace archiving state", async () => {
|
||||||
const session = createSessionForWorkspaceTests();
|
const session = createSessionForWorkspaceTests();
|
||||||
const archivingAt = "2026-04-30T20:45:00.000Z";
|
const archivingAt = "2026-04-30T20:45:00.000Z";
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const FETCH_WORKSPACES_SORT_KEYS = [
|
|||||||
* mask to a lower-priority bucket, the new entry time is the unmask time
|
* mask to a lower-priority bucket, the new entry time is the unmask time
|
||||||
* (i.e., the moment the higher-priority bucket cleared), not when the
|
* (i.e., the moment the higher-priority bucket cleared), not when the
|
||||||
* underlying agent originally entered the lower-priority bucket. Cleared when
|
* underlying agent originally entered the lower-priority bucket. Cleared when
|
||||||
* the workspace becomes `done` with no contributing agents.
|
* the workspace has never had contributing agents.
|
||||||
*/
|
*/
|
||||||
interface WorkspaceBucketHistoryEntry {
|
interface WorkspaceBucketHistoryEntry {
|
||||||
bucket: WorkspaceStateBucket;
|
bucket: WorkspaceStateBucket;
|
||||||
@@ -279,7 +279,10 @@ export class WorkspaceDirectory {
|
|||||||
// - priority unmasking: when the winning bucket transitions (e.g. a
|
// - priority unmasking: when the winning bucket transitions (e.g. a
|
||||||
// higher-priority bucket cleared), the new entry time is "now";
|
// higher-priority bucket cleared), the new entry time is "now";
|
||||||
// - same-bucket emits reuse the previous entered-at;
|
// - same-bucket emits reuse the previous entered-at;
|
||||||
// - empty workspaces (no contributing agents) get `statusEnteredAt: null`.
|
// - empty workspaces that never had contributing agents get
|
||||||
|
// `statusEnteredAt: null`.
|
||||||
|
// - when archived agents leave a previously active workspace empty, keep
|
||||||
|
// the previous done timestamp or stamp the transition to done now.
|
||||||
private resolveStatusEnteredAt(params: {
|
private resolveStatusEnteredAt(params: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
winningBucket: WorkspaceStateBucket;
|
winningBucket: WorkspaceStateBucket;
|
||||||
@@ -294,16 +297,15 @@ export class WorkspaceDirectory {
|
|||||||
const { winningBucket, contributingAgents, previous, nowIso } = params;
|
const { winningBucket, contributingAgents, previous, nowIso } = params;
|
||||||
|
|
||||||
if (contributingAgents.length === 0) {
|
if (contributingAgents.length === 0) {
|
||||||
if (previous && previous.bucket !== "done") {
|
if (!previous) {
|
||||||
return { statusEnteredAt: null, recordDelete: true };
|
return { statusEnteredAt: null };
|
||||||
}
|
}
|
||||||
if (previous) {
|
|
||||||
return {
|
const enteredAt = previous.bucket === "done" ? previous.enteredAt : nowIso;
|
||||||
statusEnteredAt: null,
|
return {
|
||||||
recordUpdate: { bucket: "done", enteredAt: previous.enteredAt },
|
statusEnteredAt: enteredAt,
|
||||||
};
|
recordUpdate: { bucket: "done", enteredAt },
|
||||||
}
|
};
|
||||||
return { statusEnteredAt: null };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!previous) {
|
if (!previous) {
|
||||||
|
|||||||
Reference in New Issue
Block a user