Update workspace and app changes
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user