Files
dsh-superagent/tests/reclaim.spec.ts
T

70 lines
4.5 KiB
TypeScript

// Reclaiming the directories of Ended Tickets: what may go, and what the readout counts.
import { resolve } from 'node:path'
import { expect, it } from 'vitest'
import { reclaimable, within } from '../src/app/reclaim.ts'
import type { Ticket } from '../src/core/index.ts'
const DAY = 86_400_000
const root = '/srv/dsh-superagent/work'
const now = Date.parse('2026-10-31T00:00:00.000Z')
const at = (daysAgo: number) => new Date(now - daysAgo * DAY).toISOString()
/** A Ticket as the reclaimer reads it: an id, its workspace, and when it ended. */
function ticket(id: string, name: string, ended: string | undefined, directory = `${root}/${name}`): Ticket {
return { id, title: id, decisionId: 'd', channels: [], undelivered: [], createdAt: at(30), updatedAt: at(30),
mandate: { strength: 'explicit', policyVersion: 'v', basis: { via: 'listed' }, actor: { source: 'feishu', id: 'ou_alice' }, scope: '' },
origin: { triggerId: 'e1', triggerIds: ['e1'] }, assignments: [], inputs: [],
...ended ? { ended: { at: ended, by: 'executor' as const, outcome: 'completed' as const } } : {},
execution: { name, directory, assignment: '' } }
}
it('tells a path inside the execution root from one beside or above it', () => {
expect(within(root, `${root}/create-wiki-06e6e4ae`)).toBe(true)
expect(within(root, `${root}/a/b/c`)).toBe(true)
// A sibling whose name starts with the root's, a parent, and the root itself are not inside it.
expect(within(root, `${root}-other/ticket`)).toBe(false)
expect(within(root, '/srv/dsh-superagent')).toBe(false)
expect(within(root, root)).toBe(false)
expect(within(root, '/elsewhere/ticket')).toBe(false)
})
it('reclaims an Ended Ticket\'s own directory once it outlives the retention, and no other', () => {
const tickets = [ticket('t1', 'create-wiki-06e6e4ae', at(20)), ticket('t2', 'create-hi-7aa4cedc44b4', at(2))]
expect(reclaimable(tickets, root, now, 14).map(item => item.ticketId)).toEqual(['t1'])
// A Ticket still open holds its directory however old it is.
expect(reclaimable([ticket('t1', 'create-wiki-06e6e4ae', undefined)], root, now, 14)).toEqual([])
// A retention of zero keeps everything, which is how an operator opts out entirely.
expect(reclaimable(tickets, root, now, 0)).toEqual([])
// The retention is a whole number of days, so a Ticket ended a moment ago waits.
expect(reclaimable([ticket('t1', 'create-wiki-06e6e4ae', at(13))], root, now, 14)).toEqual([])
})
it('never reclaims a directory the Executor did not name under its root', () => {
// An operator's own directory outside the root: the Executor recorded it, but removing it would reach past the tree.
expect(reclaimable([ticket('t1', 'my-checkout', at(20), '/home/me/my-checkout')], root, now, 14)).toEqual([])
// A root that merely shares a prefix with the directory is not the parent of it.
expect(reclaimable([ticket('t1', 'ticket', at(20), `${root}-other/ticket`)], root, now, 14)).toEqual([])
// A directory the Executor recorded under a different name than the Ticket's own is not one it made.
expect(reclaimable([ticket('t1', 'create-wiki-06e6e4ae', at(20), `${root}/something-else`)], root, now, 14)).toEqual([])
// A Ticket that never got a directory, such as one never accepted, holds nothing.
const bare: Ticket = { ...ticket('t1', 'x', at(20)), execution: undefined }
expect(reclaimable([bare], root, now, 14)).toEqual([])
})
it('leaves a directory that an open Ticket uses, and lists each directory once', () => {
// A Ticket that ended is not removed while a newer open Ticket works in the same directory.
const shared = `${root}/create-wiki-06e6e4ae`
const tickets = [ticket('t1', 'create-wiki-06e6e4ae', at(20), shared), ticket('t2', 'create-wiki-06e6e4ae', undefined, shared)]
expect(reclaimable(tickets, root, now, 14)).toEqual([])
// Two ended Tickets naming the same directory reclaim it once, not twice.
const twice = [ticket('t1', 'create-wiki-06e6e4ae', at(20), shared), ticket('t2', 'create-wiki-06e6e4ae', at(25), shared)]
// Paths come back resolved, so the expectation is resolved too rather than spelled with the platform's separator.
expect(reclaimable(twice, root, now, 14)).toEqual([{ ticketId: 't1', directory: resolve(shared) }])
// Either order reclaims it once, named after whichever Ticket the list reaches first.
expect(reclaimable([...twice].reverse(), root, now, 14)).toEqual([{ ticketId: 't2', directory: resolve(shared) }])
})
it('ignores an Ended Ticket whose end time cannot be read', () => {
expect(reclaimable([ticket('t1', 'create-wiki-06e6e4ae', 'not a time')], root, now, 14)).toEqual([])
})