mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 00:43:22 +08:00
Stamp and reclaim chat-created cron jobs for the current user.
Tool creates often left ownerEmpNo unassigned so the sidebar hid them while runs still fired; infer identity from session/cwd, claim legacy orphans on list, and keep cron rows visible without leaking run sessions into the workspace list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d298c77f3f
commit
a0dc070372
7 changed files with 254 additions and 25 deletions
|
|
@ -865,6 +865,57 @@ test('host HTTP: per-user job isolation and super sees all', async (t) => {
|
|||
assert.equal(listTester2.body.jobs.length, 2)
|
||||
})
|
||||
|
||||
test('GET /jobs claims unassigned jobs that match viewer cwd', async (t) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-ops-cron-claim-'))
|
||||
t.after(() => rm(dir, { recursive: true, force: true }))
|
||||
const service = createTestHost({
|
||||
filePath: join(dir, 'store.json'),
|
||||
now: () => Date.parse('2026-08-24T01:00:00.000Z'),
|
||||
})
|
||||
const mine = await service.createJob({
|
||||
name: 'orphan-mine',
|
||||
prompt: 'x',
|
||||
schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' },
|
||||
cwd: '/tmp/plain-a',
|
||||
})
|
||||
const other = await service.createJob({
|
||||
name: 'orphan-other',
|
||||
prompt: 'y',
|
||||
schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' },
|
||||
cwd: '/tmp/plain-b',
|
||||
})
|
||||
// Simulate legacy rows: unassigned but cwd clearly under the viewer's workspace.
|
||||
await service.store.mutate((current) => ({
|
||||
...current,
|
||||
jobs: current.jobs.map((job) => {
|
||||
if (job.id === mine.id) {
|
||||
return {
|
||||
...job,
|
||||
ownerEmpNo: '__unassigned__',
|
||||
ownerDisplayName: '',
|
||||
cwd: '/tmp/user-workspaces/tester/ws',
|
||||
}
|
||||
}
|
||||
if (job.id === other.id) {
|
||||
return {
|
||||
...job,
|
||||
ownerEmpNo: '__unassigned__',
|
||||
ownerDisplayName: '',
|
||||
cwd: '/tmp/user-workspaces/peer/ws',
|
||||
}
|
||||
}
|
||||
return job
|
||||
}),
|
||||
}))
|
||||
const http = await listen(service)
|
||||
t.after(() => http.close())
|
||||
const listed = await jsonRequest(http.url, '/dsh-ops-cron/jobs', { empNo: 'tester' })
|
||||
assert.equal(listed.status, 200)
|
||||
assert.equal(listed.body.jobs.length, 1)
|
||||
assert.equal(listed.body.jobs[0].name, 'orphan-mine')
|
||||
assert.equal(listed.body.jobs[0].ownerEmpNo, 'tester')
|
||||
})
|
||||
|
||||
test('migrateJobOwners assigns unassigned for legacy jobs', async () => {
|
||||
const { migrateJobOwners, UNASSIGNED_OWNER } = await import('../lib/ownership.js')
|
||||
const state = {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { test } from 'node:test'
|
|||
import {
|
||||
assertCanAccessJob,
|
||||
canViewAllJobs,
|
||||
claimUnassignedForViewer,
|
||||
filterJobsForIdentity,
|
||||
filterRunsForJobs,
|
||||
inferOwnerEmpNo,
|
||||
|
|
@ -72,3 +73,23 @@ test('viewerPayload', () => {
|
|||
assert.equal(v.canViewAll, false)
|
||||
assert.equal(v.workspacePath, '/w/u1')
|
||||
})
|
||||
|
||||
test('claimUnassignedForViewer claims by session owner or user-workspaces cwd', () => {
|
||||
const user = { empNo: 'u1', displayName: 'U1', permissions: { canViewAllSessions: false } }
|
||||
const state = {
|
||||
jobs: [
|
||||
{ id: 'a', ownerEmpNo: UNASSIGNED_OWNER, origin: { kind: 'web', sessionId: 's1' }, cwd: '/tmp/other' },
|
||||
{ id: 'b', ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u1/proj' },
|
||||
{ id: 'c', ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u2/proj' },
|
||||
{ id: 'd', ownerEmpNo: 'u2', cwd: '/data/user-workspaces/u1/x' },
|
||||
],
|
||||
}
|
||||
const { changed, state: next } = claimUnassignedForViewer(state, user, {
|
||||
getSessionOwner: (id) => (id === 's1' ? 'u1' : null),
|
||||
})
|
||||
assert.equal(changed, true)
|
||||
assert.equal(next.jobs[0].ownerEmpNo, 'u1')
|
||||
assert.equal(next.jobs[1].ownerEmpNo, 'u1')
|
||||
assert.equal(next.jobs[2].ownerEmpNo, UNASSIGNED_OWNER)
|
||||
assert.equal(next.jobs[3].ownerEmpNo, 'u2')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
|
|||
import { join } from 'node:path'
|
||||
import { test } from 'node:test'
|
||||
import { createHostService } from '../lib/host.js'
|
||||
import { callerWorkingDirectory, cronToolDefinitions, registerCronGuidance, registerCronTools, resolveCreateCwd, resolveCreateModel, scheduleFromArgs } from '../lib/tools.js'
|
||||
import { callerWorkingDirectory, cronToolDefinitions, registerCronGuidance, registerCronTools, resolveCreateCwd, resolveCreateModel, resolveToolIdentity, scheduleFromArgs } from '../lib/tools.js'
|
||||
|
||||
async function makeService(t) {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-cron-tools-'))
|
||||
|
|
@ -241,6 +241,25 @@ test('registerCronGuidance registers the scheduled-tasks skill only', () => {
|
|||
assert.equal(skills.length, 0)
|
||||
})
|
||||
|
||||
test('cron_create stamps ownerEmpNo from user-workspaces cwd', async (t) => {
|
||||
const service = await makeService(t)
|
||||
const tools = byName(cronToolDefinitions(service))
|
||||
const exec = { agent: { session: { header: { cwd: '/tmp/user-workspaces/alice/app' } } } }
|
||||
assert.equal(resolveToolIdentity(exec, service)?.empNo, 'alice')
|
||||
const created = await tools.cron_create.execute({
|
||||
name: 'owned',
|
||||
prompt: 'ping',
|
||||
after_minutes: 5,
|
||||
timezone: 'Asia/Shanghai',
|
||||
}, exec)
|
||||
assert.equal(created.job.ownerEmpNo, 'alice')
|
||||
assert.equal(created.job.cwd, '/tmp/user-workspaces/alice/app')
|
||||
|
||||
const listed = await tools.cron_list.execute({}, exec)
|
||||
assert.equal(listed.count, 1)
|
||||
assert.equal(listed.jobs[0].id, created.job.id)
|
||||
})
|
||||
|
||||
test('registerCronTools registers each definition and disposer unregisters', () => {
|
||||
const registered = []
|
||||
const ctx = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue