mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
126 lines
5.1 KiB
TypeScript
126 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { isSafePtySessionId, mintPtySessionId, parsePtySessionId } from './pty-session-id'
|
|
|
|
const USER_DATA = '/tmp/orca-userdata'
|
|
|
|
describe('mintPtySessionId', () => {
|
|
it('returns a UUID when no worktreeId is provided', () => {
|
|
const id = mintPtySessionId()
|
|
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
|
|
})
|
|
|
|
it('prefixes the worktreeId and suffixes an 8-char hex tag', () => {
|
|
const id = mintPtySessionId('wt-alpha')
|
|
expect(id).toMatch(/^wt-alpha@@[0-9a-f]{8}$/)
|
|
})
|
|
|
|
it('preserves path-shaped worktreeIds verbatim in the prefix', () => {
|
|
// Why: real worktreeIds are `${repo.id}::${absolutePath}` and contain
|
|
// slashes. The mint must not rewrite or sanitize them — reconcileOnStartup
|
|
// splits on `@@` to recover the worktreeId.
|
|
const id = mintPtySessionId('repo-123::/Users/me/work/wt-1')
|
|
expect(id).toMatch(/^repo-123::\/Users\/me\/work\/wt-1@@[0-9a-f]{8}$/)
|
|
})
|
|
})
|
|
|
|
describe('isSafePtySessionId', () => {
|
|
it('accepts minted UUIDs', () => {
|
|
expect(isSafePtySessionId(mintPtySessionId(), USER_DATA)).toBe(true)
|
|
})
|
|
|
|
it('accepts minted worktree-scoped ids (happy path, hyphen-only)', () => {
|
|
expect(isSafePtySessionId(mintPtySessionId('wt-alpha'), USER_DATA)).toBe(true)
|
|
})
|
|
|
|
it('accepts minted ids with path-shaped worktreeIds containing slashes', () => {
|
|
// Why: real worktreeIds are `${repo.id}::${absolutePath}`, so the minted
|
|
// sessionId contains `/` in its prefix. A char-denylist validator that
|
|
// rejected `/` would break every real daemon spawn.
|
|
const id = mintPtySessionId('repo-abc123::/Users/thebr/work/wt-1')
|
|
expect(isSafePtySessionId(id, USER_DATA)).toBe(true)
|
|
})
|
|
|
|
it('accepts caller-supplied path-shaped ids that stay inside userData', () => {
|
|
expect(isSafePtySessionId('some-repo::/Users/me/wt/abc@@deadbeef', USER_DATA)).toBe(true)
|
|
})
|
|
|
|
it('rejects empty string', () => {
|
|
expect(isSafePtySessionId('', USER_DATA)).toBe(false)
|
|
})
|
|
|
|
it('rejects ids longer than 512 characters', () => {
|
|
expect(isSafePtySessionId('a'.repeat(513), USER_DATA)).toBe(false)
|
|
})
|
|
|
|
it('rejects ids containing a NUL byte', () => {
|
|
expect(isSafePtySessionId('safe\0evil', USER_DATA)).toBe(false)
|
|
})
|
|
|
|
it('rejects ids that traverse out of userData via ..', () => {
|
|
expect(isSafePtySessionId('../etc/passwd', USER_DATA)).toBe(false)
|
|
})
|
|
|
|
it('rejects ids that traverse out of userData via deep ..', () => {
|
|
expect(isSafePtySessionId('sub/../../etc/passwd', USER_DATA)).toBe(false)
|
|
})
|
|
|
|
it('rejects ids that resolve to the userData root itself', () => {
|
|
// Why: if id resolves to `.` (the root), callers could overwrite userData
|
|
// meta files. Guard ensures the target is a strict subpath.
|
|
expect(isSafePtySessionId('.', USER_DATA)).toBe(false)
|
|
})
|
|
|
|
it('accepts ids with nested valid path segments inside userData', () => {
|
|
// Why: minted ids can contain `/` (from worktreeId::absolute-path) so the
|
|
// validator must allow that as long as the result stays inside userData.
|
|
expect(isSafePtySessionId('sub/path/ok@@12345678', USER_DATA)).toBe(true)
|
|
})
|
|
|
|
it('accepts ids with path segments that merely start with dot-dot', () => {
|
|
expect(isSafePtySessionId('..sessions/ok@@12345678', USER_DATA)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('parsePtySessionId', () => {
|
|
it('round-trips a minted id back to its worktreeId', () => {
|
|
const wt = 'repo-abc::/Users/me/wt/feature'
|
|
expect(parsePtySessionId(mintPtySessionId(wt))).toEqual({ worktreeId: wt })
|
|
})
|
|
|
|
it('rejects bare UUIDs (no @@)', () => {
|
|
expect(parsePtySessionId(mintPtySessionId())).toEqual({ worktreeId: null })
|
|
})
|
|
|
|
it('rejects ids with @@ but no `::` worktree shape', () => {
|
|
// Why: callers use the returned worktreeId as a memory-attribution key.
|
|
// A non-minted id like `wt-only@@abcd1234` would synthesize a bogus
|
|
// worktreeId; require the canonical `${repoId}::${path}` shape.
|
|
expect(parsePtySessionId('wt-only@@abcd1234')).toEqual({ worktreeId: null })
|
|
})
|
|
|
|
it('returns null for an empty string', () => {
|
|
expect(parsePtySessionId('')).toEqual({ worktreeId: null })
|
|
})
|
|
|
|
it('handles worktreeIds whose path contains @ characters', () => {
|
|
// Why: the parser uses lastIndexOf('@@') so `@`-containing paths still
|
|
// round-trip cleanly as long as `@@` only appears as the separator.
|
|
const wt = 'repo::/Users/me/email@host/wt'
|
|
expect(parsePtySessionId(`${wt}@@deadbeef`)).toEqual({ worktreeId: wt })
|
|
})
|
|
|
|
it('rejects degenerate `::@@…` ids with empty repo and path halves', () => {
|
|
// Why: `String.includes('::')` would accept '::' as a worktreeId.
|
|
// Memory-attribution callers must not bucket sessions under an empty key.
|
|
expect(parsePtySessionId('::@@deadbeef')).toEqual({ worktreeId: null })
|
|
})
|
|
|
|
it('rejects ids with an empty path half (`repo::@@…`)', () => {
|
|
expect(parsePtySessionId('repo::@@deadbeef')).toEqual({ worktreeId: null })
|
|
})
|
|
|
|
it('rejects ids with an empty repoId half (`::path@@…`)', () => {
|
|
expect(parsePtySessionId('::path@@deadbeef')).toEqual({ worktreeId: null })
|
|
})
|
|
})
|