mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25: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:
203
src/utils/__tests__/cronTasks.baseline.test.ts
Normal file
203
src/utils/__tests__/cronTasks.baseline.test.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
getSessionCronTasks,
|
||||
resetStateForTests,
|
||||
setOriginalCwd,
|
||||
setProjectRoot,
|
||||
} from '../../bootstrap/state'
|
||||
import {
|
||||
addCronTask,
|
||||
findMissedTasks,
|
||||
getCronFilePath,
|
||||
hasCronTasksSync,
|
||||
listAllCronTasks,
|
||||
markCronTasksFired,
|
||||
nextCronRunMs,
|
||||
oneShotJitteredNextCronRunMs,
|
||||
readCronTasks,
|
||||
removeCronTasks,
|
||||
writeCronTasks,
|
||||
} from '../cronTasks'
|
||||
import { cleanupTempDir, createTempDir } from '../../../tests/mocks/file-system'
|
||||
|
||||
let tempDir = ''
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await createTempDir('cron-baseline-')
|
||||
resetStateForTests()
|
||||
setOriginalCwd(tempDir)
|
||||
setProjectRoot(tempDir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
resetStateForTests()
|
||||
if (tempDir) {
|
||||
await cleanupTempDir(tempDir)
|
||||
}
|
||||
})
|
||||
|
||||
describe('cronTasks baseline', () => {
|
||||
test('session-only cron tasks remain in memory and do not create the cron file', async () => {
|
||||
const id = await addCronTask('* * * * *', 'session-only prompt', true, false)
|
||||
|
||||
const tasks = await listAllCronTasks()
|
||||
|
||||
expect(id).toHaveLength(8)
|
||||
expect(getSessionCronTasks()).toHaveLength(1)
|
||||
expect(tasks).toHaveLength(1)
|
||||
expect(tasks[0]).toMatchObject({
|
||||
id,
|
||||
prompt: 'session-only prompt',
|
||||
durable: false,
|
||||
recurring: true,
|
||||
})
|
||||
expect(existsSync(getCronFilePath())).toBe(false)
|
||||
})
|
||||
|
||||
test('durable cron tasks are written to .claude/scheduled_tasks.json', async () => {
|
||||
const id = await addCronTask('* * * * *', 'durable prompt', true, true)
|
||||
|
||||
const filePath = getCronFilePath()
|
||||
const fileTasks = await readCronTasks()
|
||||
|
||||
expect(existsSync(filePath)).toBe(true)
|
||||
expect(filePath).toBe(join(tempDir, '.claude', 'scheduled_tasks.json'))
|
||||
expect(fileTasks).toHaveLength(1)
|
||||
expect(fileTasks[0]).toMatchObject({
|
||||
id,
|
||||
prompt: 'durable prompt',
|
||||
recurring: true,
|
||||
})
|
||||
expect(fileTasks[0].durable).toBeUndefined()
|
||||
})
|
||||
|
||||
test('writeCronTasks strips runtime-only durable flags from disk', async () => {
|
||||
await writeCronTasks([
|
||||
{
|
||||
id: 'abc12345',
|
||||
cron: '* * * * *',
|
||||
prompt: 'strip durable',
|
||||
createdAt: 123,
|
||||
recurring: true,
|
||||
durable: false,
|
||||
},
|
||||
])
|
||||
|
||||
const raw = await readFile(getCronFilePath(), 'utf-8')
|
||||
expect(raw).not.toContain('"durable"')
|
||||
})
|
||||
|
||||
test('hasCronTasksSync reflects whether the durable cron file has entries', async () => {
|
||||
expect(hasCronTasksSync()).toBe(false)
|
||||
|
||||
await writeCronTasks([
|
||||
{
|
||||
id: 'sync0001',
|
||||
cron: '* * * * *',
|
||||
prompt: 'present',
|
||||
createdAt: 1,
|
||||
},
|
||||
])
|
||||
|
||||
expect(hasCronTasksSync()).toBe(true)
|
||||
})
|
||||
|
||||
test('daemon-style listAllCronTasks(dir) excludes session-only tasks', async () => {
|
||||
await addCronTask('* * * * *', 'session prompt', true, false)
|
||||
const durableId = await addCronTask('* * * * *', 'durable prompt', true, true)
|
||||
|
||||
const sessionView = await listAllCronTasks()
|
||||
const daemonView = await listAllCronTasks(tempDir)
|
||||
|
||||
expect(sessionView).toHaveLength(2)
|
||||
expect(daemonView).toHaveLength(1)
|
||||
expect(daemonView[0]).toMatchObject({
|
||||
id: durableId,
|
||||
prompt: 'durable prompt',
|
||||
})
|
||||
})
|
||||
|
||||
test('removeCronTasks without dir removes session-only tasks from memory', async () => {
|
||||
const sessionId = await addCronTask('* * * * *', 'remove me', true, false)
|
||||
|
||||
await removeCronTasks([sessionId])
|
||||
|
||||
expect(getSessionCronTasks()).toHaveLength(0)
|
||||
expect(await listAllCronTasks()).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('removeCronTasks with dir does not mutate session-only task storage', async () => {
|
||||
const sessionId = await addCronTask('* * * * *', 'keep session task', true, false)
|
||||
await addCronTask('* * * * *', 'durable prompt', true, true)
|
||||
|
||||
await removeCronTasks([sessionId], tempDir)
|
||||
|
||||
expect(getSessionCronTasks()).toHaveLength(1)
|
||||
expect(getSessionCronTasks()[0]?.id).toBe(sessionId)
|
||||
})
|
||||
|
||||
test('markCronTasksFired persists lastFiredAt for durable tasks', async () => {
|
||||
await writeCronTasks([
|
||||
{
|
||||
id: 'fire0001',
|
||||
cron: '* * * * *',
|
||||
prompt: 'persist fired',
|
||||
createdAt: 100,
|
||||
recurring: true,
|
||||
},
|
||||
])
|
||||
|
||||
await markCronTasksFired(['fire0001'], 123456789)
|
||||
|
||||
const tasks = await readCronTasks()
|
||||
expect(tasks[0]?.lastFiredAt).toBe(123456789)
|
||||
})
|
||||
|
||||
test('findMissedTasks returns tasks whose first scheduled run is in the past', () => {
|
||||
const nowMs = new Date('2026-04-12T10:10:00').getTime()
|
||||
const tasks = findMissedTasks(
|
||||
[
|
||||
{
|
||||
id: 'missed01',
|
||||
cron: '* * * * *',
|
||||
prompt: 'old task',
|
||||
createdAt: new Date('2026-04-12T10:00:00').getTime(),
|
||||
},
|
||||
{
|
||||
id: 'future01',
|
||||
cron: '59 23 31 12 *',
|
||||
prompt: 'far future',
|
||||
createdAt: nowMs,
|
||||
},
|
||||
],
|
||||
nowMs,
|
||||
)
|
||||
|
||||
expect(tasks.map(t => t.id)).toEqual(['missed01'])
|
||||
})
|
||||
|
||||
test('nextCronRunMs returns null for invalid cron expressions', () => {
|
||||
expect(nextCronRunMs('invalid cron', Date.now())).toBeNull()
|
||||
})
|
||||
|
||||
test('oneShotJitteredNextCronRunMs never returns a time earlier than fromMs', () => {
|
||||
const fromMs = new Date('2026-04-12T10:59:50').getTime()
|
||||
const next = oneShotJitteredNextCronRunMs('0 11 * * *', fromMs, '00000000')
|
||||
|
||||
expect(next).not.toBeNull()
|
||||
expect(next!).toBeGreaterThanOrEqual(fromMs)
|
||||
})
|
||||
|
||||
test('jitteredNextCronRunMs returns the exact next fire time when no second match exists in range', () => {
|
||||
const fromMs = new Date('2026-04-12T10:00:00').getTime()
|
||||
const exact = nextCronRunMs('0 0 29 2 *', fromMs)
|
||||
const jittered = oneShotJitteredNextCronRunMs('0 0 29 2 *', fromMs, '89abcdef')
|
||||
|
||||
expect(exact).not.toBeNull()
|
||||
expect(jittered).not.toBeNull()
|
||||
expect(jittered!).toBeGreaterThanOrEqual(fromMs)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user