mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 04:13:14 +08:00
Make uds-auth optional: standalone local mode for cron.
Without udsAuth, trusted HTTP uses a synthetic __local__ viewer that sees all jobs; with uds-auth, keep multi-user empNo isolation and admin folders. Document that IM channels are not default UDS accounts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1c504eb1fd
commit
933bad9201
8 changed files with 132 additions and 27 deletions
15
README.md
15
README.md
|
|
@ -17,16 +17,25 @@ dsh plugin --profile web add -w "github:hansjone/dsh-ops-cron"
|
||||||
# restart dsh web
|
# restart dsh web
|
||||||
```
|
```
|
||||||
|
|
||||||
## Multi-user isolation (with uds-auth)
|
## Auth modes (uds-auth is optional)
|
||||||
|
|
||||||
Requires **uds-auth** in the same profile. Jobs carry `ownerEmpNo`; visibility matches workspace ACL:
|
`uds-auth` is a soft dependency. Cron and IM plugins work without it.
|
||||||
|
|
||||||
|
| Mode | When | Behavior |
|
||||||
|
|------|------|----------|
|
||||||
|
| **standalone / local** | `udsAuth` not provided | Trusted local/same-origin HTTP works with synthetic `__local__` identity; all jobs visible; flat sidebar (no user folders); create does not force an empNo |
|
||||||
|
| **multi-user** | `uds-auth` installed and providing `ctx.udsAuth` | Jobs carry `ownerEmpNo`; Web login required; isolation matches workspace ACL |
|
||||||
|
|
||||||
|
### Multi-user isolation (with uds-auth)
|
||||||
|
|
||||||
| Role | Sees |
|
| Role | Sees |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `super_admin` / `fallback_admin` | All users’ jobs (sidebar groups by empNo folder) and their run sessions |
|
| `super_admin` / `fallback_admin` | All users’ jobs (sidebar groups by empNo folder) and their run sessions |
|
||||||
| `admin` / `user` | Only own jobs and runs |
|
| `admin` / `user` | Only own jobs and runs |
|
||||||
|
|
||||||
Create stamps the logged-in empNo. Legacy jobs without owner migrate to `__unassigned__` (super-only). Fire cwd prefers the owner’s provisioned workspace under `user-workspaces/<empNo>`.
|
Create stamps the logged-in empNo. Legacy / unclaimed jobs stay `__unassigned__` (super-only until claimed by session/cwd evidence). Fire cwd prefers the owner’s provisioned workspace under `user-workspaces/<empNo>`.
|
||||||
|
|
||||||
|
**Channel / IM sessions are not a UDS account.** New WhatsApp/IM chats use the bot workspace (`workspaces.json`); they are not mapped to `administrator` or any empNo by default. IM cron jobs stay peer-scoped (or unassigned for Web ACL).
|
||||||
|
|
||||||
## Delivery defaults
|
## Delivery defaults
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
const searchRef = useRef(null)
|
const searchRef = useRef(null)
|
||||||
const needle = query.trim().toLowerCase()
|
const needle = query.trim().toLowerCase()
|
||||||
const canViewAll = !!viewer?.canViewAll
|
const canViewAll = !!viewer?.canViewAll
|
||||||
|
const multiUser = viewer?.mode === 'multi'
|
||||||
const visibleJobs = !needle ? jobs : jobs.filter((job) => {
|
const visibleJobs = !needle ? jobs : jobs.filter((job) => {
|
||||||
const hay = [
|
const hay = [
|
||||||
job.name,
|
job.name,
|
||||||
|
|
@ -910,7 +911,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
h('div', { className: skin.list },
|
h('div', { className: skin.list },
|
||||||
jobs.length === 0 ? h('p', { className: skin.empty }, error || t('emptyJobs'))
|
jobs.length === 0 ? h('p', { className: skin.empty }, error || t('emptyJobs'))
|
||||||
: visibleJobs.length === 0 ? h('p', { className: skin.empty }, t('searchEmpty'))
|
: visibleJobs.length === 0 ? h('p', { className: skin.empty }, t('searchEmpty'))
|
||||||
: (canViewAll ? renderGrouped() : visibleJobs.map((job) => renderJobGroup(job))),
|
: (multiUser && canViewAll ? renderGrouped() : visibleJobs.map((job) => renderJobGroup(job))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -932,7 +933,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
function CwdField({ t, form, setForm, workspaces, viewer }) {
|
function CwdField({ t, form, setForm, workspaces, viewer }) {
|
||||||
const rows = Array.isArray(workspaces) ? workspaces : []
|
const rows = Array.isArray(workspaces) ? workspaces : []
|
||||||
const mode = cwdSelectValue(form.cwd, rows)
|
const mode = cwdSelectValue(form.cwd, rows)
|
||||||
const allowCustom = !!viewer?.canViewAll
|
const allowCustom = viewer?.mode === 'multi' ? !!viewer?.canViewAll : true
|
||||||
return h('label', null, t('cwd'),
|
return h('label', null, t('cwd'),
|
||||||
h('select', {
|
h('select', {
|
||||||
value: mode,
|
value: mode,
|
||||||
|
|
|
||||||
17
lib/host.js
17
lib/host.js
|
|
@ -29,6 +29,8 @@ import {
|
||||||
claimUnassignedForViewer,
|
claimUnassignedForViewer,
|
||||||
filterJobsForIdentity,
|
filterJobsForIdentity,
|
||||||
filterRunsForJobs,
|
filterRunsForJobs,
|
||||||
|
isMultiUserIdentity,
|
||||||
|
localIdentity,
|
||||||
migrateJobOwners,
|
migrateJobOwners,
|
||||||
normalizeOwnerEmpNo,
|
normalizeOwnerEmpNo,
|
||||||
UNASSIGNED_OWNER,
|
UNASSIGNED_OWNER,
|
||||||
|
|
@ -151,16 +153,16 @@ async function requireIdentity(request, write, getUdsAuth) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const uds = typeof getUdsAuth === 'function' ? getUdsAuth() : null
|
const uds = typeof getUdsAuth === 'function' ? getUdsAuth() : null
|
||||||
|
// Soft dep: without uds-auth, run in standalone single-tenant mode.
|
||||||
if (!uds || typeof uds.resolveRequestIdentity !== 'function') {
|
if (!uds || typeof uds.resolveRequestIdentity !== 'function') {
|
||||||
write(503, { ok: false, error: 'auth_unavailable', message: 'uds-auth 未就绪,无法使用定时任务' })
|
return localIdentity()
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
const identity = await resolveBrowserIdentity(request, getUdsAuth)
|
const identity = await resolveBrowserIdentity(request, getUdsAuth)
|
||||||
if (!identity?.empNo) {
|
if (!identity?.empNo) {
|
||||||
write(401, { ok: false, error: 'login_required', message: '登录后才能使用定时任务' })
|
write(401, { ok: false, error: 'login_required', message: '登录后才能使用定时任务' })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return identity
|
return { ...identity, mode: 'multi' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeJobCwd(cwd, identity, getUdsAuth, { forOwnerEmpNo } = {}) {
|
function sanitizeJobCwd(cwd, identity, getUdsAuth, { forOwnerEmpNo } = {}) {
|
||||||
|
|
@ -311,14 +313,17 @@ export function createHostService(options = {}) {
|
||||||
const t = now()
|
const t = now()
|
||||||
// Ignore client-supplied ids on create — otherwise POST/tools can overwrite.
|
// Ignore client-supplied ids on create — otherwise POST/tools can overwrite.
|
||||||
const { id: _ignoredId, ownerEmpNo: _ignoreOwner, ...safeInput } = input && typeof input === 'object' ? input : {}
|
const { id: _ignoredId, ownerEmpNo: _ignoreOwner, ...safeInput } = input && typeof input === 'object' ? input : {}
|
||||||
let ownerIdentity = identity?.empNo ? identity : inferIdentityFromJobInput(safeInput, getUdsAuth)
|
let ownerIdentity = isMultiUserIdentity(identity)
|
||||||
if (ownerIdentity?.empNo) {
|
? identity
|
||||||
|
: inferIdentityFromJobInput(safeInput, getUdsAuth)
|
||||||
|
if (isMultiUserIdentity(ownerIdentity)) {
|
||||||
safeInput.ownerEmpNo = ownerIdentity.empNo
|
safeInput.ownerEmpNo = ownerIdentity.empNo
|
||||||
safeInput.ownerDisplayName = ownerIdentity.displayName || ownerIdentity.empNo
|
safeInput.ownerDisplayName = ownerIdentity.displayName || ownerIdentity.empNo
|
||||||
safeInput.cwd = sanitizeJobCwd(safeInput.cwd, ownerIdentity, getUdsAuth, { forOwnerEmpNo: ownerIdentity.empNo })
|
safeInput.cwd = sanitizeJobCwd(safeInput.cwd, ownerIdentity, getUdsAuth, { forOwnerEmpNo: ownerIdentity.empNo })
|
||||||
} else if (safeInput.ownerEmpNo) {
|
} else if (safeInput.ownerEmpNo) {
|
||||||
safeInput.ownerEmpNo = normalizeOwnerEmpNo(safeInput.ownerEmpNo)
|
safeInput.ownerEmpNo = normalizeOwnerEmpNo(safeInput.ownerEmpNo)
|
||||||
} else {
|
} else {
|
||||||
|
// Standalone / no inferred owner: leave unassigned (visible to everyone in local mode).
|
||||||
safeInput.ownerEmpNo = UNASSIGNED_OWNER
|
safeInput.ownerEmpNo = UNASSIGNED_OWNER
|
||||||
}
|
}
|
||||||
let created
|
let created
|
||||||
|
|
@ -803,7 +808,7 @@ export function createHostService(options = {}) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const code = error && error.code
|
const code = error && error.code
|
||||||
if (code === 'NOT_FOUND') return write(404, { ok: false, error: error.message })
|
if (code === 'NOT_FOUND') return write(404, { ok: false, error: error.message })
|
||||||
if (code === 'LOGIN_REQUIRED') return write(401, { ok: false, error: 'login_required', message: error.message })
|
write(401, { ok: false, error: 'login_required', message: '登录后才能使用定时任务' })
|
||||||
if (code === 'INVALID_CRON' || code === 'INVALID_AT' || code === 'INVALID_SCHEDULE' || code === 'INVALID_JOB' || code === 'INVALID_TIMEZONE' || code === 'INVALID_CWD') {
|
if (code === 'INVALID_CRON' || code === 'INVALID_AT' || code === 'INVALID_SCHEDULE' || code === 'INVALID_JOB' || code === 'INVALID_TIMEZONE' || code === 'INVALID_CWD') {
|
||||||
return write(400, { ok: false, error: error.message, code })
|
return write(400, { ok: false, error: error.message, code })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
lib/index.d.ts
vendored
4
lib/index.d.ts
vendored
|
|
@ -17,6 +17,10 @@ export function createHostService(options?: object): object
|
||||||
export function makeLiveSessionPort(ctx: object): object
|
export function makeLiveSessionPort(ctx: object): object
|
||||||
export function resolveSessionPlacement(ctx: object, job?: object, deps?: object): { cwd: string, workspace: object | null }
|
export function resolveSessionPlacement(ctx: object, job?: object, deps?: object): { cwd: string, workspace: object | null }
|
||||||
export const UNASSIGNED_OWNER: '__unassigned__'
|
export const UNASSIGNED_OWNER: '__unassigned__'
|
||||||
|
export const LOCAL_EMP_NO: '__local__'
|
||||||
|
export function isMultiUserIdentity(identity: object | null | undefined): boolean
|
||||||
|
export function localIdentity(overrides?: object): object
|
||||||
|
export function claimUnassignedForViewer(state: object, identity: object, deps?: object): { state: object, changed: boolean }
|
||||||
export function jobVisibleToIdentity(job: object, identity: object): boolean
|
export function jobVisibleToIdentity(job: object, identity: object): boolean
|
||||||
export function canViewAllJobs(identity: object): boolean
|
export function canViewAllJobs(identity: object): boolean
|
||||||
export function assertCanAccessJob(job: object, identity: object): object
|
export function assertCanAccessJob(job: object, identity: object): object
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,12 @@ export { normalizeJobModel } from './store.js'
|
||||||
export {
|
export {
|
||||||
assertCanAccessJob,
|
assertCanAccessJob,
|
||||||
canViewAllJobs,
|
canViewAllJobs,
|
||||||
|
claimUnassignedForViewer,
|
||||||
filterJobsForIdentity,
|
filterJobsForIdentity,
|
||||||
|
isMultiUserIdentity,
|
||||||
jobVisibleToIdentity,
|
jobVisibleToIdentity,
|
||||||
|
LOCAL_EMP_NO,
|
||||||
|
localIdentity,
|
||||||
migrateJobOwners,
|
migrateJobOwners,
|
||||||
UNASSIGNED_OWNER,
|
UNASSIGNED_OWNER,
|
||||||
viewerPayload,
|
viewerPayload,
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,48 @@
|
||||||
/**
|
/**
|
||||||
* Job ownership helpers for per-user cron isolation (aligns with uds-auth roles).
|
* Job ownership helpers for per-user cron isolation (aligns with uds-auth roles).
|
||||||
|
*
|
||||||
|
* Two modes:
|
||||||
|
* - standalone (no uds-auth): identity empNo is LOCAL_EMP_NO → everyone sees all jobs
|
||||||
|
* - multi-user (uds-auth present): filter/claim by real empNo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const UNASSIGNED_OWNER = '__unassigned__'
|
export const UNASSIGNED_OWNER = '__unassigned__'
|
||||||
|
export const LOCAL_EMP_NO = '__local__'
|
||||||
|
|
||||||
export function normalizeOwnerEmpNo(value) {
|
export function normalizeOwnerEmpNo(value) {
|
||||||
const empNo = String(value || '').trim()
|
const empNo = String(value || '').trim()
|
||||||
return empNo || UNASSIGNED_OWNER
|
return empNo || UNASSIGNED_OWNER
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canViewAllJobs(identity) {
|
/** True when uds-auth supplied a real user identity (not standalone local). */
|
||||||
return !!identity?.permissions?.canViewAllSessions
|
export function isMultiUserIdentity(identity) {
|
||||||
|
const empNo = String(identity?.empNo || '').trim()
|
||||||
|
return !!empNo && empNo !== LOCAL_EMP_NO
|
||||||
}
|
}
|
||||||
|
|
||||||
function empNoFromUserWorkspacePath(cwd) {
|
export function localIdentity(overrides = {}) {
|
||||||
const norm = String(cwd || '').replace(/\\/g, '/')
|
return {
|
||||||
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
|
empNo: LOCAL_EMP_NO,
|
||||||
return match ? decodeURIComponent(match[1]) : null
|
role: 'local',
|
||||||
|
displayName: 'local',
|
||||||
|
permissions: { canViewAllSessions: true },
|
||||||
|
workspacePath: null,
|
||||||
|
mode: 'local',
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canViewAllJobs(identity) {
|
||||||
|
if (!isMultiUserIdentity(identity)) return true
|
||||||
|
return !!identity?.permissions?.canViewAllSessions
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jobVisibleToIdentity(job, identity) {
|
export function jobVisibleToIdentity(job, identity) {
|
||||||
if (!identity?.empNo) return false
|
if (!identity?.empNo) return false
|
||||||
if (canViewAllJobs(identity)) return true
|
// Standalone / local: no empNo isolation.
|
||||||
|
if (!isMultiUserIdentity(identity) || canViewAllJobs(identity)) return true
|
||||||
const owner = normalizeOwnerEmpNo(job?.ownerEmpNo)
|
const owner = normalizeOwnerEmpNo(job?.ownerEmpNo)
|
||||||
// Unclaimed jobs are admin-only (super_admin / fallback_admin via canViewAll).
|
// Unclaimed jobs are admin-only in multi-user mode.
|
||||||
if (owner === UNASSIGNED_OWNER) return false
|
if (owner === UNASSIGNED_OWNER) return false
|
||||||
return owner === String(identity.empNo)
|
return owner === String(identity.empNo)
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +69,7 @@ export function assertCanAccessJob(job, identity) {
|
||||||
export function filterJobsForIdentity(jobs, identity) {
|
export function filterJobsForIdentity(jobs, identity) {
|
||||||
const list = Array.isArray(jobs) ? jobs : []
|
const list = Array.isArray(jobs) ? jobs : []
|
||||||
if (!identity?.empNo) return []
|
if (!identity?.empNo) return []
|
||||||
if (canViewAllJobs(identity)) return [...list]
|
if (!isMultiUserIdentity(identity) || canViewAllJobs(identity)) return [...list]
|
||||||
return list.filter((job) => jobVisibleToIdentity(job, identity))
|
return list.filter((job) => jobVisibleToIdentity(job, identity))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,21 +128,23 @@ export function migrateJobOwners(state, deps = {}) {
|
||||||
|
|
||||||
export function viewerPayload(identity) {
|
export function viewerPayload(identity) {
|
||||||
if (!identity?.empNo) return null
|
if (!identity?.empNo) return null
|
||||||
|
const multi = isMultiUserIdentity(identity)
|
||||||
return {
|
return {
|
||||||
empNo: identity.empNo,
|
empNo: identity.empNo,
|
||||||
role: identity.role || 'user',
|
role: identity.role || (multi ? 'user' : 'local'),
|
||||||
displayName: identity.displayName || identity.empNo,
|
displayName: identity.displayName || identity.empNo,
|
||||||
canViewAll: canViewAllJobs(identity),
|
canViewAll: canViewAllJobs(identity),
|
||||||
workspacePath: identity.workspacePath || null,
|
workspacePath: identity.workspacePath || null,
|
||||||
|
mode: multi ? 'multi' : 'local',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claim unassigned jobs that clearly belong to the viewer (origin session / cwd).
|
* Claim unassigned jobs that clearly belong to the viewer (origin session / cwd).
|
||||||
* Returns { state, changed }.
|
* No-op in standalone / canViewAll. Returns { state, changed }.
|
||||||
*/
|
*/
|
||||||
export function claimUnassignedForViewer(state, identity, deps = {}) {
|
export function claimUnassignedForViewer(state, identity, deps = {}) {
|
||||||
if (!identity?.empNo || canViewAllJobs(identity)) {
|
if (!isMultiUserIdentity(identity) || canViewAllJobs(identity)) {
|
||||||
return { state, changed: false }
|
return { state, changed: false }
|
||||||
}
|
}
|
||||||
const jobs = Array.isArray(state?.jobs) ? state.jobs : []
|
const jobs = Array.isArray(state?.jobs) ? state.jobs : []
|
||||||
|
|
@ -134,14 +155,14 @@ export function claimUnassignedForViewer(state, identity, deps = {}) {
|
||||||
? job.origin.sessionId.trim()
|
? job.origin.sessionId.trim()
|
||||||
: ''
|
: ''
|
||||||
let mine = false
|
let mine = false
|
||||||
// Only claim when evidence ties the job to this viewer. Remaining unassigned
|
// Only claim when evidence ties the job to this viewer.
|
||||||
// jobs stay admin-only until a super/fallback admin reassigns them.
|
|
||||||
if (sessionId && typeof deps.getSessionOwner === 'function') {
|
if (sessionId && typeof deps.getSessionOwner === 'function') {
|
||||||
mine = deps.getSessionOwner(sessionId) === identity.empNo
|
mine = deps.getSessionOwner(sessionId) === identity.empNo
|
||||||
}
|
}
|
||||||
if (!mine && job.cwd) {
|
if (!mine && job.cwd) {
|
||||||
const pathOwner = empNoFromUserWorkspacePath(job.cwd)
|
const norm = String(job.cwd).replace(/\\/g, '/')
|
||||||
if (pathOwner === identity.empNo) mine = true
|
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
|
||||||
|
mine = !!(match && decodeURIComponent(match[1]) === identity.empNo)
|
||||||
}
|
}
|
||||||
if (!mine) return job
|
if (!mine) return job
|
||||||
changed = true
|
changed = true
|
||||||
|
|
|
||||||
|
|
@ -916,6 +916,47 @@ test('GET /jobs claims unassigned jobs that match viewer cwd', async (t) => {
|
||||||
assert.equal(listed.body.jobs[0].ownerEmpNo, 'tester')
|
assert.equal(listed.body.jobs[0].ownerEmpNo, 'tester')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('HTTP works standalone without uds-auth (local mode)', async (t) => {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-ops-cron-local-'))
|
||||||
|
t.after(() => rm(dir, { recursive: true, force: true }))
|
||||||
|
const service = createHostService({
|
||||||
|
filePath: join(dir, 'store.json'),
|
||||||
|
now: () => Date.parse('2026-08-24T01:00:00.000Z'),
|
||||||
|
sessionPort: {
|
||||||
|
async createAndPrompt() {
|
||||||
|
return { sessionId: 'local-sess', status: 'succeeded', summary: 'ok' }
|
||||||
|
},
|
||||||
|
async archiveSession() {},
|
||||||
|
},
|
||||||
|
getUdsAuth: () => undefined,
|
||||||
|
})
|
||||||
|
const http = await listen(service)
|
||||||
|
t.after(() => http.close())
|
||||||
|
|
||||||
|
const created = await jsonRequest(http.url, '/dsh-ops-cron/jobs', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'local job',
|
||||||
|
prompt: 'ping',
|
||||||
|
schedule: { kind: 'cron', expr: '0 9 * * *', timezone: 'UTC' },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
assert.equal(created.status, 200)
|
||||||
|
assert.equal(created.body.job.name, 'local job')
|
||||||
|
assert.ok(!created.body.job.ownerEmpNo || created.body.job.ownerEmpNo === '__unassigned__')
|
||||||
|
|
||||||
|
const listed = await jsonRequest(http.url, '/dsh-ops-cron/jobs')
|
||||||
|
assert.equal(listed.status, 200)
|
||||||
|
assert.equal(listed.body.viewer.mode, 'local')
|
||||||
|
assert.equal(listed.body.viewer.canViewAll, true)
|
||||||
|
assert.equal(listed.body.jobs.length, 1)
|
||||||
|
|
||||||
|
const ran = await jsonRequest(http.url, `/dsh-ops-cron/jobs/${created.body.job.id}/run`, { method: 'POST' })
|
||||||
|
assert.equal(ran.status, 200)
|
||||||
|
assert.equal(ran.body.run.sessionId, 'local-sess')
|
||||||
|
})
|
||||||
|
|
||||||
test('migrateJobOwners assigns unassigned for legacy jobs', async () => {
|
test('migrateJobOwners assigns unassigned for legacy jobs', async () => {
|
||||||
const { migrateJobOwners, UNASSIGNED_OWNER } = await import('../lib/ownership.js')
|
const { migrateJobOwners, UNASSIGNED_OWNER } = await import('../lib/ownership.js')
|
||||||
const state = {
|
const state = {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import {
|
||||||
filterJobsForIdentity,
|
filterJobsForIdentity,
|
||||||
filterRunsForJobs,
|
filterRunsForJobs,
|
||||||
inferOwnerEmpNo,
|
inferOwnerEmpNo,
|
||||||
|
isMultiUserIdentity,
|
||||||
jobVisibleToIdentity,
|
jobVisibleToIdentity,
|
||||||
|
LOCAL_EMP_NO,
|
||||||
|
localIdentity,
|
||||||
migrateJobOwners,
|
migrateJobOwners,
|
||||||
UNASSIGNED_OWNER,
|
UNASSIGNED_OWNER,
|
||||||
viewerPayload,
|
viewerPayload,
|
||||||
|
|
@ -73,6 +76,23 @@ test('viewerPayload', () => {
|
||||||
assert.equal(v.empNo, 'u1')
|
assert.equal(v.empNo, 'u1')
|
||||||
assert.equal(v.canViewAll, false)
|
assert.equal(v.canViewAll, false)
|
||||||
assert.equal(v.workspacePath, '/w/u1')
|
assert.equal(v.workspacePath, '/w/u1')
|
||||||
|
assert.equal(v.mode, 'multi')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('standalone local identity sees all jobs including unassigned', () => {
|
||||||
|
const local = localIdentity()
|
||||||
|
assert.equal(isMultiUserIdentity(local), false)
|
||||||
|
assert.equal(local.empNo, LOCAL_EMP_NO)
|
||||||
|
assert.equal(canViewAllJobs(local), true)
|
||||||
|
assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, local), true)
|
||||||
|
assert.equal(jobVisibleToIdentity({ ownerEmpNo: 'u2' }, local), true)
|
||||||
|
const payload = viewerPayload(local)
|
||||||
|
assert.equal(payload.mode, 'local')
|
||||||
|
assert.equal(payload.canViewAll, true)
|
||||||
|
assert.deepEqual(
|
||||||
|
filterJobsForIdentity([{ id: '1', ownerEmpNo: 'u1' }, { id: '2', ownerEmpNo: UNASSIGNED_OWNER }], local).map((j) => j.id),
|
||||||
|
['1', '2'],
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('claimUnassignedForViewer claims by session owner or user-workspaces cwd', () => {
|
test('claimUnassignedForViewer claims by session owner or user-workspaces cwd', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue