initialize Electron desktop app and remove unused files
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
|
||||
const mockGetTask = vi.hoisted(() => vi.fn())
|
||||
const mockRequest = vi.hoisted(() => vi.fn())
|
||||
const mockCompleteTasks = vi.hoisted(() => vi.fn())
|
||||
const mockBlockTask = vi.hoisted(() => vi.fn())
|
||||
const mockUnblockTasks = vi.hoisted(() => vi.fn())
|
||||
const mockAssignTask = vi.hoisted(() => vi.fn())
|
||||
const mockAddComment = vi.hoisted(() => vi.fn())
|
||||
const mockGetTaskLog = vi.hoisted(() => vi.fn())
|
||||
const mockGetDiagnostics = vi.hoisted(() => vi.fn())
|
||||
const mockReclaimTask = vi.hoisted(() => vi.fn())
|
||||
const mockReassignTask = vi.hoisted(() => vi.fn())
|
||||
const mockSpecifyTask = vi.hoisted(() => vi.fn())
|
||||
const mockRouterPush = vi.hoisted(() => vi.fn())
|
||||
const mockUseMessage = vi.hoisted(() => vi.fn(() => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
})))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockRouterPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/client', () => ({
|
||||
request: mockRequest,
|
||||
}))
|
||||
|
||||
vi.mock('@/api/hermes/kanban', () => ({
|
||||
getTask: mockGetTask,
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/hermes/kanban', () => ({
|
||||
useKanbanStore: () => ({
|
||||
selectedBoard: 'project-a',
|
||||
assignees: [{ name: 'alice', counts: { todo: 1 } }, { name: 'bob', counts: { ready: 1 } }],
|
||||
completeTasks: mockCompleteTasks,
|
||||
blockTask: mockBlockTask,
|
||||
unblockTasks: mockUnblockTasks,
|
||||
assignTask: mockAssignTask,
|
||||
addComment: mockAddComment,
|
||||
getTaskLog: mockGetTaskLog,
|
||||
getDiagnostics: mockGetDiagnostics,
|
||||
reclaimTask: mockReclaimTask,
|
||||
reassignTask: mockReassignTask,
|
||||
specifyTask: mockSpecifyTask,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/hermes/chat/HistoryMessageList.vue', () => ({
|
||||
default: defineComponent({
|
||||
name: 'HistoryMessageList',
|
||||
props: { session: { type: Object, required: false } },
|
||||
template: '<div class="history-message-list-stub">{{ session ? session.id : "none" }}</div>',
|
||||
}),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Overlays teleport to `document.body` in jsdom, so the drawer and the messages
|
||||
* modal are stubbed to render inline. Stubs are keyed by Arco's *internal*
|
||||
* component name — not the `a-*` template tag — because Arco is registered
|
||||
* globally via `app.use(ArcoVue)`.
|
||||
*/
|
||||
const ADrawerStub = defineComponent({
|
||||
name: 'Drawer',
|
||||
props: { visible: { type: Boolean, required: false }, title: { type: String, required: false } },
|
||||
emits: ['cancel'],
|
||||
template: '<div v-if="visible" class="arco-drawer-stub" :data-title="title"><slot /></div>',
|
||||
})
|
||||
|
||||
const AModalStub = defineComponent({
|
||||
name: 'Modal',
|
||||
props: { visible: { type: Boolean, required: false }, title: { type: String, required: false } },
|
||||
emits: ['update:visible', 'close'],
|
||||
template: '<div v-if="visible" class="arco-modal-stub" :data-title="title"><slot /></div>',
|
||||
})
|
||||
|
||||
/** Arco's a-select has no native <select>, so keep one to drive option values. */
|
||||
const ASelectStub = defineComponent({
|
||||
name: 'Select',
|
||||
props: { modelValue: { required: false }, options: { type: Array, default: () => [] }, placeholder: { type: String, required: false } },
|
||||
emits: ['update:modelValue', 'change'],
|
||||
template: `<select class="arco-select-stub" :value="modelValue" @change="onChange">
|
||||
<option value=""></option>
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||
</select>`,
|
||||
methods: {
|
||||
onChange(event: Event) {
|
||||
const value = (event.target as HTMLSelectElement).value || null
|
||||
this.$emit('update:modelValue', value)
|
||||
this.$emit('change', value)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const arcoGlobal = {
|
||||
plugins: [ArcoVue],
|
||||
stubs: { Drawer: ADrawerStub, Modal: AModalStub, Select: ASelectStub },
|
||||
}
|
||||
|
||||
/** Toolbar buttons are located by their label, never by DOM order. */
|
||||
function buttonByText(wrapper: any, text: string) {
|
||||
const button = wrapper.findAll('button').find((node: any) => node.text().trim() === text)
|
||||
if (!button) throw new Error(`button not found: ${text}`)
|
||||
return button
|
||||
}
|
||||
|
||||
/** Text inputs are located by the placeholder the view renders for them. */
|
||||
function inputByPlaceholder(wrapper: any, placeholder: string) {
|
||||
return wrapper.get(`input[placeholder="${placeholder}"]`)
|
||||
}
|
||||
|
||||
vi.mock('@/composables/useMessage', () => ({
|
||||
useMessage: () => mockUseMessage(),
|
||||
}))
|
||||
|
||||
import KanbanTaskDrawer from '@/components/hermes/kanban/KanbanTaskDrawer.vue'
|
||||
|
||||
describe('KanbanTaskDrawer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockRequest.mockResolvedValue({ results: [] })
|
||||
mockCompleteTasks.mockResolvedValue(undefined)
|
||||
mockBlockTask.mockResolvedValue(undefined)
|
||||
mockUnblockTasks.mockResolvedValue(undefined)
|
||||
mockAssignTask.mockResolvedValue(undefined)
|
||||
mockAddComment.mockResolvedValue({ ok: true })
|
||||
mockGetTaskLog.mockResolvedValue({ task_id: 'task-1', path: null, exists: true, size_bytes: 10, content: 'worker log', truncated: false })
|
||||
mockGetDiagnostics.mockResolvedValue([{ task_id: 'task-1' }])
|
||||
mockReclaimTask.mockResolvedValue({ ok: true })
|
||||
mockReassignTask.mockResolvedValue({ ok: true })
|
||||
mockSpecifyTask.mockResolvedValue([{ task_id: 'task-1' }])
|
||||
mockGetTask.mockResolvedValue({
|
||||
task: {
|
||||
id: 'task-1',
|
||||
title: 'Ship kanban',
|
||||
body: 'Implement feature',
|
||||
assignee: 'alice',
|
||||
status: 'done',
|
||||
priority: 2,
|
||||
created_at: 100,
|
||||
started_at: 110,
|
||||
completed_at: 120,
|
||||
tenant: null,
|
||||
result: 'Done summary',
|
||||
},
|
||||
latest_summary: 'Done summary',
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [{ id: 1, profile: 'alice', status: 'done', started_at: 110, ended_at: 120 }],
|
||||
session: {
|
||||
id: 'session-1',
|
||||
title: 'Hermes session',
|
||||
source: 'codex',
|
||||
model: 'gpt-5.5',
|
||||
started_at: 110,
|
||||
ended_at: 120,
|
||||
messages: [
|
||||
{ id: 'm1', role: 'user', content: 'hello', timestamp: 111 },
|
||||
{ id: 'm2', role: 'assistant', content: 'world', timestamp: 112 },
|
||||
{ id: 'm3', role: 'tool', content: 'ignore', timestamp: 113 },
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('renders completed-result messages through HistoryMessageList', async () => {
|
||||
const wrapper = mount(KanbanTaskDrawer, {
|
||||
props: { taskId: 'task-1' },
|
||||
global: arcoGlobal,
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('.result-summary').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const modal = wrapper.find('.arco-modal-stub')
|
||||
expect(modal.exists()).toBe(true)
|
||||
expect(modal.attributes('data-title')).toBe('Ship kanban')
|
||||
|
||||
const history = wrapper.find('.history-message-list-stub')
|
||||
expect(history.exists()).toBe(true)
|
||||
expect(history.text()).toBe('session-1')
|
||||
|
||||
const sessionProp = wrapper.getComponent({ name: 'HistoryMessageList' }).props('session') as any
|
||||
expect(sessionProp.messages).toEqual([
|
||||
{ id: 'm1', role: 'user', content: 'hello', timestamp: 111 },
|
||||
{ id: 'm2', role: 'assistant', content: 'world', timestamp: 112 },
|
||||
])
|
||||
})
|
||||
|
||||
it('uses the latest run profile when searching related sessions', async () => {
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-2',
|
||||
title: 'Retry task',
|
||||
body: null,
|
||||
assignee: 'bob',
|
||||
status: 'running',
|
||||
priority: 2,
|
||||
created_at: 100,
|
||||
started_at: 110,
|
||||
completed_at: null,
|
||||
tenant: null,
|
||||
result: null,
|
||||
},
|
||||
latest_summary: null,
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [
|
||||
{ id: 1, profile: 'stale', status: 'failed', started_at: 110, ended_at: 120 },
|
||||
{ id: 2, profile: 'fresh', status: 'running', started_at: 130, ended_at: null },
|
||||
],
|
||||
})
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
results: [{ id: 'session-2', title: 'Found session', source: 'codex', model: 'gpt-5.5', started_at: 130 }],
|
||||
})
|
||||
|
||||
const wrapper = mount(KanbanTaskDrawer, { props: { taskId: 'task-2' }, global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
const sessionsTitle = wrapper.findAll('.section-title').find(node => node.text() === 'kanban.detail.sessions')
|
||||
await sessionsTitle?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith('/api/hermes/kanban/search-sessions?task_id=task-2&profile=fresh&board=project-a')
|
||||
await wrapper.find('.session-item').trigger('click')
|
||||
expect(mockRouterPush).toHaveBeenCalledWith({ name: 'hermes.chat', query: { session: 'session-2' } })
|
||||
})
|
||||
|
||||
it('renders task run history through a paged window', async () => {
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-many-runs',
|
||||
title: 'Many runs',
|
||||
body: null,
|
||||
assignee: 'alice',
|
||||
status: 'done',
|
||||
priority: 2,
|
||||
created_at: 100,
|
||||
started_at: 110,
|
||||
completed_at: 220,
|
||||
tenant: null,
|
||||
result: null,
|
||||
},
|
||||
latest_summary: null,
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: Array.from({ length: 25 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
task_id: 'task-many-runs',
|
||||
profile: `runner-${String(index + 1).padStart(2, '0')}`,
|
||||
status: 'done',
|
||||
outcome: null,
|
||||
summary: `summary-${index + 1}`,
|
||||
error: null,
|
||||
metadata: null,
|
||||
worker_pid: null,
|
||||
started_at: 110 + index,
|
||||
ended_at: 120 + index,
|
||||
})),
|
||||
})
|
||||
|
||||
const wrapper = mount(KanbanTaskDrawer, { props: { taskId: 'task-many-runs' }, global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.run-item')).toHaveLength(10)
|
||||
expect(wrapper.text()).toContain('1-10 / 25')
|
||||
expect(wrapper.text()).toContain('runner-01')
|
||||
expect(wrapper.text()).toContain('runner-10')
|
||||
expect(wrapper.text()).not.toContain('runner-11')
|
||||
|
||||
await buttonByText(wrapper, '›').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.run-item')).toHaveLength(10)
|
||||
expect(wrapper.text()).toContain('11-20 / 25')
|
||||
expect(wrapper.text()).not.toContain('runner-10')
|
||||
expect(wrapper.text()).toContain('runner-11')
|
||||
expect(wrapper.text()).toContain('runner-20')
|
||||
})
|
||||
|
||||
it('clears task-scoped operation output when switching tasks', async () => {
|
||||
const wrapper = mount(KanbanTaskDrawer, { props: { taskId: 'task-1' }, global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
await buttonByText(wrapper, 'kanban.action.loadLog').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('worker log')
|
||||
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-2',
|
||||
title: 'Next task',
|
||||
body: null,
|
||||
assignee: 'bob',
|
||||
status: 'todo',
|
||||
priority: 1,
|
||||
created_at: 200,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
tenant: null,
|
||||
result: null,
|
||||
},
|
||||
latest_summary: null,
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [],
|
||||
})
|
||||
await wrapper.setProps({ taskId: 'task-2' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).not.toContain('worker log')
|
||||
})
|
||||
|
||||
it('does not expose mutation actions for archived tasks', async () => {
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-archived',
|
||||
title: 'Archived task',
|
||||
body: null,
|
||||
assignee: 'alice',
|
||||
status: 'archived',
|
||||
priority: 1,
|
||||
created_at: 100,
|
||||
started_at: 110,
|
||||
completed_at: 120,
|
||||
tenant: null,
|
||||
result: 'Archived summary',
|
||||
},
|
||||
latest_summary: 'Archived summary',
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [],
|
||||
})
|
||||
|
||||
const wrapper = mount(KanbanTaskDrawer, { props: { taskId: 'task-archived' }, global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).not.toContain('kanban.action.complete')
|
||||
expect(wrapper.text()).not.toContain('kanban.action.block')
|
||||
expect(wrapper.text()).not.toContain('kanban.action.assign')
|
||||
})
|
||||
|
||||
it('executes complete, block, unblock, and assign actions', async () => {
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-0',
|
||||
title: 'Todo task',
|
||||
body: null,
|
||||
assignee: null,
|
||||
status: 'todo',
|
||||
priority: 1,
|
||||
created_at: 100,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
tenant: null,
|
||||
result: null,
|
||||
},
|
||||
latest_summary: null,
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [],
|
||||
})
|
||||
const wrapper = mount(KanbanTaskDrawer, {
|
||||
props: { taskId: 'task-0' },
|
||||
global: arcoGlobal,
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
await buttonByText(wrapper, 'kanban.action.complete').trigger('click')
|
||||
await inputByPlaceholder(wrapper, 'kanban.action.completeSummary').setValue('done summary')
|
||||
await buttonByText(wrapper, 'common.ok').trigger('click')
|
||||
await flushPromises()
|
||||
expect(mockCompleteTasks).toHaveBeenCalledWith(['task-0'], 'done summary')
|
||||
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-3',
|
||||
title: 'Blocked task',
|
||||
body: null,
|
||||
assignee: 'alice',
|
||||
status: 'blocked',
|
||||
priority: 1,
|
||||
created_at: 100,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
tenant: null,
|
||||
result: null,
|
||||
},
|
||||
latest_summary: null,
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [],
|
||||
})
|
||||
await wrapper.setProps({ taskId: 'task-3' })
|
||||
await flushPromises()
|
||||
await buttonByText(wrapper, 'kanban.action.unblock').trigger('click')
|
||||
expect(mockUnblockTasks).toHaveBeenCalledWith(['task-3'])
|
||||
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-4',
|
||||
title: 'Todo task',
|
||||
body: null,
|
||||
assignee: null,
|
||||
status: 'todo',
|
||||
priority: 1,
|
||||
created_at: 100,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
tenant: null,
|
||||
result: null,
|
||||
},
|
||||
latest_summary: null,
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [],
|
||||
})
|
||||
mockGetTask.mockResolvedValueOnce({
|
||||
task: {
|
||||
id: 'task-4',
|
||||
title: 'Todo task',
|
||||
body: null,
|
||||
assignee: 'bob',
|
||||
status: 'todo',
|
||||
priority: 1,
|
||||
created_at: 100,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
tenant: null,
|
||||
result: null,
|
||||
},
|
||||
latest_summary: null,
|
||||
comments: [],
|
||||
events: [],
|
||||
runs: [],
|
||||
})
|
||||
await wrapper.setProps({ taskId: 'task-4' })
|
||||
await flushPromises()
|
||||
await buttonByText(wrapper, 'kanban.action.block').trigger('click')
|
||||
await inputByPlaceholder(wrapper, 'kanban.action.blockReason').setValue('waiting dependency')
|
||||
await buttonByText(wrapper, 'common.ok').trigger('click')
|
||||
expect(mockBlockTask).toHaveBeenCalledWith('task-4', 'waiting dependency')
|
||||
|
||||
const select = wrapper.get('select.arco-select-stub')
|
||||
await select.setValue('bob')
|
||||
await buttonByText(wrapper, 'kanban.action.assign').trigger('click')
|
||||
await flushPromises()
|
||||
expect(mockAssignTask).toHaveBeenCalledWith('task-4', 'bob')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user