UNPKG

@tanstack/ai-sandbox

Version:

Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.

473 lines (451 loc) 16.7 kB
import { describe, expect, it } from 'vitest' import type { SandboxCheckpoint, SandboxCheckpointStore, SandboxCheckpointStoreOptions, SandboxCheckpointWriter, } from '../checkpoint-store' const writers = new WeakMap< SandboxCheckpointStore, Map<string, Promise<SandboxCheckpointWriter>> >() async function writerFor(store: SandboxCheckpointStore, threadId: string) { let storeWriters = writers.get(store) if (!storeWriters) { storeWriters = new Map() writers.set(store, storeWriters) } let writer = storeWriters.get(threadId) if (!writer) { writer = store.acquireWriter(threadId) storeWriters.set(threadId, writer) } return writer } async function append( store: SandboxCheckpointStore, input: { checkpoint: SandboxCheckpoint; expectedHeadId: string | null }, ) { return store.append({ ...input, writer: await writerFor(store, input.checkpoint.threadId), }) } async function deleteHead( store: SandboxCheckpointStore, input: { threadId: string; checkpointId: string }, ) { return store.deleteHead({ ...input, writer: await writerFor(store, input.threadId), }) } async function expectThreadState( store: SandboxCheckpointStore, staleCheckpointId: string, expected: { head: string | null list: Array<SandboxCheckpoint> checkpoint: SandboxCheckpoint | null references: Array<{ key: string; references: number }> }, ) { expect(await store.getHead('thread-a')).toBe(expected.head) expect(await store.list('thread-a')).toEqual(expected.list) expect(await store.get(staleCheckpointId)).toEqual(expected.checkpoint) expect(await store.listBlobReferences()).toEqual(expected.references) } function checkpoint( id: string, parentCheckpointId: string | null = null, threadId = 'thread-a', ): SandboxCheckpoint { const hash = (Number(id.replace(/\D/g, '')) || 1) .toString(16) .padStart(64, '0') return { id, threadId, parentCheckpointId, createdAt: Number(id.replace(/\D/g, '')) || 1, reason: 'automatic', files: [ { path: `${id}.txt`, kind: 'file', blobKey: `sandbox-files/sha256/${hash}`, size: 1, }, ], conversation: [{ role: 'user', content: id }], artifacts: [], } } export function runSandboxCheckpointStoreConformance( name: string, makeStore: ( options?: SandboxCheckpointStoreOptions, ) => SandboxCheckpointStore | Promise<SandboxCheckpointStore>, options?: SandboxCheckpointStoreOptions, ): void { describe(`SandboxCheckpointStore conformance: ${name}`, () => { it('returns null and an empty list for an unknown thread', async () => { const store = await makeStore(options) expect(await store.get('missing')).toBeNull() expect(await store.getHead('missing-thread')).toBeNull() expect(await store.list('missing-thread')).toEqual([]) }) it('appends a root and then a parent-linked checkpoint', async () => { const store = await makeStore(options) await expect( append(store, { checkpoint: checkpoint('1'), expectedHeadId: null }), ).resolves.toEqual({ headId: '1' }) await expect( append(store, { checkpoint: checkpoint('2', '1'), expectedHeadId: '1', }), ).resolves.toEqual({ headId: '2' }) expect(await store.getHead('thread-a')).toBe('2') expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([ '1', '2', ]) }) it('enforces expected-head and parent compare-and-swap rules', async () => { const store = await makeStore(options) await append(store, { checkpoint: checkpoint('1'), expectedHeadId: null }) await expect( append(store, { checkpoint: checkpoint('2'), expectedHeadId: null }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_STALE_HEAD' }) await expect( append(store, { checkpoint: checkpoint('3'), expectedHeadId: '1' }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_PARENT_MISMATCH' }) }) it('rejects duplicate checkpoint ids', async () => { const store = await makeStore(options) await append(store, { checkpoint: checkpoint('1'), expectedHeadId: null }) await expect( append(store, { checkpoint: checkpoint('1'), expectedHeadId: '1' }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_DUPLICATE_ID' }) }) it('isolates checkpoints and heads by thread', async () => { const store = await makeStore(options) await append(store, { checkpoint: checkpoint('a', null, 'thread-a'), expectedHeadId: null, }) await append(store, { checkpoint: checkpoint('b', null, 'thread-b'), expectedHeadId: null, }) expect(await store.getHead('thread-a')).toBe('a') expect(await store.getHead('thread-b')).toBe('b') expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([ 'a', ]) expect((await store.list('thread-b')).map((entry) => entry.id)).toEqual([ 'b', ]) }) it('lists checkpoints by createdAt and then checkpoint id', async () => { const store = await makeStore(options) const first = { ...checkpoint('z'), createdAt: 2 } const second = { ...checkpoint('a', 'z'), createdAt: 1 } const third = { ...checkpoint('b', 'a'), createdAt: 1 } await append(store, { checkpoint: first, expectedHeadId: null }) await append(store, { checkpoint: second, expectedHeadId: 'z' }) await append(store, { checkpoint: third, expectedHeadId: 'a' }) expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([ 'a', 'b', 'z', ]) }) it('orders checkpoint ids and blob keys by UTF-8 bytes', async () => { const store = await makeStore(options) const ids = ['é', 'a', 'B', '😀'] const keys = ['1', '2', '3', '4'].map( (value) => `sandbox-files/sha256/${value.padStart(64, '0')}`, ) let parent: string | null = null for (const [index, id] of ids.entries()) { const blobKey = keys[index] if (!blobKey) throw new Error(`Missing blob key for '${id}'`) await append(store, { checkpoint: { ...checkpoint(id, parent), files: [ { path: `${id}.txt`, kind: 'file', blobKey, size: 1, }, ], }, expectedHeadId: parent, }) parent = id } expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([ 'B', 'a', 'é', '😀', ]) expect( (await store.listBlobReferences()).map((entry) => entry.key), ).toEqual([ `sandbox-files/sha256/${'0'.repeat(63)}1`, `sandbox-files/sha256/${'0'.repeat(63)}2`, `sandbox-files/sha256/${'0'.repeat(63)}3`, `sandbox-files/sha256/${'0'.repeat(63)}4`, ]) }) it('rejects malformed Unicode checkpoint ids and blob keys', async () => { const store = await makeStore(options) const malformed = '\uD800' await expect( append(store, { checkpoint: checkpoint(malformed), expectedHeadId: null, }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_INVALID_ID' }) await expect( append(store, { checkpoint: { ...checkpoint('root'), files: [ { path: 'root.txt', kind: 'file', blobKey: `sandbox-files/sha256/${malformed}${'0'.repeat(63)}`, size: 1, }, ], }, expectedHeadId: null, }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_INVALID_ENTRY' }) }) it.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY])( 'rejects non-finite createdAt without changing state: %s', async (createdAt) => { const store = await makeStore(options) const before = { head: await store.getHead('thread-a'), list: await store.list('thread-a'), references: await store.listBlobReferences(), } await expect( append(store, { checkpoint: { ...checkpoint('invalid'), createdAt }, expectedHeadId: null, }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_INVALID_ENTRY' }) expect(await store.getHead('thread-a')).toBe(before.head) expect(await store.list('thread-a')).toEqual(before.list) expect(await store.listBlobReferences()).toEqual(before.references) }, ) it('keeps checkpoints immutable and reference counts distinct blob keys', async () => { const store = await makeStore(options) const value = { ...checkpoint('1'), files: [ ...checkpoint('1').files, { path: 'same.txt', kind: 'file' as const, blobKey: `sandbox-files/sha256/${'0'.repeat(63)}1`, size: 1, }, ], } await append(store, { checkpoint: value, expectedHeadId: null }) const inputFile = value.files[0] if (!inputFile) throw new Error('Expected input file') inputFile.path = 'input-mutated' const loaded = await store.get('1') expect(loaded).not.toBe(value) if (!loaded) throw new Error('Expected stored checkpoint') const loadedFile = loaded.files[0] if (!loadedFile) throw new Error('Expected stored file') expect(loadedFile.path).toBe('1.txt') loadedFile.path = 'changed' expect((await store.get('1'))?.files[0]?.path).toBe('1.txt') const listed = await store.list('thread-a') const listedCheckpoint = listed[0] const listedFile = listedCheckpoint?.files[0] if (!listedFile) throw new Error('Expected listed file') listedFile.path = 'list-mutated' expect((await store.list('thread-a'))[0]?.files[0]?.path).toBe('1.txt') expect(await store.listBlobReferences()).toEqual([ { key: `sandbox-files/sha256/${'0'.repeat(63)}1`, references: 1 }, ]) }) it('counts one reference per checkpoint for shared blob keys', async () => { const store = await makeStore(options) const shared = `sandbox-files/sha256/${'f'.repeat(64)}` await append(store, { checkpoint: { ...checkpoint('1'), files: [{ path: 'a', kind: 'file', blobKey: shared, size: 1 }], }, expectedHeadId: null, }) await append(store, { checkpoint: { ...checkpoint('2', '1'), files: [{ path: 'b', kind: 'file', blobKey: shared, size: 1 }], }, expectedHeadId: '1', }) expect(await store.listBlobReferences()).toEqual([ { key: shared, references: 2 }, ]) await deleteHead(store, { threadId: 'thread-a', checkpointId: '2' }) expect(await store.listBlobReferences()).toEqual([ { key: shared, references: 1 }, ]) }) it('allows one non-waiting writer lease per thread', async () => { const store = await makeStore(options) const lease = await store.acquireWriter('thread-a') try { await expect(store.acquireWriter('thread-a')).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_CONFLICT', }) const secondLease = await store.acquireWriter('thread-b') try { expect(secondLease).toBeDefined() } finally { await secondLease.release() } } finally { await lease.release() } const nextLease = await store.acquireWriter('thread-a') await nextLease.release() }) it('extends a lease when renew succeeds', async () => { const config = options ?? {} let current = config.now?.() ?? 1_000 const store = await makeStore({ ...config, now: () => current }) const lease = await store.acquireWriter('thread-a') const originalExpiry = lease.expiresAt current += lease.renewAfterMs await expect(lease.renew()).resolves.toEqual({ expiresAt: current + (config.leaseDurationMs ?? 120_000), }) expect(lease.expiresAt).toBeGreaterThan(originalExpiry) await lease.release() }) it('does not let a stale lease release a replacement lease', async () => { const config = options ?? {} let current = config.now?.() ?? 1_000 const store = await makeStore({ ...config, now: () => current }) const old = await store.acquireWriter('thread-a') current += (config.leaseDurationMs ?? 120_000) + 1 const replacement = await store.acquireWriter('thread-a') await old.release() await expect(store.acquireWriter('thread-a')).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_CONFLICT', }) await replacement.release() }) it('rejects stale leases after expiry takeover without changing state', async () => { const config = options ?? {} let current = config.now?.() ?? Date.now() const takeoverStore = await makeStore({ ...config, now: () => current }) const old = await takeoverStore.acquireWriter('thread-a') await takeoverStore.append({ checkpoint: checkpoint('root'), expectedHeadId: null, writer: old, }) current += (config.leaseDurationMs ?? 120_000) + 1 const beforeExpiry = { head: await takeoverStore.getHead('thread-a'), list: await takeoverStore.list('thread-a'), checkpoint: await takeoverStore.get('root'), references: await takeoverStore.listBlobReferences(), } await expect(old.renew()).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST', }) await expect( takeoverStore.append({ checkpoint: checkpoint('stale', 'root'), expectedHeadId: 'root', writer: old, }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' }) await expectThreadState(takeoverStore, 'stale', { ...beforeExpiry, checkpoint: null, }) await expect( takeoverStore.deleteHead({ threadId: 'thread-a', checkpointId: 'root', writer: old, }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' }) await expectThreadState(takeoverStore, 'root', beforeExpiry) const next = await takeoverStore.acquireWriter('thread-a') expect(next.fence).toBeGreaterThan(old.fence) const before = { head: await takeoverStore.getHead('thread-a'), list: await takeoverStore.list('thread-a'), checkpoint: await takeoverStore.get('root'), references: await takeoverStore.listBlobReferences(), } await expect(old.renew()).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST', }) await expect( takeoverStore.append({ checkpoint: checkpoint('stale', 'root'), expectedHeadId: 'root', writer: old, }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' }) await expectThreadState(takeoverStore, 'stale', { ...before, checkpoint: null, }) await expect( takeoverStore.deleteHead({ threadId: 'thread-a', checkpointId: 'root', writer: old, }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' }) await expectThreadState(takeoverStore, 'root', before) await old.release() await expectThreadState(takeoverStore, 'stale', { ...before, checkpoint: null, }) await next.release() }) it('deletes only the current head and transitions to its parent', async () => { const store = await makeStore(options) await append(store, { checkpoint: checkpoint('1'), expectedHeadId: null }) await append(store, { checkpoint: checkpoint('2', '1'), expectedHeadId: '1', }) await expect( deleteHead(store, { threadId: 'thread-a', checkpointId: '1' }), ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_NOT_HEAD' }) await deleteHead(store, { threadId: 'thread-a', checkpointId: '2' }) expect(await store.getHead('thread-a')).toBe('1') await deleteHead(store, { threadId: 'thread-a', checkpointId: '1' }) expect(await store.getHead('thread-a')).toBeNull() expect(await store.listBlobReferences()).toEqual([]) }) }) }