mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 01:53:19 +08:00
Fix cron cwd for super_admin and harden IM channel job isolation.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b35bfde557
commit
b1bb0067a0
16 changed files with 714 additions and 49 deletions
|
|
@ -11,6 +11,7 @@ import {
|
|||
deliverRunToIm,
|
||||
mirrorRunToSession,
|
||||
jobVisibleToPeer,
|
||||
assertDeliveryAllowedForIdentity,
|
||||
} from '../lib/delivery.js'
|
||||
|
||||
test('normalizeDelivery defaults to dsh', () => {
|
||||
|
|
@ -439,3 +440,17 @@ test('formatRunResultBody matches IM delivery header', () => {
|
|||
const en = formatRunResultBody({ name: 'Daily' }, 'ok', 'en')
|
||||
assert.match(en, /^\[Scheduled tasks · Daily\]\n/)
|
||||
})
|
||||
|
||||
test('assertDeliveryAllowedForIdentity blocks non-admin web IM delivery', () => {
|
||||
const im = { kind: 'im', botId: 'b', targetId: 't' }
|
||||
assert.equal(
|
||||
assertDeliveryAllowedForIdentity(im, { empNo: 'a1', permissions: { canViewAllSessions: true } }),
|
||||
im,
|
||||
)
|
||||
assert.doesNotThrow(() => assertDeliveryAllowedForIdentity(im, { empNo: '__local__', permissions: { canViewAllSessions: true } }))
|
||||
assert.throws(
|
||||
() => assertDeliveryAllowedForIdentity(im, { empNo: 'u1', permissions: { canViewAllSessions: false } }),
|
||||
(error) => error.code === 'IM_DELIVERY_FORBIDDEN',
|
||||
)
|
||||
assert.doesNotThrow(() => assertDeliveryAllowedForIdentity({ kind: 'dsh' }, { empNo: 'u1', permissions: {} }))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ function mockUdsAuth(options = {}) {
|
|||
tester: { role: 'user', canViewAll: false, path: '/tmp/user-workspaces/tester', displayName: 'Tester' },
|
||||
peer: { role: 'user', canViewAll: false, path: '/tmp/user-workspaces/peer', displayName: 'Peer' },
|
||||
admin1: { role: 'super_admin', canViewAll: true, path: '/tmp/user-workspaces/admin1', displayName: 'Admin' },
|
||||
'10329667': { role: 'super_admin', canViewAll: true, path: '/tmp/deepseek-harness/10329667', displayName: 'Super' },
|
||||
administrator: { role: 'fallback_admin', canViewAll: true, path: '/tmp/user-workspaces/administrator', displayName: 'Fallback' },
|
||||
...(options.users || {}),
|
||||
}
|
||||
|
|
@ -119,7 +120,30 @@ function mockUdsAuth(options = {}) {
|
|||
empNo,
|
||||
role: row.role,
|
||||
displayName: row.displayName || empNo,
|
||||
permissions: { canViewAllSessions: !!row.canViewAll },
|
||||
permissions: {
|
||||
canViewAllSessions: !!row.canViewAll,
|
||||
canCreateWorkspace: !!row.canViewAll,
|
||||
},
|
||||
workspacePath: row.path,
|
||||
}
|
||||
},
|
||||
resolveIdentityForEmpNo(empNo) {
|
||||
const id = String(empNo || '').trim()
|
||||
if (!id || id.startsWith('__')) return null
|
||||
const row = users[id] || {
|
||||
role: 'user',
|
||||
canViewAll: false,
|
||||
path: `/tmp/user-workspaces/${id}`,
|
||||
displayName: id,
|
||||
}
|
||||
return {
|
||||
empNo: id,
|
||||
role: row.role,
|
||||
displayName: row.displayName || id,
|
||||
permissions: {
|
||||
canViewAllSessions: !!row.canViewAll,
|
||||
canCreateWorkspace: !!row.canViewAll,
|
||||
},
|
||||
workspacePath: row.path,
|
||||
}
|
||||
},
|
||||
|
|
@ -133,8 +157,9 @@ function mockUdsAuth(options = {}) {
|
|||
if (!candidatePath) return false
|
||||
if (options.strictPath) {
|
||||
const root = this.getProvisionedWorkspacePath(empNo)
|
||||
const cand = String(candidatePath)
|
||||
return cand === root || cand.startsWith(`${root}/`) || cand.startsWith(`${root}\\`)
|
||||
const cand = String(candidatePath).replace(/\\/g, '/')
|
||||
const normRoot = String(root).replace(/\\/g, '/')
|
||||
return cand === normRoot || cand.startsWith(`${normRoot}/`)
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
|
@ -562,6 +587,243 @@ test('resolveSessionPlacement uses recent workspace path when job cwd is empty',
|
|||
assert.equal(explicit.workspace, null)
|
||||
})
|
||||
|
||||
test('resolveSessionPlacement keeps foreign cwd for super_admin owner under strict path ACL', () => {
|
||||
const recent = { id: 'ws-recent', path: '/tmp/ws-app', async attachSession() {} }
|
||||
const ctx = {
|
||||
get(name) {
|
||||
if (name === 'workspaceRegistry') return { list: () => [recent] }
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
const uds = mockUdsAuth({ strictPath: true })
|
||||
const kept = resolveSessionPlacement(ctx, {
|
||||
cwd: 'D:/code/gpt',
|
||||
ownerEmpNo: '10329667',
|
||||
}, { udsAuth: uds })
|
||||
assert.equal(kept.cwd, 'D:/code/gpt')
|
||||
|
||||
const clamped = resolveSessionPlacement(ctx, {
|
||||
cwd: 'D:/code/gpt',
|
||||
ownerEmpNo: 'tester',
|
||||
}, { udsAuth: uds })
|
||||
assert.equal(clamped.cwd, '/tmp/user-workspaces/tester')
|
||||
})
|
||||
|
||||
test('resolveSessionPlacement prefers owner provisioned path over recent workspace when cwd empty', () => {
|
||||
const recent = { id: 'ws-recent', path: '/tmp/wrong-recent', async attachSession() {} }
|
||||
const ctx = {
|
||||
get(name) {
|
||||
if (name === 'workspaceRegistry') return { list: () => [recent] }
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
const uds = mockUdsAuth({ strictPath: true })
|
||||
const placed = resolveSessionPlacement(ctx, {
|
||||
cwd: '',
|
||||
ownerEmpNo: '10329667',
|
||||
}, { udsAuth: uds })
|
||||
assert.equal(placed.cwd, '/tmp/deepseek-harness/10329667')
|
||||
})
|
||||
|
||||
test('super_admin createJob keeps explicit foreign cwd; normal user is clamped', async (t) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-ops-cron-'))
|
||||
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'),
|
||||
udsAuthOptions: { strictPath: true },
|
||||
sessionPort: {
|
||||
async createAndPrompt() {
|
||||
return { sessionId: 's1', status: 'succeeded', summary: 'ok' }
|
||||
},
|
||||
async archiveSession() {},
|
||||
},
|
||||
})
|
||||
const superId = {
|
||||
empNo: '10329667',
|
||||
role: 'super_admin',
|
||||
displayName: 'Super',
|
||||
permissions: { canViewAllSessions: true, canCreateWorkspace: true },
|
||||
workspacePath: '/tmp/deepseek-harness/10329667',
|
||||
}
|
||||
const superJob = await service.createJob({
|
||||
name: 'gpt-job',
|
||||
prompt: 'work in gpt',
|
||||
cwd: 'D:/code/gpt',
|
||||
schedule: { kind: 'cron', expr: '0 2 * * *', timezone: 'Asia/Shanghai' },
|
||||
}, superId)
|
||||
assert.equal(superJob.cwd, 'D:/code/gpt')
|
||||
assert.equal(superJob.ownerEmpNo, '10329667')
|
||||
|
||||
const userId = {
|
||||
empNo: 'tester',
|
||||
role: 'user',
|
||||
displayName: 'Tester',
|
||||
permissions: { canViewAllSessions: false },
|
||||
workspacePath: '/tmp/user-workspaces/tester',
|
||||
}
|
||||
const userJob = await service.createJob({
|
||||
name: 'user-job',
|
||||
prompt: 'try foreign',
|
||||
cwd: 'D:/code/gpt',
|
||||
schedule: { kind: 'cron', expr: '0 3 * * *', timezone: 'Asia/Shanghai' },
|
||||
}, userId)
|
||||
assert.equal(userJob.cwd, '/tmp/user-workspaces/tester')
|
||||
})
|
||||
|
||||
test('live fire uses super_admin job cwd outside provisioned tree', async (t) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-ops-cron-'))
|
||||
t.after(() => rm(dir, { recursive: true, force: true }))
|
||||
const seen = []
|
||||
const uds = mockUdsAuth({ strictPath: true })
|
||||
const service = createHostService({
|
||||
filePath: join(dir, 'store.json'),
|
||||
now: () => Date.parse('2026-08-24T01:00:00.000Z'),
|
||||
getUdsAuth: () => uds,
|
||||
sessionPort: {
|
||||
async createAndPrompt({ job }) {
|
||||
const placement = resolveSessionPlacement({
|
||||
get() { return { list: () => [] } },
|
||||
}, job, { udsAuth: uds })
|
||||
seen.push(placement.cwd)
|
||||
return { sessionId: `run-${seen.length}`, status: 'succeeded', summary: 'ok' }
|
||||
},
|
||||
async archiveSession() {},
|
||||
},
|
||||
})
|
||||
const job = await service.createJob({
|
||||
name: 'fire-gpt',
|
||||
prompt: 'ping',
|
||||
cwd: 'D:/code/gpt',
|
||||
schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' },
|
||||
}, {
|
||||
empNo: '10329667',
|
||||
role: 'super_admin',
|
||||
permissions: { canViewAllSessions: true, canCreateWorkspace: true },
|
||||
})
|
||||
assert.equal(job.cwd, 'D:/code/gpt')
|
||||
await service.dispatchRun(job.id, 'run-now')
|
||||
assert.deepEqual(seen, ['D:/code/gpt'])
|
||||
})
|
||||
|
||||
test('IM peer create keeps bot cwd unassigned and rejects empty cwd', async (t) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-ops-cron-'))
|
||||
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'),
|
||||
udsAuthOptions: { strictPath: true },
|
||||
sessionPort: {
|
||||
async createAndPrompt() {
|
||||
return { sessionId: 's-im', status: 'succeeded', summary: 'ok' }
|
||||
},
|
||||
async archiveSession() {},
|
||||
},
|
||||
})
|
||||
const job = await service.createJob({
|
||||
name: 'im-job',
|
||||
prompt: 'from channel',
|
||||
cwd: '/data/bot-workspace/wa',
|
||||
delivery: { kind: 'im', botId: 'bot-a', targetId: 'auto-dm' },
|
||||
origin: {
|
||||
kind: 'im',
|
||||
sessionId: 'sess-im',
|
||||
peer: { botId: 'bot-a', conversationKey: 'direct:1@s.whatsapp.net', conversationId: '1@s.whatsapp.net' },
|
||||
},
|
||||
schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' },
|
||||
}, {
|
||||
empNo: '10329667',
|
||||
permissions: { canViewAllSessions: true },
|
||||
}, { fromImPeer: true })
|
||||
assert.equal(job.ownerEmpNo, '__unassigned__')
|
||||
assert.equal(job.cwd, '/data/bot-workspace/wa')
|
||||
assert.equal(job.origin.kind, 'im')
|
||||
|
||||
await assert.rejects(
|
||||
() => service.createJob({
|
||||
name: 'im-empty',
|
||||
prompt: 'x',
|
||||
cwd: '',
|
||||
origin: {
|
||||
kind: 'im',
|
||||
sessionId: 'sess-im',
|
||||
peer: { botId: 'bot-a', conversationKey: 'direct:1@s.whatsapp.net' },
|
||||
},
|
||||
schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' },
|
||||
}, null, { fromImPeer: true }),
|
||||
(error) => error.code === 'INVALID_CWD',
|
||||
)
|
||||
})
|
||||
|
||||
test('HTTP web create strips forged IM origin and rejects IM delivery for normal users', async (t) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-ops-cron-'))
|
||||
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'),
|
||||
sessionPort: {
|
||||
async createAndPrompt() {
|
||||
return { sessionId: 's', status: 'succeeded', summary: 'ok' }
|
||||
},
|
||||
async archiveSession() {},
|
||||
},
|
||||
})
|
||||
const { url, close } = await listen(service)
|
||||
t.after(close)
|
||||
|
||||
const forged = await jsonRequest(url, '/dsh-ops-cron/jobs', {
|
||||
empNo: 'tester',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'forge',
|
||||
prompt: 'x',
|
||||
schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' },
|
||||
cwd: '/tmp/user-workspaces/tester',
|
||||
origin: {
|
||||
kind: 'im',
|
||||
sessionId: 'sess-1',
|
||||
peer: { botId: 'bot-a', conversationKey: 'direct:x' },
|
||||
},
|
||||
}),
|
||||
})
|
||||
assert.equal(forged.status, 200)
|
||||
assert.equal(forged.body.job.ownerEmpNo, 'tester')
|
||||
assert.equal(forged.body.job.origin?.kind, 'web')
|
||||
|
||||
const denied = await jsonRequest(url, '/dsh-ops-cron/jobs', {
|
||||
empNo: 'tester',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'im-web',
|
||||
prompt: 'x',
|
||||
schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' },
|
||||
delivery: { kind: 'im', botId: 'bot-a', targetId: 't1' },
|
||||
}),
|
||||
})
|
||||
assert.equal(denied.status, 400)
|
||||
assert.equal(denied.body.code, 'IM_DELIVERY_FORBIDDEN')
|
||||
})
|
||||
|
||||
test('resolveSessionPlacement refuses recent-workspace fallback for IM jobs without cwd', () => {
|
||||
const recent = { id: 'ws-recent', path: '/tmp/wrong-recent', async attachSession() {} }
|
||||
const ctx = {
|
||||
get(name) {
|
||||
if (name === 'workspaceRegistry') return { list: () => [recent] }
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
const placed = resolveSessionPlacement(ctx, {
|
||||
cwd: '',
|
||||
ownerEmpNo: '__unassigned__',
|
||||
origin: {
|
||||
kind: 'im',
|
||||
peer: { botId: 'bot-a', conversationKey: 'direct:1' },
|
||||
},
|
||||
})
|
||||
assert.equal(placed.missingCwd, true)
|
||||
assert.equal(placed.cwd, '')
|
||||
})
|
||||
|
||||
test('adoptSessionIntoWorkspace attaches a fork whose cwd matches a workspace', async () => {
|
||||
const attached = []
|
||||
const workspace = {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
migrateJobOwners,
|
||||
UNASSIGNED_OWNER,
|
||||
viewerPayload,
|
||||
preferredNewJobCwd,
|
||||
} from '../lib/ownership.js'
|
||||
|
||||
test('jobVisibleToIdentity respects canViewAll and owner', () => {
|
||||
|
|
@ -123,3 +124,15 @@ test('claimUnassignedForViewer claims by session owner or user-workspaces cwd',
|
|||
// Unknown session owner: leave unassigned for admin-only visibility.
|
||||
assert.equal(next.jobs[5].ownerEmpNo, UNASSIGNED_OWNER)
|
||||
})
|
||||
|
||||
test('preferredNewJobCwd prefers provisioned viewer workspace over session cwd', () => {
|
||||
assert.equal(preferredNewJobCwd({
|
||||
viewer: { mode: 'multi', workspacePath: '/tmp/user-workspaces/10329667' },
|
||||
sessionCwd: 'D:/code/deepseek-harness/clone',
|
||||
}), '/tmp/user-workspaces/10329667')
|
||||
assert.equal(preferredNewJobCwd({
|
||||
viewer: { mode: 'local', workspacePath: null },
|
||||
sessionCwd: '/tmp/ws-app',
|
||||
}), '/tmp/ws-app')
|
||||
assert.equal(preferredNewJobCwd({}), '')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,6 +40,26 @@ test('createJobRecord defaults mirrorToSession off and accepts explicit true', (
|
|||
)
|
||||
})
|
||||
|
||||
test('createJobRecord fires ASAP when one-shot at is slightly in the past', () => {
|
||||
const now = Date.parse('2026-08-24T01:00:30.000Z')
|
||||
const at = new Date(now - 15_000).toISOString()
|
||||
const job = createJobRecord({
|
||||
name: 'soon',
|
||||
prompt: 'ping',
|
||||
schedule: { kind: 'at', at, timezone: 'UTC' },
|
||||
}, emptyState(), now)
|
||||
assert.equal(job.nextRunAt, now)
|
||||
|
||||
assert.throws(
|
||||
() => createJobRecord({
|
||||
name: 'late',
|
||||
prompt: 'ping',
|
||||
schedule: { kind: 'at', at: new Date(now - 120_000).toISOString(), timezone: 'UTC' },
|
||||
}, emptyState(), now),
|
||||
(error) => error.code === 'INVALID_AT',
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveCreateAgentPreset prefers explicit then peer then session then host default', async () => {
|
||||
assert.equal(
|
||||
await resolveCreateAgentPreset({ agent_preset: 'explicit' }, {}, {}),
|
||||
|
|
|
|||
|
|
@ -139,3 +139,24 @@ test('one-shot at fires once then waits as complete even if nextRunAt is null',
|
|||
assert.equal(second.action, 'wait')
|
||||
assert.equal(second.reason, 'complete')
|
||||
})
|
||||
|
||||
test('one-shot misfire beyond grace skips and clears nextRunAt', () => {
|
||||
const at = shanghai(2026, 8, 23, 22, 57)
|
||||
const job = {
|
||||
id: 'at-miss',
|
||||
enabled: true,
|
||||
schedule: { kind: 'at', at: new Date(at).toISOString(), timezone: TZ },
|
||||
nextRunAt: at,
|
||||
createdAt: at - 60_000,
|
||||
}
|
||||
const decision = decideDispatch({
|
||||
job,
|
||||
runs: [],
|
||||
now: at + 5 * 60_000,
|
||||
misfirePolicy: 'skip',
|
||||
graceMs: 60_000,
|
||||
})
|
||||
assert.equal(decision.action, 'skip')
|
||||
assert.equal(decision.reason, 'misfire')
|
||||
assert.equal(decision.nextRunAt, null)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -260,6 +260,163 @@ test('cron_create stamps ownerEmpNo from user-workspaces cwd', async (t) => {
|
|||
assert.equal(listed.jobs[0].id, created.job.id)
|
||||
})
|
||||
|
||||
test('resolveToolIdentity restores super_admin permissions via udsAuth', () => {
|
||||
const uds = {
|
||||
getSessionOwner(sessionId) {
|
||||
return sessionId === 'sess-super' ? '10329667' : null
|
||||
},
|
||||
resolveIdentityForEmpNo(empNo) {
|
||||
if (empNo !== '10329667') return null
|
||||
return {
|
||||
empNo: '10329667',
|
||||
role: 'super_admin',
|
||||
displayName: 'Super',
|
||||
permissions: { canViewAllSessions: true, canCreateWorkspace: true },
|
||||
workspacePath: '/tmp/deepseek-harness/10329667',
|
||||
}
|
||||
},
|
||||
getProvisionedWorkspacePath(empNo) {
|
||||
return `/tmp/deepseek-harness/${empNo}`
|
||||
},
|
||||
}
|
||||
const identity = resolveToolIdentity(
|
||||
{ agent: { session: { id: 'sess-super', header: { cwd: 'D:/code/gpt' } } } },
|
||||
{ getUdsAuth: () => uds },
|
||||
)
|
||||
assert.equal(identity.empNo, '10329667')
|
||||
assert.equal(identity.role, 'super_admin')
|
||||
assert.equal(identity.permissions.canViewAllSessions, true)
|
||||
})
|
||||
|
||||
test('cron_create keeps foreign cwd for super_admin tool identity', async (t) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-cron-tools-'))
|
||||
t.after(() => rm(dir, { recursive: true, force: true }))
|
||||
const owners = new Map([['sess-super', '10329667']])
|
||||
const uds = {
|
||||
getSessionOwner(id) { return owners.get(id) || null },
|
||||
resolveIdentityForEmpNo(empNo) {
|
||||
if (empNo !== '10329667') {
|
||||
return {
|
||||
empNo,
|
||||
role: 'user',
|
||||
permissions: { canViewAllSessions: false },
|
||||
workspacePath: `/tmp/user-workspaces/${empNo}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
empNo: '10329667',
|
||||
role: 'super_admin',
|
||||
permissions: { canViewAllSessions: true, canCreateWorkspace: true },
|
||||
workspacePath: '/tmp/deepseek-harness/10329667',
|
||||
}
|
||||
},
|
||||
getProvisionedWorkspacePath(empNo) {
|
||||
return empNo === '10329667'
|
||||
? '/tmp/deepseek-harness/10329667'
|
||||
: `/tmp/user-workspaces/${empNo}`
|
||||
},
|
||||
isUserPath(empNo, candidatePath) {
|
||||
const root = this.getProvisionedWorkspacePath(empNo)
|
||||
const cand = String(candidatePath || '').replace(/\\/g, '/')
|
||||
const normRoot = String(root).replace(/\\/g, '/')
|
||||
return cand === normRoot || cand.startsWith(`${normRoot}/`)
|
||||
},
|
||||
}
|
||||
const service = createHostService({
|
||||
filePath: join(dir, 'store.json'),
|
||||
now: () => Date.parse('2026-08-24T01:00:00.000Z'),
|
||||
getUdsAuth: () => uds,
|
||||
sessionPort: {
|
||||
async createAndPrompt() {
|
||||
return { sessionId: 'tool-sess', status: 'succeeded', summary: 'ok' }
|
||||
},
|
||||
},
|
||||
})
|
||||
const tools = byName(cronToolDefinitions(service))
|
||||
const exec = {
|
||||
agent: {
|
||||
session: {
|
||||
id: 'sess-super',
|
||||
header: { cwd: '/tmp/deepseek-harness/10329667' },
|
||||
},
|
||||
},
|
||||
}
|
||||
const created = await tools.cron_create.execute({
|
||||
name: 'gpt-cron',
|
||||
prompt: 'continue BGP',
|
||||
after_minutes: 1,
|
||||
timezone: 'Asia/Shanghai',
|
||||
cwd: 'D:/code/gpt',
|
||||
}, exec)
|
||||
assert.equal(created.job.cwd, 'D:/code/gpt')
|
||||
assert.equal(created.job.ownerEmpNo, '10329667')
|
||||
})
|
||||
|
||||
test('cron_create from IM peer stamps unassigned owner and forces session cwd', async (t) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-cron-tools-'))
|
||||
t.after(() => rm(dir, { recursive: true, force: true }))
|
||||
const peer = {
|
||||
botId: 'bot-a',
|
||||
conversationKey: 'direct:86138@s.whatsapp.net',
|
||||
conversationId: '86138@s.whatsapp.net',
|
||||
kind: 'direct',
|
||||
phone: '86138',
|
||||
}
|
||||
const dshIm = {
|
||||
resolveSessionPeer: async () => peer,
|
||||
listTargets: async () => [{
|
||||
targetId: 'auto-dm-86138',
|
||||
kind: 'user',
|
||||
route: { jid: '86138@s.whatsapp.net' },
|
||||
}],
|
||||
}
|
||||
const uds = {
|
||||
getSessionOwner() { return '10329667' },
|
||||
resolveIdentityForEmpNo(empNo) {
|
||||
return {
|
||||
empNo,
|
||||
role: 'super_admin',
|
||||
permissions: { canViewAllSessions: true, canCreateWorkspace: true },
|
||||
workspacePath: '/tmp/deepseek-harness/10329667',
|
||||
}
|
||||
},
|
||||
getProvisionedWorkspacePath(empNo) {
|
||||
return `/tmp/deepseek-harness/${empNo}`
|
||||
},
|
||||
isUserPath() { return false },
|
||||
}
|
||||
const service = createHostService({
|
||||
filePath: join(dir, 'store.json'),
|
||||
now: () => Date.parse('2026-08-24T01:00:00.000Z'),
|
||||
getUdsAuth: () => uds,
|
||||
getDshIm: () => dshIm,
|
||||
sessionPort: {
|
||||
async createAndPrompt() {
|
||||
return { sessionId: 'tool-sess', status: 'succeeded', summary: 'ok' }
|
||||
},
|
||||
},
|
||||
})
|
||||
const tools = byName(cronToolDefinitions(service, { getDshIm: () => dshIm }))
|
||||
const created = await tools.cron_create.execute({
|
||||
name: 'im-cron',
|
||||
prompt: 'channel work',
|
||||
after_minutes: 2,
|
||||
timezone: 'Asia/Shanghai',
|
||||
cwd: 'D:/code/gpt',
|
||||
}, {
|
||||
agent: {
|
||||
session: {
|
||||
id: 'sess-im',
|
||||
header: { cwd: '/data/bot-ws/wa' },
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.equal(created.job.ownerEmpNo, '__unassigned__')
|
||||
assert.equal(created.job.cwd, '/data/bot-ws/wa')
|
||||
assert.equal(created.job.origin?.kind, 'im')
|
||||
assert.equal(created.job.delivery?.kind, 'im')
|
||||
})
|
||||
|
||||
test('registerCronTools registers each definition and disposer unregisters', () => {
|
||||
const registered = []
|
||||
const ctx = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue