mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix(release): restore quarantine quality gates
Separate quarantine review enforcement from server and coverage file selection so expired review dates fail only the governance lane. Refresh stale server quarantine suites and keep only the live provider test quarantined for non-live PR gates. Tested: bun run check:policy Tested: bun run check:server Tested: bun run check:coverage Tested: bun run verify Tested: git diff --check Confidence: high Scope-risk: moderate
This commit is contained in:
parent
9322d9aba3
commit
aed3e7d038
@ -58,7 +58,7 @@ describe('release desktop workflow', () => {
|
||||
expect(workflow.indexOf('Verify macOS launch policy')).toBeLessThan(workflow.indexOf('Upload release artifacts for final publish'))
|
||||
})
|
||||
|
||||
test('release workflow fails globally before matrix fan-out when signing or notarization secrets are missing', () => {
|
||||
test('release workflow blocks on macOS signing/notarization secrets and warns for Windows signing', () => {
|
||||
const workflow = readReleaseWorkflow()
|
||||
const signingJob = workflow.match(
|
||||
/signing-preflight:[\s\S]*?(?:\n {2}[a-zA-Z0-9_-]+:|$)/,
|
||||
@ -72,12 +72,28 @@ describe('release desktop workflow', () => {
|
||||
'APPLE_ID',
|
||||
'APPLE_APP_SPECIFIC_PASSWORD',
|
||||
'APPLE_TEAM_ID',
|
||||
]) {
|
||||
expect(signingJob).toContain(secret)
|
||||
}
|
||||
for (const secret of [
|
||||
'WINDOWS_CERTIFICATE',
|
||||
'WINDOWS_CERTIFICATE_PASSWORD',
|
||||
]) {
|
||||
expect(signingJob).toContain(secret)
|
||||
}
|
||||
expect(signingJob).toContain('Missing required release signing/notarization secrets')
|
||||
expect(signingJob).toContain('Missing required macOS signing/notarization secrets')
|
||||
expect(signingJob).toContain('Windows signing secrets missing')
|
||||
expect(signingJob).toContain('::warning::Windows signing secrets missing')
|
||||
|
||||
const macRequiredBlock = signingJob?.match(
|
||||
/missing=\(\)[\s\S]*?# Windows signing is optional:/,
|
||||
)?.[0]
|
||||
const windowsOptionalBlock = signingJob?.match(
|
||||
/win_missing=\(\)[\s\S]*?fi\n/,
|
||||
)?.[0]
|
||||
expect(macRequiredBlock).toContain('exit 1')
|
||||
expect(windowsOptionalBlock).toContain('::warning::')
|
||||
expect(windowsOptionalBlock).not.toContain('exit 1')
|
||||
expect(buildJob).toContain('- quality-preflight')
|
||||
expect(buildJob).toContain('- signing-preflight')
|
||||
expect(workflow.indexOf('signing-preflight:')).toBeLessThan(workflow.indexOf('build:'))
|
||||
|
||||
@ -6,7 +6,7 @@ import { loadQuarantineManifest, quarantinedPathSet } from '../quality-gate/quar
|
||||
|
||||
const root = process.cwd()
|
||||
const roots = ['src/server', 'src/tools', 'src/utils']
|
||||
const excludedFiles = quarantinedPathSet(loadQuarantineManifest(undefined, { enforceReviewDate: true }))
|
||||
const excludedFiles = quarantinedPathSet(loadQuarantineManifest())
|
||||
|
||||
function normalize(path: string) {
|
||||
return relative(root, path).split(sep).join('/')
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
collectServerTestFiles,
|
||||
evaluateChangedLineCoverage,
|
||||
evaluateThresholds,
|
||||
parseChangedLinesFromDiff,
|
||||
@ -203,6 +207,34 @@ describe('coverage gate helpers', () => {
|
||||
expect(failures).toEqual(['desktop: coverage command exited with 1'])
|
||||
})
|
||||
|
||||
test('collects non-quarantined server tests when review windows have expired', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cc-haha-coverage-'))
|
||||
try {
|
||||
mkdirSync(join(root, 'src/server/__tests__'), { recursive: true })
|
||||
mkdirSync(join(root, 'src/tools'), { recursive: true })
|
||||
mkdirSync(join(root, 'src/utils'), { recursive: true })
|
||||
writeFileSync(join(root, 'src/server/__tests__/active.test.ts'), '')
|
||||
writeFileSync(join(root, 'src/server/__tests__/quarantined.test.ts'), '')
|
||||
|
||||
const files = collectServerTestFiles(root, {
|
||||
quarantined: [
|
||||
{
|
||||
id: 'server:expired',
|
||||
path: 'src/server/__tests__/quarantined.test.ts',
|
||||
reason: 'Known instability under review.',
|
||||
owner: 'maintainers',
|
||||
reviewAfter: '2026-01-01',
|
||||
exitCriteria: 'Make deterministic or remove from quarantine.',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(files).toEqual(['src/server/__tests__/active.test.ts'])
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('does not require every suite to exist in the ratchet baseline', () => {
|
||||
const failures = evaluateThresholds([
|
||||
{
|
||||
|
||||
@ -235,8 +235,8 @@ function walkTestFiles(path: string, files: string[], excluded: Set<string>, roo
|
||||
}
|
||||
}
|
||||
|
||||
function collectServerTestFiles(rootDir = ROOT_DIR) {
|
||||
const excluded = quarantinedPathSet(loadQuarantineManifest(undefined, { enforceReviewDate: true }))
|
||||
export function collectServerTestFiles(rootDir = ROOT_DIR, quarantineManifest = loadQuarantineManifest()) {
|
||||
const excluded = quarantinedPathSet(quarantineManifest)
|
||||
const files: string[] = []
|
||||
for (const root of ['src/server', 'src/tools', 'src/utils']) {
|
||||
walkTestFiles(join(rootDir, root), files, excluded, rootDir)
|
||||
|
||||
@ -1,44 +1,12 @@
|
||||
{
|
||||
"quarantined": [
|
||||
{
|
||||
"id": "server:cron-scheduler",
|
||||
"path": "src/server/__tests__/cron-scheduler.test.ts",
|
||||
"reason": "Known unstable scheduler timing assertions; keep out of default server gate until fixed.",
|
||||
"owner": "maintainers",
|
||||
"reviewAfter": "2026-06-01",
|
||||
"exitCriteria": "Replace timing-sensitive assertions with deterministic scheduler controls and prove the test passes in check:server for five consecutive local/CI runs."
|
||||
},
|
||||
{
|
||||
"id": "server:providers-real",
|
||||
"path": "src/server/__tests__/providers-real.test.ts",
|
||||
"reason": "Live provider coverage belongs in maintainer baseline or release mode, not the default PR gate.",
|
||||
"reason": "Reviewed 2026-06-03: suite still performs a live MiniMax connectivity request with a fake key. Default server/PR gates must stay non-live and should not depend on third-party network responses.",
|
||||
"owner": "maintainers",
|
||||
"reviewAfter": "2026-06-01",
|
||||
"exitCriteria": "Move live behavior into provider-smoke lanes and keep only non-live provider contract tests in check:server."
|
||||
},
|
||||
{
|
||||
"id": "server:tasks",
|
||||
"path": "src/server/__tests__/tasks.test.ts",
|
||||
"reason": "Known default-gate instability; keep quarantined until task timing is hardened.",
|
||||
"owner": "maintainers",
|
||||
"reviewAfter": "2026-06-01",
|
||||
"exitCriteria": "Harden task timing with fake timers or event-based waits and prove the suite passes in check:server for five consecutive local/CI runs."
|
||||
},
|
||||
{
|
||||
"id": "server:e2e:business-flow",
|
||||
"path": "src/server/__tests__/e2e/business-flow.test.ts",
|
||||
"reason": "Broad e2e flow is not deterministic enough for default PR checks.",
|
||||
"owner": "maintainers",
|
||||
"reviewAfter": "2026-06-01",
|
||||
"exitCriteria": "Split into deterministic contract tests or move to an explicit smoke lane with bounded fixtures, logs, and timeout policy."
|
||||
},
|
||||
{
|
||||
"id": "server:e2e:full-flow",
|
||||
"path": "src/server/__tests__/e2e/full-flow.test.ts",
|
||||
"reason": "Broad e2e flow is not deterministic enough for default PR checks.",
|
||||
"owner": "maintainers",
|
||||
"reviewAfter": "2026-06-01",
|
||||
"exitCriteria": "Split into deterministic contract tests or move to an explicit smoke lane with bounded fixtures, logs, and timeout policy."
|
||||
"reviewAfter": "2026-06-17",
|
||||
"exitCriteria": "Move the live connectivity assertion into provider-smoke/release-live lanes, or replace it with a deterministic fetch mock so only non-live provider contract coverage remains in check:server."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ import { CronService, type CronTask } from '../services/cronService.js'
|
||||
|
||||
let tmpDir: string
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const originalClaudeCliPath = process.env.CLAUDE_CLI_PATH
|
||||
const originalDisableTerminalShellEnv = process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
|
||||
|
||||
async function createTmpDir(): Promise<string> {
|
||||
const dir = path.join(
|
||||
@ -36,6 +38,19 @@ async function cleanupTmpDir(dir: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function createFakeCronCli(dir: string): Promise<string> {
|
||||
const cliPath = path.join(dir, 'fake-cron-cli.ts')
|
||||
await fs.writeFile(
|
||||
cliPath,
|
||||
[
|
||||
"console.log(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'fake cron output' }] } }))",
|
||||
"console.log(JSON.stringify({ type: 'result', result: 'fake cron result' }))",
|
||||
].join('\n') + '\n',
|
||||
'utf-8',
|
||||
)
|
||||
return cliPath
|
||||
}
|
||||
|
||||
// ─── fieldMatches tests ────────────────────────────────────────────────────
|
||||
|
||||
describe('fieldMatches', () => {
|
||||
@ -162,6 +177,8 @@ describe('CronScheduler', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await createTmpDir()
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||
process.env.CLAUDE_CLI_PATH = await createFakeCronCli(tmpDir)
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = '1'
|
||||
cronService = new CronService()
|
||||
scheduler = new CronScheduler(cronService)
|
||||
})
|
||||
@ -173,6 +190,16 @@ describe('CronScheduler', () => {
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
}
|
||||
if (originalClaudeCliPath) {
|
||||
process.env.CLAUDE_CLI_PATH = originalClaudeCliPath
|
||||
} else {
|
||||
delete process.env.CLAUDE_CLI_PATH
|
||||
}
|
||||
if (originalDisableTerminalShellEnv) {
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = originalDisableTerminalShellEnv
|
||||
} else {
|
||||
delete process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
|
||||
}
|
||||
await cleanupTmpDir(tmpDir)
|
||||
})
|
||||
|
||||
@ -374,6 +401,8 @@ describe('Execution log trimming', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await createTmpDir()
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||
process.env.CLAUDE_CLI_PATH = await createFakeCronCli(tmpDir)
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = '1'
|
||||
cronService = new CronService()
|
||||
scheduler = new CronScheduler(cronService)
|
||||
})
|
||||
@ -385,6 +414,16 @@ describe('Execution log trimming', () => {
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
}
|
||||
if (originalClaudeCliPath) {
|
||||
process.env.CLAUDE_CLI_PATH = originalClaudeCliPath
|
||||
} else {
|
||||
delete process.env.CLAUDE_CLI_PATH
|
||||
}
|
||||
if (originalDisableTerminalShellEnv) {
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = originalDisableTerminalShellEnv
|
||||
} else {
|
||||
delete process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
|
||||
}
|
||||
await cleanupTmpDir(tmpDir)
|
||||
})
|
||||
|
||||
|
||||
@ -9,15 +9,44 @@ import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
let server: ReturnType<typeof Bun.serve>
|
||||
let baseUrl: string
|
||||
let wsUrl: string
|
||||
let tmpDir: string
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const originalCliPath = process.env.CLAUDE_CLI_PATH
|
||||
const originalDisableTerminalShellEnv = process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
|
||||
const mockSdkCliPath = fileURLToPath(new URL('../fixtures/mock-sdk-cli.ts', import.meta.url))
|
||||
|
||||
function restoreEnv() {
|
||||
if (originalConfigDir !== undefined) {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
}
|
||||
if (originalCliPath !== undefined) {
|
||||
process.env.CLAUDE_CLI_PATH = originalCliPath
|
||||
} else {
|
||||
delete process.env.CLAUDE_CLI_PATH
|
||||
}
|
||||
if (originalDisableTerminalShellEnv !== undefined) {
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = originalDisableTerminalShellEnv
|
||||
} else {
|
||||
delete process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
|
||||
}
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
restoreEnv()
|
||||
})
|
||||
|
||||
async function startTestServer() {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-biz-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||
process.env.CLAUDE_CLI_PATH = mockSdkCliPath
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = '1'
|
||||
await fs.mkdir(path.join(tmpDir, 'projects'), { recursive: true })
|
||||
await fs.mkdir(path.join(tmpDir, 'agents'), { recursive: true })
|
||||
|
||||
@ -377,19 +406,18 @@ describe('Business Flow: Models & Effort', () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('should return 4 available models', async () => {
|
||||
it('should return available fallback models', async () => {
|
||||
const { data } = await api('GET', '/api/models')
|
||||
expect(data.models.length).toBe(4)
|
||||
expect(data.models.length).toBe(3)
|
||||
const names = data.models.map((m: any) => m.name)
|
||||
expect(names).toContain('Opus 4.7')
|
||||
expect(names).toContain('Opus 4.7 1M')
|
||||
expect(names).toContain('Sonnet 4.6')
|
||||
expect(names).toContain('Haiku 4.5')
|
||||
})
|
||||
|
||||
it('should default to Sonnet model', async () => {
|
||||
it('should default to Opus model', async () => {
|
||||
const { data } = await api('GET', '/api/models/current')
|
||||
expect(data.model.id).toBe('claude-sonnet-4-6')
|
||||
expect(data.model.id).toBe('claude-opus-4-7')
|
||||
})
|
||||
|
||||
it('should switch to Opus 4.7', async () => {
|
||||
@ -468,8 +496,10 @@ describe('Business Flow: Sessions & CLI Interop', () => {
|
||||
let sessionId: string
|
||||
|
||||
it('should create a session', async () => {
|
||||
const workDir = path.join(tmpDir, 'my-project')
|
||||
await fs.mkdir(workDir, { recursive: true })
|
||||
const { status, data } = await api('POST', '/api/sessions', {
|
||||
workDir: '/Users/dev/my-project',
|
||||
workDir,
|
||||
})
|
||||
expect(status).toBe(201)
|
||||
expect(data.sessionId).toMatch(/^[0-9a-f-]{36}$/)
|
||||
|
||||
@ -8,15 +8,44 @@ import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
let server: ReturnType<typeof Bun.serve>
|
||||
let baseUrl: string
|
||||
let tmpDir: string
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const originalCliPath = process.env.CLAUDE_CLI_PATH
|
||||
const originalDisableTerminalShellEnv = process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
|
||||
const mockSdkCliPath = fileURLToPath(new URL('../fixtures/mock-sdk-cli.ts', import.meta.url))
|
||||
|
||||
function restoreEnv() {
|
||||
if (originalConfigDir !== undefined) {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
}
|
||||
if (originalCliPath !== undefined) {
|
||||
process.env.CLAUDE_CLI_PATH = originalCliPath
|
||||
} else {
|
||||
delete process.env.CLAUDE_CLI_PATH
|
||||
}
|
||||
if (originalDisableTerminalShellEnv !== undefined) {
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = originalDisableTerminalShellEnv
|
||||
} else {
|
||||
delete process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
|
||||
}
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
restoreEnv()
|
||||
})
|
||||
|
||||
// Use dynamic import to avoid bundling issues
|
||||
async function startTestServer() {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-e2e-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||
process.env.CLAUDE_CLI_PATH = mockSdkCliPath
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = '1'
|
||||
|
||||
// Create required directories
|
||||
await fs.mkdir(path.join(tmpDir, 'projects'), { recursive: true })
|
||||
@ -158,7 +187,7 @@ describe('E2E: Full Flow', () => {
|
||||
|
||||
it('should list available models', async () => {
|
||||
const { data } = await api('GET', '/api/models')
|
||||
expect(data.models.length).toBe(4)
|
||||
expect(data.models.length).toBe(3)
|
||||
expect(data.models[0].name).toBe('Opus 4.7')
|
||||
})
|
||||
|
||||
@ -332,13 +361,12 @@ describe('E2E: Full Flow', () => {
|
||||
// 10. CORS
|
||||
// =============================================
|
||||
|
||||
it('should handle CORS preflight', async () => {
|
||||
it('should block browser CORS preflight while H5 access is disabled', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/status`, {
|
||||
method: 'OPTIONS',
|
||||
headers: { 'Origin': 'http://localhost:3000' },
|
||||
})
|
||||
expect(res.status).toBe(204)
|
||||
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('http://localhost:3000')
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
// =============================================
|
||||
|
||||
@ -8,6 +8,16 @@ import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import { TaskService } from '../services/taskService.js'
|
||||
|
||||
const taskFixture = (overrides: Record<string, unknown>) => ({
|
||||
id: '1',
|
||||
subject: 'Test task',
|
||||
description: '',
|
||||
status: 'pending',
|
||||
blocks: [],
|
||||
blockedBy: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// TaskService unit tests
|
||||
// ============================================================================
|
||||
@ -32,68 +42,60 @@ describe('TaskService', () => {
|
||||
})
|
||||
|
||||
it('should list tasks from JSON files', async () => {
|
||||
const tasksDir = path.join(tmpDir, 'tasks')
|
||||
const tasksDir = path.join(tmpDir, 'tasks', 'default-list')
|
||||
await fs.mkdir(tasksDir, { recursive: true })
|
||||
|
||||
await fs.writeFile(path.join(tasksDir, 'task-001.json'), JSON.stringify({
|
||||
await fs.writeFile(path.join(tasksDir, '1.json'), JSON.stringify(taskFixture({
|
||||
id: 'task-001',
|
||||
type: 'local_agent',
|
||||
subject: 'code-review',
|
||||
status: 'completed',
|
||||
name: 'code-review',
|
||||
description: 'Review PR #42',
|
||||
createdAt: Date.now() - 60000,
|
||||
completedAt: Date.now(),
|
||||
}))
|
||||
})))
|
||||
|
||||
await fs.writeFile(path.join(tasksDir, 'task-002.json'), JSON.stringify({
|
||||
await fs.writeFile(path.join(tasksDir, '2.json'), JSON.stringify(taskFixture({
|
||||
id: 'task-002',
|
||||
type: 'in_process_teammate',
|
||||
status: 'running',
|
||||
name: 'frontend-dev',
|
||||
teamName: 'ui-team',
|
||||
createdAt: Date.now(),
|
||||
}))
|
||||
subject: 'frontend-dev',
|
||||
status: 'in_progress',
|
||||
owner: 'ui-team',
|
||||
})))
|
||||
|
||||
const svc = new TaskService()
|
||||
const tasks = await svc.listTasks()
|
||||
expect(tasks.length).toBe(2)
|
||||
// 按 createdAt 倒序
|
||||
expect(tasks[0].id).toBe('task-002')
|
||||
expect(tasks[1].id).toBe('task-001')
|
||||
expect(tasks[0].id).toBe('task-001')
|
||||
expect(tasks[1].id).toBe('task-002')
|
||||
})
|
||||
|
||||
it('should scan nested team task directories', async () => {
|
||||
const teamDir = path.join(tmpDir, 'tasks', 'my-team')
|
||||
await fs.mkdir(teamDir, { recursive: true })
|
||||
|
||||
await fs.writeFile(path.join(teamDir, 'member-1.json'), JSON.stringify({
|
||||
await fs.writeFile(path.join(teamDir, 'member-1.json'), JSON.stringify(taskFixture({
|
||||
id: 'member-1',
|
||||
type: 'in_process_teammate',
|
||||
subject: 'Implement feature',
|
||||
status: 'completed',
|
||||
teamName: 'my-team',
|
||||
}))
|
||||
})))
|
||||
|
||||
const svc = new TaskService()
|
||||
const tasks = await svc.listTasks()
|
||||
expect(tasks.length).toBe(1)
|
||||
expect(tasks[0].teamName).toBe('my-team')
|
||||
expect(tasks[0].taskListId).toBe('my-team')
|
||||
})
|
||||
|
||||
it('should get single task by ID', async () => {
|
||||
const tasksDir = path.join(tmpDir, 'tasks')
|
||||
const tasksDir = path.join(tmpDir, 'tasks', 'default-list')
|
||||
await fs.mkdir(tasksDir, { recursive: true })
|
||||
|
||||
await fs.writeFile(path.join(tasksDir, 'abc.json'), JSON.stringify({
|
||||
await fs.writeFile(path.join(tasksDir, 'abc.json'), JSON.stringify(taskFixture({
|
||||
id: 'abc',
|
||||
type: 'local_shell',
|
||||
status: 'failed',
|
||||
name: 'build',
|
||||
}))
|
||||
subject: 'build',
|
||||
status: 'completed',
|
||||
})))
|
||||
|
||||
const svc = new TaskService()
|
||||
const task = await svc.getTask('abc')
|
||||
const task = await svc.getTask('default-list', 'abc')
|
||||
expect(task).toBeDefined()
|
||||
expect(task!.status).toBe('failed')
|
||||
expect(task!.status).toBe('completed')
|
||||
})
|
||||
|
||||
it('should return null for unknown task', async () => {
|
||||
@ -103,7 +105,7 @@ describe('TaskService', () => {
|
||||
})
|
||||
|
||||
it('should skip invalid JSON files gracefully', async () => {
|
||||
const tasksDir = path.join(tmpDir, 'tasks')
|
||||
const tasksDir = path.join(tmpDir, 'tasks', 'default-list')
|
||||
await fs.mkdir(tasksDir, { recursive: true })
|
||||
await fs.writeFile(path.join(tasksDir, 'bad.json'), 'not json {{{')
|
||||
|
||||
@ -147,20 +149,22 @@ describe('Tasks API', () => {
|
||||
})
|
||||
|
||||
it('should return tasks when files exist', async () => {
|
||||
const tasksDir = path.join(tmpDir, 'tasks')
|
||||
const tasksDir = path.join(tmpDir, 'tasks', 'default-list')
|
||||
await fs.mkdir(tasksDir, { recursive: true })
|
||||
await fs.writeFile(path.join(tasksDir, 'test.json'), JSON.stringify({
|
||||
id: 'test', type: 'local_agent', status: 'completed', name: 'test-task',
|
||||
}))
|
||||
await fs.writeFile(path.join(tasksDir, 'test.json'), JSON.stringify(taskFixture({
|
||||
id: 'test',
|
||||
status: 'completed',
|
||||
subject: 'test-task',
|
||||
})))
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/tasks`)
|
||||
const data = await res.json()
|
||||
expect(data.tasks.length).toBe(1)
|
||||
expect(data.tasks[0].name).toBe('test-task')
|
||||
expect(data.tasks[0].subject).toBe('test-task')
|
||||
})
|
||||
|
||||
it('should return 404 for unknown task', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/tasks/nonexistent`)
|
||||
const res = await fetch(`${baseUrl}/api/tasks/lists/default-list/nonexistent`)
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user