mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
Feat/integrate lint preview (#285)
* feat: 适配 zed acp 协议 * docs: 完善 acp 文档 * feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎 Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct detectMimeFromBase64 to decode raw bytes from base64 Cherry-picked from origin/lint/preview (ee36954). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构 Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes. - 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层 - 修复 --daemon-worker=kind 等号格式解析 - 修复 daemon/bg fast path 缺少 setShellIfWindows() - 修复 checkPathExists 用 existsSync 替代 execSync('dir') - 7 个 spawn 站点迁移到 CliLaunchSpec Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: merge tsconfig.base.json into tsconfig.json with full compiler options The cherry-pick from637c908dropped jsx/strict/etc settings when removing tsconfig.base.json. This commit restores them in a single tsconfig.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: merge tsconfig.base.json into tsconfig.json with full compiler options The cherry-pick from637c908dropped jsx/strict/etc settings when removing tsconfig.base.json. This commit restores them in a single tsconfig.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
140
src/jobs/__tests__/classifier.test.ts
Normal file
140
src/jobs/__tests__/classifier.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Tests for src/jobs/classifier.ts
|
||||
*
|
||||
* Uses real temp directories instead of mocking fs to avoid
|
||||
* cross-test mock pollution in bun test.
|
||||
*
|
||||
* classifier.ts takes jobDir as a parameter, so no envUtils mock needed.
|
||||
*/
|
||||
import { describe, expect, test, beforeEach, afterAll } from 'bun:test'
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import type { AssistantMessage } from '../../types/message.js'
|
||||
import { classifyAndWriteState } from '../classifier.js'
|
||||
|
||||
// ─── setup: real temp dir ──────────────────────────────────────────────────
|
||||
|
||||
let tempBase: string
|
||||
let jobDir: string
|
||||
let stateFile: string
|
||||
|
||||
tempBase = mkdtempSync(join(tmpdir(), 'classifier-test-'))
|
||||
|
||||
function freshJobDir(): void {
|
||||
jobDir = mkdtempSync(join(tempBase, 'job-'))
|
||||
stateFile = join(jobDir, 'state.json')
|
||||
}
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function makeAssistantMessage(
|
||||
content: any[],
|
||||
extra: Record<string, any> = {},
|
||||
): AssistantMessage {
|
||||
return {
|
||||
type: 'assistant',
|
||||
uuid: '00000000-0000-0000-0000-000000000000' as any,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content,
|
||||
...extra,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
|
||||
// ─── lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
beforeEach(() => {
|
||||
freshJobDir()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
rmSync(tempBase, { recursive: true, force: true })
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
})
|
||||
|
||||
// ─── tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('classifyAndWriteState', () => {
|
||||
test('does nothing when state.json is missing', async () => {
|
||||
await classifyAndWriteState(jobDir, [])
|
||||
// stateFile should still not exist
|
||||
let exists = false
|
||||
try {
|
||||
readFileSync(stateFile, 'utf-8')
|
||||
exists = true
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
expect(exists).toBe(false)
|
||||
})
|
||||
|
||||
test('sets status to running when last message has tool_use block', async () => {
|
||||
writeFileSync(
|
||||
stateFile,
|
||||
JSON.stringify({ status: 'created', updatedAt: '2026-01-01' }),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const msg = makeAssistantMessage([
|
||||
{ type: 'text', text: 'Let me check...' },
|
||||
{ type: 'tool_use', id: 'toolu_1', name: 'bash', input: {} },
|
||||
])
|
||||
|
||||
await classifyAndWriteState(jobDir, [msg])
|
||||
|
||||
const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
||||
expect(state.status).toBe('running')
|
||||
})
|
||||
|
||||
test('sets status to completed when stop_reason is end_turn', async () => {
|
||||
writeFileSync(
|
||||
stateFile,
|
||||
JSON.stringify({ status: 'running', updatedAt: '2026-01-01' }),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const msg = makeAssistantMessage([{ type: 'text', text: 'All done.' }], {
|
||||
stop_reason: 'end_turn',
|
||||
})
|
||||
|
||||
await classifyAndWriteState(jobDir, [msg])
|
||||
|
||||
const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
||||
expect(state.status).toBe('completed')
|
||||
})
|
||||
|
||||
test('sets status to running for empty messages (state exists)', async () => {
|
||||
writeFileSync(
|
||||
stateFile,
|
||||
JSON.stringify({ status: 'created', updatedAt: '2026-01-01' }),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
await classifyAndWriteState(jobDir, [])
|
||||
|
||||
const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
||||
expect(state.status).toBe('running')
|
||||
})
|
||||
|
||||
test('sets status to running when stop_reason is max_tokens', async () => {
|
||||
writeFileSync(
|
||||
stateFile,
|
||||
JSON.stringify({ status: 'running', updatedAt: '2026-01-01' }),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const msg = makeAssistantMessage([{ type: 'text', text: 'I need more' }], {
|
||||
stop_reason: 'max_tokens',
|
||||
})
|
||||
|
||||
await classifyAndWriteState(jobDir, [msg])
|
||||
|
||||
const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
||||
expect(state.status).toBe('running')
|
||||
})
|
||||
})
|
||||
91
src/jobs/__tests__/state.test.ts
Normal file
91
src/jobs/__tests__/state.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Tests for src/jobs/state.ts
|
||||
*
|
||||
* Uses real temp directories and CLAUDE_CONFIG_DIR env var
|
||||
* instead of mocking fs, to avoid cross-test mock pollution.
|
||||
*/
|
||||
import { describe, expect, test, beforeEach, afterAll } from 'bun:test'
|
||||
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
// ─── setup: real temp dir via env var ──────────────────────────────────────
|
||||
|
||||
const tempBase = mkdtempSync(join(tmpdir(), 'jobs-state-test-'))
|
||||
|
||||
beforeEach(() => {
|
||||
// Each test gets a fresh config dir
|
||||
const tempHome = mkdtempSync(join(tempBase, 'home-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tempHome
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
try {
|
||||
rmSync(tempBase, { recursive: true, force: true })
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
})
|
||||
|
||||
// ─── import ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const { createJob, readJobState, appendJobReply, getJobDir } = await import(
|
||||
'../state.js'
|
||||
)
|
||||
|
||||
// ─── tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('createJob', () => {
|
||||
test('creates job directory and writes state, template, and input files', () => {
|
||||
const dir = createJob('job-1', 'my-template', '# Template', 'hello', [
|
||||
'--flag',
|
||||
])
|
||||
expect(dir).toContain('job-1')
|
||||
expect(existsSync(dir)).toBe(true)
|
||||
|
||||
const stateFile = join(dir, 'state.json')
|
||||
expect(existsSync(stateFile)).toBe(true)
|
||||
const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
||||
expect(state.jobId).toBe('job-1')
|
||||
expect(state.templateName).toBe('my-template')
|
||||
expect(state.status).toBe('created')
|
||||
expect(state.args).toEqual(['--flag'])
|
||||
|
||||
expect(readFileSync(join(dir, 'template.md'), 'utf-8')).toBe('# Template')
|
||||
expect(readFileSync(join(dir, 'input.txt'), 'utf-8')).toBe('hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('readJobState', () => {
|
||||
test('returns null when job does not exist', () => {
|
||||
expect(readJobState('nonexistent')).toBeNull()
|
||||
})
|
||||
|
||||
test('returns parsed state when job exists', () => {
|
||||
createJob('job-2', 'tpl', 'content', 'input', [])
|
||||
const result = readJobState('job-2')
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.jobId).toBe('job-2')
|
||||
expect(result!.status).toBe('created')
|
||||
})
|
||||
})
|
||||
|
||||
describe('appendJobReply', () => {
|
||||
test('returns false when job does not exist', () => {
|
||||
expect(appendJobReply('no-job', 'hello')).toBe(false)
|
||||
})
|
||||
|
||||
test('appends reply and updates state', () => {
|
||||
createJob('job-3', 'tpl', 'content', 'input', [])
|
||||
|
||||
const result = appendJobReply('job-3', 'my reply')
|
||||
expect(result).toBe(true)
|
||||
|
||||
const dir = getJobDir('job-3')
|
||||
const repliesPath = join(dir, 'replies.jsonl')
|
||||
expect(existsSync(repliesPath)).toBe(true)
|
||||
const replyLine = JSON.parse(readFileSync(repliesPath, 'utf-8').trim())
|
||||
expect(replyLine.text).toBe('my reply')
|
||||
})
|
||||
})
|
||||
87
src/jobs/__tests__/templates.test.ts
Normal file
87
src/jobs/__tests__/templates.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Tests for src/jobs/templates.ts
|
||||
*
|
||||
* Uses real temp directories and CLAUDE_CONFIG_DIR env var
|
||||
* instead of mocking fs, to avoid cross-test mock pollution.
|
||||
*/
|
||||
import { describe, expect, test, beforeEach, afterAll } from 'bun:test'
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
// ─── setup: real temp dir via env var ──────────────────────────────────────
|
||||
|
||||
const tempBase = mkdtempSync(join(tmpdir(), 'jobs-templates-test-'))
|
||||
|
||||
beforeEach(() => {
|
||||
const tempHome = mkdtempSync(join(tempBase, 'home-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tempHome
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
try {
|
||||
rmSync(tempBase, { recursive: true, force: true })
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
})
|
||||
|
||||
// ─── import ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const { listTemplates, loadTemplate } = await import('../templates.js')
|
||||
|
||||
// ─── tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('listTemplates', () => {
|
||||
test('returns empty array when no template dirs exist', () => {
|
||||
const result = listTemplates()
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test('discovers templates from user-level dir', () => {
|
||||
const userDir = join(process.env.CLAUDE_CONFIG_DIR!, 'templates')
|
||||
mkdirSync(userDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(userDir, 'greeting.md'),
|
||||
'---\ndescription: A greeting template\n---\nHello {{name}}',
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const result = listTemplates()
|
||||
expect(result.length).toBe(1)
|
||||
expect(result[0]!.name).toBe('greeting')
|
||||
expect(result[0]!.description).toBe('A greeting template')
|
||||
expect(result[0]!.content).toBe('Hello {{name}}')
|
||||
})
|
||||
|
||||
test('skips non-md files', () => {
|
||||
const userDir = join(process.env.CLAUDE_CONFIG_DIR!, 'templates')
|
||||
mkdirSync(userDir, { recursive: true })
|
||||
writeFileSync(join(userDir, 'notes.txt'), 'not a template', 'utf-8')
|
||||
writeFileSync(join(userDir, 'data.json'), '{}', 'utf-8')
|
||||
|
||||
const result = listTemplates()
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadTemplate', () => {
|
||||
test('returns null when template not found', () => {
|
||||
expect(loadTemplate('nonexistent')).toBeNull()
|
||||
})
|
||||
|
||||
test('returns template by name', () => {
|
||||
const userDir = join(process.env.CLAUDE_CONFIG_DIR!, 'templates')
|
||||
mkdirSync(userDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(userDir, 'deploy.md'),
|
||||
'---\ndescription: Deploy script\n---\nrun deploy',
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const result = loadTemplate('deploy')
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.name).toBe('deploy')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user