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 from 637c908 dropped 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 from 637c908 dropped 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:
claude-code-best
2026-04-16 20:59:29 +08:00
committed by GitHub
parent a02dc0bded
commit c8d08d235b
137 changed files with 13267 additions and 837 deletions

View 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')
})
})

View 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')
})
})

View 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')
})
})