feat: implement full-stack TaskPilot scheduler
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { Task, TaskListResponse } from './api/client'
|
||||
import App from './App'
|
||||
|
||||
const task: Task = {
|
||||
id: 'echo_task',
|
||||
name: 'Echo task',
|
||||
cron: '0 0 2 * * *',
|
||||
timezone: 'Asia/Shanghai',
|
||||
enabled: true,
|
||||
timeout_secs: 300,
|
||||
concurrency_policy: 'skip',
|
||||
retry_policy: {
|
||||
max_attempts: 0,
|
||||
delay_secs: 0,
|
||||
},
|
||||
action: {
|
||||
type: 'script',
|
||||
working_dir: 'D:\\Projects\\echo',
|
||||
interpreter: 'python.exe',
|
||||
script: 'main.py',
|
||||
args: [],
|
||||
env: {},
|
||||
},
|
||||
created_at: '2026-10-18T10:00:00Z',
|
||||
updated_at: '2026-10-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const taskList: TaskListResponse = {
|
||||
items: [task],
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
}
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
function createFetchMock() {
|
||||
return vi.fn(
|
||||
async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url =
|
||||
typeof input === 'string'
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.toString()
|
||||
: input.url
|
||||
|
||||
if (url.endsWith('/api/v1/health')) {
|
||||
return jsonResponse({
|
||||
status: 'ok',
|
||||
service: 'task-pilot-backend',
|
||||
version: '0.1.0',
|
||||
})
|
||||
}
|
||||
if (url.endsWith('/api/v1/session')) {
|
||||
return jsonResponse({ token: 'test-token' })
|
||||
}
|
||||
if (url.endsWith('/api/v1/cron/preview')) {
|
||||
return jsonResponse({
|
||||
timezone: 'Asia/Shanghai',
|
||||
runs: [
|
||||
'2026-10-19 02:00:00',
|
||||
'2026-10-20 02:00:00',
|
||||
],
|
||||
})
|
||||
}
|
||||
if (url.includes('/api/v1/tasks') && init?.method === 'POST') {
|
||||
return jsonResponse(
|
||||
{
|
||||
...task,
|
||||
...JSON.parse(String(init.body)),
|
||||
},
|
||||
201,
|
||||
)
|
||||
}
|
||||
if (url.includes('/api/v1/tasks')) {
|
||||
return jsonResponse(taskList)
|
||||
}
|
||||
return jsonResponse({ code: 'NOT_FOUND', message: 'not found', details: {} }, 404)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function renderApp() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<App />
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('TaskDashboard', () => {
|
||||
it('renders tasks returned by the Rust API', async () => {
|
||||
vi.stubGlobal('fetch', createFetchMock())
|
||||
renderApp()
|
||||
|
||||
expect(await screen.findByText('Echo task')).toBeInTheDocument()
|
||||
expect(screen.getByText('echo_task')).toBeInTheDocument()
|
||||
expect(screen.getByText('0 0 2 * * *')).toBeInTheDocument()
|
||||
expect(await screen.findByText('在线')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('creates a script task with the visible form values', async () => {
|
||||
const fetchMock = createFetchMock()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const user = userEvent.setup()
|
||||
renderApp()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '+ 新建任务' }))
|
||||
await user.clear(screen.getByLabelText('任务标识'))
|
||||
await user.type(screen.getByLabelText('任务标识'), 'new_task')
|
||||
await user.type(screen.getByLabelText('任务名称'), 'New task')
|
||||
await user.type(screen.getByLabelText('工作目录'), 'D:\\Projects\\new')
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: '保存并热刷新' }),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const createCall = fetchMock.mock.calls.find(
|
||||
([input, init]) =>
|
||||
String(input).endsWith('/api/v1/tasks') && init?.method === 'POST',
|
||||
)
|
||||
expect(createCall).toBeDefined()
|
||||
})
|
||||
|
||||
const createCall = fetchMock.mock.calls.find(
|
||||
([input, init]) =>
|
||||
String(input).endsWith('/api/v1/tasks') && init?.method === 'POST',
|
||||
)
|
||||
const payload = JSON.parse(String(createCall?.[1]?.body))
|
||||
expect(payload.id).toBe('new_task')
|
||||
expect(payload.name).toBe('New task')
|
||||
expect(payload.action.type).toBe('script')
|
||||
expect(payload.action.working_dir).toBe('D:\\Projects\\new')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user