@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.
148 lines (136 loc) • 7.19 kB
text/typescript
/**
* Conformance for the DURABLE-RUN fields on a `RunStore`.
*
* These four fields (`sandboxKey`, `detachedSince`, `cancelRequested`,
* `driverEpoch`) exist for durable sandboxed runs: detach on disconnect, takeover
* by a later host, and the reaper. A chat-only app never writes them, so proving
* them is NOT part of `runPersistenceConformance` in `@tanstack/ai-persistence`.
* They live here, next to the takeover and reaper suites that depend on them.
*
* Run this when your app wires `withSandbox(sandbox, { runs, durability })`. The
* fields round-trip through the REQUIRED `update`/`get` pair, so a backend can
* pass every persistence case while silently dropping one of them, and the
* failure then shows up as a run that looks permanently detached or a takeover
* that cannot fence a superseded host.
*
* ```ts
* import { runDurableRunFieldsConformance } from '@tanstack/ai-sandbox/testkit'
* import { myPersistence } from './persistence'
*
* runDurableRunFieldsConformance('my postgres runs', () => myPersistence().stores.runs)
* ```
*/
import { describe, expect, it } from 'vitest'
import type { RunStore } from '@tanstack/ai'
/** Factory for the store under test. A fresh one per case keeps them isolated. */
export type MakeRunStore = () => RunStore | Promise<RunStore>
export function runDurableRunFieldsConformance(
name: string,
makeStore: MakeRunStore,
): void {
describe(`durable run fields conformance: ${name}`, () => {
// One case, because the four fields share one failure mode: a backend that
// filters `undefined` out of its `SET` clause, or coerces an absent column to
// a falsy default, passes every other assertion while breaking detach and
// takeover. Splitting it per field would hide that they must all behave the
// same way through one `update`.
it('round-trips the durable run fields, overwrites driverEpoch, and clears every one of them on explicit undefined', async () => {
const store = await makeStore()
await store.createOrResume({
runId: 'fc-1',
threadId: 'fc-t',
startedAt: 1,
})
// 0. A fresh run that was never patched with these fields must read
// back as undefined -- not null, not false, not 0. A backend that
// coerces a NULL/absent column to a falsy default (e.g.
// `cancelRequested: false`) is claiming knowledge ("explicitly not
// cancelled") it does not have, and `toBeFalsy()` would not catch
// it since `false` is falsy too.
const fresh = await store.get('fc-1')
expect(fresh?.cancelRequested).toBeUndefined()
expect(fresh?.detachedSince).toBeUndefined()
expect(fresh?.sandboxKey).toBeUndefined()
expect(fresh?.driverEpoch).toBeUndefined()
// 1. All four fields round-trip through update -> get.
await store.update('fc-1', {
sandboxKey: 'sandbox-abc',
detachedSince: 500,
cancelRequested: true,
driverEpoch: 1,
})
const afterFirstUpdate = await store.get('fc-1')
expect(afterFirstUpdate?.sandboxKey).toBe('sandbox-abc')
expect(afterFirstUpdate?.detachedSince).toBe(500)
expect(afterFirstUpdate?.cancelRequested).toBe(true)
expect(afterFirstUpdate?.driverEpoch).toBe(1)
// 2. A monotonic driverEpoch bump overwrites, it is not ignored (a
// takeover host bumping the fencing token must actually stick).
await store.update('fc-1', { driverEpoch: 2 })
const afterEpochBump = await store.get('fc-1')
expect(afterEpochBump?.driverEpoch).toBe(2)
// Sibling fields untouched by an update that only names driverEpoch.
expect(afterEpochBump?.sandboxKey).toBe('sandbox-abc')
expect(afterEpochBump?.cancelRequested).toBe(true)
// 3. update({ detachedSince: undefined }) actually CLEARS the field.
// A backend whose SQL adapter filters `undefined` out of its `SET`
// clause leaves the old value, and every re-attached run then looks
// permanently detached to the reaper.
await store.update('fc-1', { detachedSince: undefined })
const afterClear = await store.get('fc-1')
expect(afterClear?.detachedSince).toBeUndefined()
// Clearing detachedSince must not clobber the other durable fields.
expect(afterClear?.sandboxKey).toBe('sandbox-abc')
expect(afterClear?.cancelRequested).toBe(true)
expect(afterClear?.driverEpoch).toBe(2)
// 4. cancelRequested: false written EXPLICITLY must round-trip as
// `false`, distinct from the fresh-run `undefined` checked in step 0.
// A backend storing this boolean in an integer/NULL-able column has
// to preserve the false/undefined distinction in both directions,
// not just collapse both to falsy.
await store.update('fc-1', { cancelRequested: false })
const afterExplicitFalse = await store.get('fc-1')
expect(afterExplicitFalse?.cancelRequested).toBe(false)
expect(afterExplicitFalse?.cancelRequested).not.toBeUndefined()
// 5. An explicit `undefined` clears EVERY durable field, not just
// `detachedSince`. Step 3 only exercised one of the four, so a backend
// half-converted to `'field' in patch` -- `in` for `detachedSince`,
// still `patch.field !== undefined` for the rest -- passed the whole
// suite while its clears silently no-opped. Step 4's explicit `false`
// also survives a `!== undefined` guard, so nothing else here bites
// either. Re-populate first, so each clear has a value to remove and
// an assertion that fails when the clear is dropped.
await store.update('fc-1', {
sandboxKey: 'sandbox-xyz',
detachedSince: 900,
cancelRequested: true,
driverEpoch: 3,
})
const beforeFullClear = await store.get('fc-1')
expect(beforeFullClear?.sandboxKey).toBe('sandbox-xyz')
expect(beforeFullClear?.detachedSince).toBe(900)
expect(beforeFullClear?.cancelRequested).toBe(true)
expect(beforeFullClear?.driverEpoch).toBe(3)
await store.update('fc-1', {
sandboxKey: undefined,
detachedSince: undefined,
cancelRequested: undefined,
driverEpoch: undefined,
})
const afterFullClear = await store.get('fc-1')
expect(afterFullClear?.sandboxKey).toBeUndefined()
expect(afterFullClear?.detachedSince).toBeUndefined()
expect(afterFullClear?.cancelRequested).toBeUndefined()
expect(afterFullClear?.driverEpoch).toBeUndefined()
// Clearing the durable fields is not a delete: the run row survives,
// and the fields the patch never named keep their values.
expect(afterFullClear?.status).toBe('running')
expect(afterFullClear?.startedAt).toBe(1)
})
// `findActiveRun` is REQUIRED on the RunStore contract — every backend that
// provides a `runs` store must satisfy these invariants (most-recent-running
// wins, thread-scoped, null when idle). Reconnect is built on it, and a
// backend that always answers `null` disables reconnect indistinguishably
// from one that is merely idle, so this must never degrade to a skip.
})
}