Show cron state/labels in sidebar and push archive history.

Surface lifecycle badges and editable monitor fields in the UI, and POST terminal runs to persist_history archive endpoints on settle/tick.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-10 15:29:04 +08:00
parent b6934b20a3
commit 2e13432b0a
4 changed files with 500 additions and 13 deletions

90
test/archive.test.js Normal file
View file

@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import {
markRunsArchived,
pruneArchivedRuns,
pushArchiveBatch,
runsNeedingArchive,
} from '../lib/archive.js'
import { createHostService } from '../lib/host.js'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
test('runsNeedingArchive skips active and already archived', () => {
const runs = [
{ id: '1', jobId: 'j', status: 'running' },
{ id: '2', jobId: 'j', status: 'succeeded' },
{ id: '3', jobId: 'j', status: 'failed', archivedAt: 1 },
{ id: '4', jobId: 'other', status: 'succeeded' },
]
assert.deepEqual(runsNeedingArchive(runs, 'j').map((r) => r.id), ['2'])
})
test('pushArchiveBatch posts JSON and mark/prune archive local copy', async () => {
const posts = []
const result = await pushArchiveBatch('https://archive.example/cron', {
job: { id: 'j1', name: 'n' },
runs: [{ id: 'r1', status: 'succeeded' }],
}, {
now: () => Date.parse('2026-09-10T08:00:00.000Z'),
fetchImpl: async (url, init) => {
posts.push({ url, init })
return { ok: true, status: 200, async json() { return { received: 1 } } }
},
})
assert.equal(result.ok, true)
assert.equal(posts[0].url, 'https://archive.example/cron')
assert.match(posts[0].init.body, /"plugin":"dsh-ops-cron"/)
let state = {
jobs: [{ id: 'j1', persistHistory: { kind: 'archive', endpoint: 'https://archive.example/cron' } }],
runs: Array.from({ length: 60 }, (_, i) => ({
id: `r${i}`,
jobId: 'j1',
status: 'succeeded',
actualAt: i,
archivedAt: i < 55 ? 1000 + i : undefined,
})),
}
state = markRunsArchived(state, ['r59'], 9999)
state = pruneArchivedRuns(state, 10)
const archived = state.runs.filter((r) => r.archivedAt)
assert.ok(archived.length <= 10)
assert.ok(state.runs.some((r) => r.id === 'r59'))
})
test('host archiveJobRuns pushes and marks runs', async (t) => {
const dir = await mkdtemp(join(tmpdir(), 'dsh-cron-arch-'))
t.after(() => rm(dir, { recursive: true, force: true }))
const posts = []
const service = createHostService({
filePath: join(dir, 'store.json'),
now: () => Date.parse('2026-09-10T08:00:00.000Z'),
fetchImpl: async (url, init) => {
posts.push({ url, body: JSON.parse(init.body) })
return { ok: true, status: 204, async json() { return null } }
},
sessionPort: {
async createAndPrompt() {
return { sessionId: 's-arch', status: 'succeeded', summary: 'done' }
},
},
})
const job = await service.createJob({
name: 'arch-job',
prompt: 'work',
schedule: { kind: 'at', at: new Date(Date.parse('2026-09-10T08:01:00.000Z')).toISOString(), timezone: 'Asia/Shanghai' },
cwd: dir,
persistHistory: { kind: 'archive', endpoint: 'https://archive.example/intake' },
})
await service.dispatchRun(job.id, 'run-now')
assert.ok(posts.length >= 1, 'settle should auto-archive')
assert.equal(posts[0].url, 'https://archive.example/intake')
assert.equal(posts[0].body.job.id, job.id)
const snap = await service.snapshot()
const run = (snap.runs || []).find((row) => row.jobId === job.id && row.status === 'succeeded')
assert.ok(run?.archivedAt)
const again = await service.archiveJobRuns(job.id, { limit: 10 })
assert.equal(again.archived, 0)
})