Update workspace and app changes

This commit is contained in:
Mohamed Boudra
2026-03-07 19:09:00 +07:00
parent 1d1c7058f1
commit 7d76da3249
40 changed files with 2693 additions and 976 deletions
@@ -1,6 +1,7 @@
import { describe, expect, test, vi } from 'vitest'
import { Session } from './session.js'
import type { AgentSnapshotPayload } from '../shared/messages.js'
import { createPersistedProjectRecord, createPersistedWorkspaceRecord } from './workspace-registry.js'
function makeAgent(input: {
id: string
@@ -79,6 +80,24 @@ function createSessionForWorkspaceTests(): Session {
list: async () => [],
get: async () => null,
} as any,
projectRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
workspaceRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
createAgentMcpTransport: async () => {
throw new Error('not used')
},
@@ -91,6 +110,17 @@ function createSessionForWorkspaceTests(): Session {
describe('workspace aggregation', () => {
test('non-git workspace uses deterministic directory name and no unknown branch fallback', async () => {
const session = createSessionForWorkspaceTests() as any
session.workspaceRegistry.list = async () => [
createPersistedWorkspaceRecord({
workspaceId: '/tmp/non-git',
projectId: '/tmp/non-git',
cwd: '/tmp/non-git',
kind: 'directory',
displayName: 'non-git',
createdAt: '2026-03-01T12:00:00.000Z',
updatedAt: '2026-03-01T12:00:00.000Z',
}),
]
session.listAgentPayloads = async () => [
makeAgent({
id: 'a1',
@@ -99,19 +129,6 @@ describe('workspace aggregation', () => {
updatedAt: '2026-03-01T12:00:00.000Z',
}),
]
session.buildProjectPlacement = async (cwd: string) => ({
projectKey: cwd,
projectName: 'non-git',
checkout: {
cwd,
isGit: false,
currentBranch: null,
remoteUrl: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
},
})
const result = await session.listFetchWorkspacesEntries({
type: 'fetch_workspaces_request',
requestId: 'req-1',
@@ -124,6 +141,17 @@ describe('workspace aggregation', () => {
test('git branch workspace uses branch as canonical name', async () => {
const session = createSessionForWorkspaceTests() as any
session.workspaceRegistry.list = async () => [
createPersistedWorkspaceRecord({
workspaceId: '/tmp/repo-branch',
projectId: '/tmp/repo-branch',
cwd: '/tmp/repo-branch',
kind: 'local_checkout',
displayName: 'feature/name-from-server',
createdAt: '2026-03-01T12:00:00.000Z',
updatedAt: '2026-03-01T12:00:00.000Z',
}),
]
session.listAgentPayloads = async () => [
makeAgent({
id: 'a1',
@@ -144,7 +172,6 @@ describe('workspace aggregation', () => {
mainRepoRoot: null,
},
})
const result = await session.listFetchWorkspacesEntries({
type: 'fetch_workspaces_request',
requestId: 'req-branch',
@@ -156,6 +183,17 @@ describe('workspace aggregation', () => {
test('branch/detached policies and dominant status bucket are deterministic', async () => {
const session = createSessionForWorkspaceTests() as any
session.workspaceRegistry.list = async () => [
createPersistedWorkspaceRecord({
workspaceId: '/tmp/repo',
projectId: '/tmp/repo',
cwd: '/tmp/repo',
kind: 'local_checkout',
displayName: 'repo',
createdAt: '2026-03-01T12:00:00.000Z',
updatedAt: '2026-03-01T12:00:00.000Z',
}),
]
session.listAgentPayloads = async () => [
makeAgent({
id: 'a1',
@@ -177,19 +215,6 @@ describe('workspace aggregation', () => {
pendingPermissions: 1,
}),
]
session.buildProjectPlacement = async (cwd: string) => ({
projectKey: cwd,
projectName: 'repo',
checkout: {
cwd,
isGit: true,
currentBranch: 'HEAD',
remoteUrl: 'https://github.com/acme/repo.git',
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
},
})
const result = await session.listFetchWorkspacesEntries({
type: 'fetch_workspaces_request',
requestId: 'req-2',
@@ -200,7 +225,7 @@ describe('workspace aggregation', () => {
expect(result.entries[0]?.status).toBe('needs_input')
})
test('workspace update stream emits upsert and remove on lifecycle changes', async () => {
test('workspace update stream keeps persisted workspace visible after agents stop', async () => {
const emitted: Array<{ type: string; payload: unknown }> = []
const logger = {
child: () => logger,
@@ -227,6 +252,24 @@ describe('workspace aggregation', () => {
list: async () => [],
get: async () => null,
} as any,
projectRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
workspaceRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
createAgentMcpTransport: async () => {
throw new Error('not used')
},
@@ -246,6 +289,10 @@ describe('workspace aggregation', () => {
{
id: '/tmp/repo',
projectId: '/tmp/repo',
projectDisplayName: 'repo',
projectRootPath: '/tmp/repo',
projectKind: 'non_git',
workspaceKind: 'directory',
name: 'repo',
status: 'running',
activityAt: '2026-03-01T12:00:00.000Z',
@@ -253,15 +300,37 @@ describe('workspace aggregation', () => {
]
await session.emitWorkspaceUpdateForCwd('/tmp/repo')
session.listWorkspaceDescriptors = async () => []
session.listWorkspaceDescriptors = async () => [
{
id: '/tmp/repo',
projectId: '/tmp/repo',
projectDisplayName: 'repo',
projectRootPath: '/tmp/repo',
projectKind: 'non_git',
workspaceKind: 'directory',
name: 'repo',
status: 'done',
activityAt: null,
},
]
await session.emitWorkspaceUpdateForCwd('/tmp/repo')
const workspaceUpdates = emitted.filter((message) => message.type === 'workspace_update')
expect(workspaceUpdates).toHaveLength(2)
expect((workspaceUpdates[0] as any).payload.kind).toBe('upsert')
expect((workspaceUpdates[1] as any).payload).toEqual({
kind: 'remove',
id: '/tmp/repo',
kind: 'upsert',
workspace: {
id: '/tmp/repo',
projectId: '/tmp/repo',
projectDisplayName: 'repo',
projectRootPath: '/tmp/repo',
projectKind: 'non_git',
workspaceKind: 'directory',
name: 'repo',
status: 'done',
activityAt: null,
},
})
})
@@ -289,4 +358,80 @@ describe('workspace aggregation', () => {
expect(emitWorkspaceUpdateForCwd).toHaveBeenNthCalledWith(1, '/tmp/repo')
expect(emitWorkspaceUpdateForCwd).toHaveBeenNthCalledWith(2, '/tmp/repo/sub')
})
test('open_project_request registers a workspace before any agent exists', async () => {
const emitted: Array<{ type: string; payload: unknown }> = []
const session = createSessionForWorkspaceTests() as any
const projects = new Map<string, ReturnType<typeof createPersistedProjectRecord>>()
const workspaces = new Map<string, ReturnType<typeof createPersistedWorkspaceRecord>>()
session.emit = (message: any) => 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 (workspaceId: string) => workspaces.get(workspaceId) ?? 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())
session.buildProjectPlacement = async (cwd: string) => ({
projectKey: cwd,
projectName: 'repo',
checkout: {
cwd,
isGit: false,
currentBranch: null,
remoteUrl: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
},
})
await session.handleMessage({
type: 'open_project_request',
cwd: '/tmp/repo',
requestId: 'req-open',
})
expect(workspaces.get('/tmp/repo')).toBeTruthy()
const response = emitted.find((message) => message.type === 'open_project_response') as any
expect(response?.payload.error).toBeNull()
expect(response?.payload.workspace?.id).toBe('/tmp/repo')
})
test('archive_workspace_request hides non-destructive workspace records', async () => {
const emitted: Array<{ type: string; payload: unknown }> = []
const session = createSessionForWorkspaceTests() as any
const workspace = createPersistedWorkspaceRecord({
workspaceId: '/tmp/repo',
projectId: '/tmp/repo',
cwd: '/tmp/repo',
kind: 'directory',
displayName: 'repo',
createdAt: '2026-03-01T12:00:00.000Z',
updatedAt: '2026-03-01T12:00:00.000Z',
})
session.emit = (message: any) => emitted.push(message)
session.workspaceRegistry.get = async () => workspace
session.workspaceRegistry.archive = async (_workspaceId: string, archivedAt: string) => {
workspace.archivedAt = archivedAt
}
session.workspaceRegistry.list = async () => [workspace]
session.projectRegistry.archive = async () => {}
await session.handleMessage({
type: 'archive_workspace_request',
workspaceId: '/tmp/repo',
requestId: 'req-archive',
})
expect(workspace.archivedAt).toBeTruthy()
const response = emitted.find((message) => message.type === 'archive_workspace_response') as any
expect(response?.payload.error).toBeNull()
})
})