GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Account for Electron-as-Node when probing the current executable and update the Linux workspace registry scenario to validate the writable handle behavior shipped with remote file management.
419 lines
13 KiB
TypeScript
419 lines
13 KiB
TypeScript
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { ControllerRegistry } from './controller-registry'
|
|
import { WorkspaceRegistry } from './workspace-registry'
|
|
|
|
const temporaryPaths: string[] = []
|
|
const linuxIt = process.platform === 'linux' ? it : it.skip
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
for (const path of temporaryPaths.splice(0)) {
|
|
rmSync(path, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe('WorkspaceRegistry', () => {
|
|
linuxIt('binds ephemeral handles to owner and connection generation', async () => {
|
|
const root = temporaryDirectory()
|
|
const controllers = new ControllerRegistry()
|
|
const owner = controllers.attach('controller-owner')
|
|
const other = controllers.attach('controller-other')
|
|
const registry = workspaceRegistry(controllers)
|
|
const validated = await validate(registry, root, owner)
|
|
|
|
await expect(
|
|
registry.get(
|
|
validated.handle.workspaceId,
|
|
validated.handle.generation,
|
|
other
|
|
)
|
|
).rejects.toMatchObject({ code: 'not-owner' })
|
|
|
|
controllers.disconnect(owner.controllerId, owner.generation)
|
|
const replacement = controllers.attach(owner.controllerId)
|
|
await expect(
|
|
registry.resume(
|
|
{
|
|
workspaceId: validated.handle.workspaceId,
|
|
generation: validated.handle.generation,
|
|
workspaceIdentity: validated.handle.workspaceIdentity
|
|
},
|
|
replacement
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
await expect(
|
|
registry.open(
|
|
{
|
|
workspaceIdentity: validated.handle.workspaceIdentity,
|
|
requestedAccess: 'read-only'
|
|
},
|
|
replacement
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
})
|
|
|
|
linuxIt('resolves only a current Workspace lease owned by the controller', async () => {
|
|
const root = temporaryDirectory()
|
|
const controllers = new ControllerRegistry()
|
|
const owner = controllers.attach('controller-owner')
|
|
const other = controllers.attach('controller-other')
|
|
const registry = workspaceRegistry(controllers)
|
|
const validated = await validate(registry, root, owner)
|
|
|
|
await expect(
|
|
registry.getCurrentByIdentity(
|
|
validated.handle.workspaceIdentity,
|
|
owner
|
|
)
|
|
).resolves.toMatchObject({
|
|
controllerId: owner.controllerId,
|
|
controllerGeneration: owner.generation,
|
|
handle: {
|
|
workspaceIdentity: validated.handle.workspaceIdentity
|
|
}
|
|
})
|
|
await expect(
|
|
registry.getCurrentByIdentity(
|
|
validated.handle.workspaceIdentity,
|
|
other
|
|
)
|
|
).rejects.toMatchObject({ code: 'not-owner' })
|
|
|
|
controllers.disconnect(owner.controllerId, owner.generation)
|
|
const replacement = controllers.attach(owner.controllerId)
|
|
await expect(
|
|
registry.getCurrentByIdentity(
|
|
validated.handle.workspaceIdentity,
|
|
replacement
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
})
|
|
|
|
linuxIt('revokes a validation grant and all handles opened from it', async () => {
|
|
const root = temporaryDirectory()
|
|
const controllers = new ControllerRegistry()
|
|
const controller = controllers.attach('controller-owner')
|
|
const registry = workspaceRegistry(controllers)
|
|
const validated = await validate(registry, root, controller)
|
|
const opened = await registry.open(
|
|
{
|
|
workspaceIdentity: validated.handle.workspaceIdentity,
|
|
requestedAccess: 'read-only'
|
|
},
|
|
controller
|
|
)
|
|
|
|
await expect(
|
|
registry.close(
|
|
validated.handle.workspaceId,
|
|
validated.handle.generation,
|
|
controller
|
|
)
|
|
).resolves.toMatchObject({ closed: true })
|
|
expect(registry.activeHandleCount()).toBe(0)
|
|
await expect(
|
|
registry.get(opened.workspaceId, opened.generation, controller)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
await expect(
|
|
registry.open(
|
|
{
|
|
workspaceIdentity: validated.handle.workspaceIdentity,
|
|
requestedAccess: 'read-only'
|
|
},
|
|
controller
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
})
|
|
|
|
linuxIt('supports writable handles while enforcing caps and symlink-root rejection', async () => {
|
|
const root = temporaryDirectory()
|
|
const linked = join(tmpdir(), `goodbuddy-linked-${Date.now()}`)
|
|
symlinkSync(root, linked, 'dir')
|
|
temporaryPaths.push(linked)
|
|
const controllers = new ControllerRegistry()
|
|
const controller = controllers.attach('controller-owner')
|
|
const registry = new WorkspaceRegistry({
|
|
controllers,
|
|
maximumControllerHandles: 1,
|
|
inspectGit: async () => 'not-a-repository'
|
|
})
|
|
const writable = await registry.validate(
|
|
{
|
|
remoteRootPath: root,
|
|
requestedAccess: 'read-write',
|
|
requiredCapabilities: []
|
|
},
|
|
controller
|
|
)
|
|
expect(writable.handle.access).toBe('read-write')
|
|
await registry.close(
|
|
writable.handle.workspaceId,
|
|
writable.handle.generation,
|
|
controller
|
|
)
|
|
await expect(
|
|
registry.validate(
|
|
{
|
|
remoteRootPath: linked,
|
|
requestedAccess: 'read-only',
|
|
requiredCapabilities: []
|
|
},
|
|
controller
|
|
)
|
|
).rejects.toMatchObject({ code: 'symlink-rejected' })
|
|
|
|
const validated = await validate(registry, root, controller)
|
|
await expect(
|
|
registry.open(
|
|
{
|
|
workspaceIdentity: validated.handle.workspaceIdentity,
|
|
requestedAccess: 'read-only'
|
|
},
|
|
controller
|
|
)
|
|
).rejects.toMatchObject({ code: 'capacity-exceeded' })
|
|
})
|
|
|
|
linuxIt('does not mutate workspace or registry storage during validation', async () => {
|
|
const state = temporaryDirectory()
|
|
const root = join(state, 'workspace')
|
|
mkdirSync(root)
|
|
writeFileSync(join(root, 'existing.txt'), 'unchanged')
|
|
const before = readdirSync(root)
|
|
const storagePath = join(state, 'workspaces.json')
|
|
const controllers = new ControllerRegistry()
|
|
const controller = controllers.attach('controller-owner')
|
|
const registry = new WorkspaceRegistry({
|
|
controllers,
|
|
storagePath,
|
|
inspectGit: async () => 'not-a-repository'
|
|
})
|
|
|
|
const validated = await validate(registry, root, controller)
|
|
expect(existsSync(storagePath)).toBe(false)
|
|
expect(readdirSync(root)).toEqual(before)
|
|
expect(readFileSync(join(root, 'existing.txt'), 'utf8')).toBe(
|
|
'unchanged'
|
|
)
|
|
|
|
await registry.close(
|
|
validated.handle.workspaceId,
|
|
validated.handle.generation,
|
|
controller
|
|
)
|
|
expect(existsSync(storagePath)).toBe(false)
|
|
})
|
|
|
|
linuxIt('leaves no grant when validation is cancelled', async () => {
|
|
const state = temporaryDirectory()
|
|
const root = join(state, 'workspace')
|
|
mkdirSync(root)
|
|
const storagePath = join(state, 'workspaces.json')
|
|
const controllers = new ControllerRegistry()
|
|
const controller = controllers.attach('controller-owner')
|
|
const abort = new AbortController()
|
|
let releaseInspection: ((value: 'not-a-repository') => void) | undefined
|
|
let markInspectionStarted: (() => void) | undefined
|
|
const inspectionStarted = new Promise<void>((resolve) => {
|
|
markInspectionStarted = resolve
|
|
})
|
|
const registry = new WorkspaceRegistry({
|
|
controllers,
|
|
storagePath,
|
|
inspectGit: async () => {
|
|
markInspectionStarted?.()
|
|
return await new Promise<'not-a-repository'>((resolve) => {
|
|
releaseInspection = resolve
|
|
})
|
|
}
|
|
})
|
|
|
|
const pending = validate(registry, root, controller, {
|
|
signal: abort.signal
|
|
})
|
|
await inspectionStarted
|
|
abort.abort(new Error('cancelled'))
|
|
releaseInspection?.('not-a-repository')
|
|
|
|
await expect(pending).rejects.toMatchObject({
|
|
code: 'aborted',
|
|
message: 'Workspace operation was aborted'
|
|
})
|
|
expect(registry.activeHandleCount()).toBe(0)
|
|
expect(existsSync(storagePath)).toBe(false)
|
|
})
|
|
|
|
linuxIt('requires path revalidation after daemon restart', async () => {
|
|
const state = temporaryDirectory()
|
|
const root = join(state, 'workspace')
|
|
mkdirSync(root)
|
|
const storagePath = join(state, 'workspaces.json')
|
|
const firstControllers = new ControllerRegistry()
|
|
const firstController = firstControllers.attach('controller-owner')
|
|
const firstRegistry = new WorkspaceRegistry({
|
|
controllers: firstControllers,
|
|
storagePath,
|
|
inspectGit: async () => 'not-a-repository'
|
|
})
|
|
const first = await validate(firstRegistry, root, firstController)
|
|
firstRegistry.closeAll()
|
|
|
|
const replacementControllers = new ControllerRegistry()
|
|
const replacement = replacementControllers.attach('controller-owner')
|
|
const replacementRegistry = new WorkspaceRegistry({
|
|
controllers: replacementControllers,
|
|
storagePath,
|
|
inspectGit: async () => 'not-a-repository'
|
|
})
|
|
await expect(
|
|
replacementRegistry.resume(
|
|
{
|
|
workspaceId: first.handle.workspaceId,
|
|
generation: first.handle.generation,
|
|
workspaceIdentity: first.handle.workspaceIdentity
|
|
},
|
|
replacement
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
await expect(
|
|
replacementRegistry.open(
|
|
{
|
|
workspaceIdentity: first.handle.workspaceIdentity,
|
|
requestedAccess: 'read-only'
|
|
},
|
|
replacement
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
|
|
const revalidated = await validate(
|
|
replacementRegistry,
|
|
root,
|
|
replacement
|
|
)
|
|
expect(revalidated.handle.workspaceIdentity).toBe(
|
|
first.handle.workspaceIdentity
|
|
)
|
|
expect(revalidated.handle.workspaceId).not.toBe(
|
|
first.handle.workspaceId
|
|
)
|
|
})
|
|
|
|
it('clears legacy persisted roots and never authorizes them', async () => {
|
|
const state = temporaryDirectory()
|
|
const root = join(state, 'workspace')
|
|
mkdirSync(root)
|
|
const storagePath = join(state, 'workspaces.json')
|
|
writeFileSync(
|
|
storagePath,
|
|
JSON.stringify({
|
|
version: 1,
|
|
roots: [
|
|
{
|
|
canonicalPath: root,
|
|
device: '1',
|
|
inode: '2',
|
|
workspaceIdentity: 'workspace-legacy',
|
|
git: 'not-a-repository',
|
|
trustAttestationRevision: 1,
|
|
authorizedControllers: ['controller-owner']
|
|
}
|
|
],
|
|
leases: [
|
|
{
|
|
handle: {
|
|
workspaceId: 'workspace-legacy-handle',
|
|
workspaceIdentity: 'workspace-legacy',
|
|
canonicalDisplayPath: root,
|
|
access: 'read-only',
|
|
git: 'not-a-repository',
|
|
capabilities: ['list', 'stat', 'read-text', 'search'],
|
|
generation: 1
|
|
},
|
|
controllerId: 'controller-owner',
|
|
controllerGeneration: controllerGenerationForLegacyFixture
|
|
}
|
|
]
|
|
}),
|
|
{ mode: 0o600 }
|
|
)
|
|
const controllers = new ControllerRegistry()
|
|
const controller = controllers.attach('controller-owner')
|
|
const registry = new WorkspaceRegistry({
|
|
controllers,
|
|
storagePath,
|
|
inspectGit: async () => 'not-a-repository'
|
|
})
|
|
|
|
expect(JSON.parse(readFileSync(storagePath, 'utf8'))).toEqual({
|
|
version: 2,
|
|
authorization: 'ephemeral'
|
|
})
|
|
await expect(
|
|
registry.open(
|
|
{
|
|
workspaceIdentity: 'workspace-legacy',
|
|
requestedAccess: 'read-only'
|
|
},
|
|
controller
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
await expect(
|
|
registry.resume(
|
|
{
|
|
workspaceId: 'workspace-legacy-handle',
|
|
workspaceIdentity: 'workspace-legacy',
|
|
generation: 1
|
|
},
|
|
controller
|
|
)
|
|
).rejects.toMatchObject({ code: 'workspace-not-found' })
|
|
expect(registry.activeHandleCount()).toBe(0)
|
|
})
|
|
})
|
|
|
|
function workspaceRegistry(
|
|
controllers: ControllerRegistry
|
|
): WorkspaceRegistry {
|
|
return new WorkspaceRegistry({
|
|
controllers,
|
|
inspectGit: async () => 'not-a-repository'
|
|
})
|
|
}
|
|
|
|
const controllerGenerationForLegacyFixture = 1
|
|
|
|
async function validate(
|
|
registry: WorkspaceRegistry,
|
|
root: string,
|
|
controller: ReturnType<ControllerRegistry['attach']>,
|
|
options: { signal?: AbortSignal } = {}
|
|
) {
|
|
return await registry.validate(
|
|
{
|
|
remoteRootPath: root,
|
|
requestedAccess: 'read-only',
|
|
requiredCapabilities: []
|
|
},
|
|
controller,
|
|
options
|
|
)
|
|
}
|
|
|
|
function temporaryDirectory(): string {
|
|
const path = mkdtempSync(join(tmpdir(), 'goodbuddy-registry-'))
|
|
temporaryPaths.push(path)
|
|
return path
|
|
}
|