mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-17 17:03:19 +08:00
Recover from corrupt ops-cron store and tighten cron tool schemas.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b1bb0067a0
commit
25454a5c8c
4 changed files with 58 additions and 6 deletions
39
lib/store.js
39
lib/store.js
|
|
@ -216,11 +216,46 @@ export function createStore(options = {}) {
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const raw = await readFile(filePath, 'utf8')
|
const raw = await readFile(filePath, 'utf8')
|
||||||
|
// Reject embedded NUL — JSON.parse would throw an opaque "Unexpected token".
|
||||||
|
if (raw.includes('\u0000')) {
|
||||||
|
const error = new Error(`ops-cron store is corrupt (NUL byte) at ${filePath}; moved aside and starting empty`)
|
||||||
|
error.code = 'STORE_CORRUPT'
|
||||||
|
try {
|
||||||
|
const bak = `${filePath}.corrupt.${Date.now()}.bak`
|
||||||
|
await writeFile(bak, raw, 'utf8')
|
||||||
|
error.message = `${error.message} (backup: ${bak})`
|
||||||
|
} catch {
|
||||||
|
// best-effort backup
|
||||||
|
}
|
||||||
|
console.warn(`[dsh-ops-cron] ${error.message}`)
|
||||||
|
state = emptyState()
|
||||||
|
loaded = true
|
||||||
|
await persist()
|
||||||
|
return snapshot()
|
||||||
|
}
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = JSON.parse(raw)
|
||||||
state = hydrate(parsed)
|
state = hydrate(parsed)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error && error.code === 'ENOENT') state = emptyState()
|
if (error && error.code === 'ENOENT') {
|
||||||
else throw error
|
state = emptyState()
|
||||||
|
} else if (error instanceof SyntaxError) {
|
||||||
|
const wrapped = new Error(`ops-cron store JSON parse failed at ${filePath}: ${error.message}`)
|
||||||
|
wrapped.code = 'STORE_CORRUPT'
|
||||||
|
wrapped.cause = error
|
||||||
|
try {
|
||||||
|
const raw = await readFile(filePath, 'utf8').catch(() => '')
|
||||||
|
if (raw) await writeFile(`${filePath}.corrupt.${Date.now()}.bak`, raw, 'utf8')
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
console.warn(`[dsh-ops-cron] ${wrapped.message}; starting empty store`)
|
||||||
|
state = emptyState()
|
||||||
|
loaded = true
|
||||||
|
await persist()
|
||||||
|
return snapshot()
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
return snapshot()
|
return snapshot()
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,9 @@ const JOB_SCHEMA = {
|
||||||
schedule: { type: 'object', additionalProperties: true },
|
schedule: { type: 'object', additionalProperties: true },
|
||||||
createdAt: { type: 'number' },
|
createdAt: { type: 'number' },
|
||||||
updatedAt: { type: 'number' },
|
updatedAt: { type: 'number' },
|
||||||
lastRunAt: {},
|
lastRunAt: { type: ['number', 'null'] },
|
||||||
lastStatus: {},
|
lastStatus: { type: ['string', 'null'] },
|
||||||
nextRunAt: {},
|
nextRunAt: { type: ['number', 'null'] },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-ops-cron",
|
"name": "dsh-ops-cron",
|
||||||
"description": "Scheduled tasks for DeepSeek Harness: Agent cron_* tools + sidebar 定时任务, with DSH or WhatsApp/IM delivery per job.",
|
"description": "Scheduled tasks for DeepSeek Harness: Agent cron_* tools + sidebar 定时任务, with DSH or WhatsApp/IM delivery per job.",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,23 @@ test('createJobRecord fires ASAP when one-shot at is slightly in the past', () =
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('createStore recovers from NUL-corrupt store.json', async () => {
|
||||||
|
const { createStore } = await import('../lib/store.js')
|
||||||
|
const { mkdtemp, writeFile, readFile, rm } = await import('node:fs/promises')
|
||||||
|
const { tmpdir } = await import('node:os')
|
||||||
|
const { join } = await import('node:path')
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-cron-store-'))
|
||||||
|
const filePath = join(dir, 'store.json')
|
||||||
|
await writeFile(filePath, `{\u0000"jobs":[]}`, 'utf8')
|
||||||
|
const store = createStore({ filePath })
|
||||||
|
const state = await store.read()
|
||||||
|
assert.equal(Array.isArray(state.jobs), true)
|
||||||
|
assert.equal(state.jobs.length, 0)
|
||||||
|
const rewritten = await readFile(filePath, 'utf8')
|
||||||
|
assert.equal(rewritten.includes('\u0000'), false)
|
||||||
|
await rm(dir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
test('resolveCreateAgentPreset prefers explicit then peer then session then host default', async () => {
|
test('resolveCreateAgentPreset prefers explicit then peer then session then host default', async () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await resolveCreateAgentPreset({ agent_preset: 'explicit' }, {}, {}),
|
await resolveCreateAgentPreset({ agent_preset: 'explicit' }, {}, {}),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue