diff --git a/lib/ownership.js b/lib/ownership.js index b4267ef..e5baf1b 100644 --- a/lib/ownership.js +++ b/lib/ownership.js @@ -23,14 +23,8 @@ export function jobVisibleToIdentity(job, identity) { if (!identity?.empNo) return false if (canViewAllJobs(identity)) return true const owner = normalizeOwnerEmpNo(job?.ownerEmpNo) - // Legacy / tool-created rows often lack ownerEmpNo. Keep them visible so the - // sidebar is not empty, then claimUnassignedForViewer can adopt them — unless - // the cwd clearly belongs to another user. - if (owner === UNASSIGNED_OWNER) { - const pathOwner = empNoFromUserWorkspacePath(job?.cwd) - if (pathOwner && pathOwner !== String(identity.empNo)) return false - return true - } + // Unclaimed jobs are admin-only (super_admin / fallback_admin via canViewAll). + if (owner === UNASSIGNED_OWNER) return false return owner === String(identity.empNo) } @@ -140,23 +134,14 @@ export function claimUnassignedForViewer(state, identity, deps = {}) { ? job.origin.sessionId.trim() : '' let mine = false - // Prefer session ownership / user-workspaces cwd. Adopt web/legacy orphans when - // no other owner is known so chat-created jobs reappear in the sidebar. + // Only claim when evidence ties the job to this viewer. Remaining unassigned + // jobs stay admin-only until a super/fallback admin reassigns them. if (sessionId && typeof deps.getSessionOwner === 'function') { - const sessionOwner = deps.getSessionOwner(sessionId) - if (sessionOwner && sessionOwner !== identity.empNo) return job - mine = !sessionOwner || sessionOwner === identity.empNo + mine = deps.getSessionOwner(sessionId) === identity.empNo } if (!mine && job.cwd) { - const norm = String(job.cwd).replace(/\\/g, '/') - const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/) - if (match) { - if (decodeURIComponent(match[1]) === identity.empNo) mine = true - else return job - } - } - if (!mine && job.origin?.kind !== 'im') { - mine = true + const pathOwner = empNoFromUserWorkspacePath(job.cwd) + if (pathOwner === identity.empNo) mine = true } if (!mine) return job changed = true diff --git a/test/ownership.test.js b/test/ownership.test.js index 4285544..990fb17 100644 --- a/test/ownership.test.js +++ b/test/ownership.test.js @@ -18,8 +18,8 @@ test('jobVisibleToIdentity respects canViewAll and owner', () => { const admin = { empNo: 'a1', permissions: { canViewAllSessions: true } } assert.equal(jobVisibleToIdentity({ ownerEmpNo: 'u1' }, user), true) assert.equal(jobVisibleToIdentity({ ownerEmpNo: 'u2' }, user), false) - assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, user), true) - assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u2/x' }, user), false) + assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, user), false) + assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u1/x' }, user), false) assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, admin), true) assert.equal(canViewAllJobs(admin), true) }) @@ -84,7 +84,7 @@ test('claimUnassignedForViewer claims by session owner or user-workspaces cwd', { id: 'c', ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u2/proj' }, { id: 'd', ownerEmpNo: 'u2', cwd: '/data/user-workspaces/u1/x' }, { id: 'e', ownerEmpNo: UNASSIGNED_OWNER, origin: { kind: 'im', peer: { botId: 'b' } } }, - { id: 'f', ownerEmpNo: UNASSIGNED_OWNER, origin: { kind: 'web', sessionId: 'foreign' } }, + { id: 'f', ownerEmpNo: UNASSIGNED_OWNER, origin: { kind: 'web', sessionId: 'orphan' } }, ], } const { changed, state: next } = claimUnassignedForViewer(state, user, { @@ -97,9 +97,9 @@ test('claimUnassignedForViewer claims by session owner or user-workspaces cwd', assert.equal(changed, true) assert.equal(next.jobs[0].ownerEmpNo, 'u1') assert.equal(next.jobs[1].ownerEmpNo, 'u1') - // Foreign user-workspaces path must not be stolen. assert.equal(next.jobs[2].ownerEmpNo, UNASSIGNED_OWNER) assert.equal(next.jobs[3].ownerEmpNo, 'u2') assert.equal(next.jobs[4].ownerEmpNo, UNASSIGNED_OWNER) + // Unknown session owner: leave unassigned for admin-only visibility. assert.equal(next.jobs[5].ownerEmpNo, UNASSIGNED_OWNER) })