@tanstack/ai-sandbox 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/agents-file.js +53 -34
- package/dist/esm/agents-file.js.map +1 -1
- package/dist/esm/align.d.ts +121 -0
- package/dist/esm/align.js +197 -0
- package/dist/esm/align.js.map +1 -0
- package/dist/esm/approvals.js +63 -29
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/attach-preflight.d.ts +85 -0
- package/dist/esm/attach-preflight.js +189 -0
- package/dist/esm/attach-preflight.js.map +1 -0
- package/dist/esm/bootstrap.js +103 -117
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/bridge-events.js +96 -71
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/capabilities.d.ts +0 -5
- package/dist/esm/capabilities.js +32 -28
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/chunk-identity.d.ts +52 -0
- package/dist/esm/chunk-identity.js +102 -0
- package/dist/esm/chunk-identity.js.map +1 -0
- package/dist/esm/claim.d.ts +187 -0
- package/dist/esm/claim.js +349 -0
- package/dist/esm/claim.js.map +1 -0
- package/dist/esm/contracts.d.ts +13 -0
- package/dist/esm/driver.d.ts +83 -0
- package/dist/esm/driver.js +138 -0
- package/dist/esm/driver.js.map +1 -0
- package/dist/esm/durability.d.ts +263 -0
- package/dist/esm/durability.js +230 -0
- package/dist/esm/durability.js.map +1 -0
- package/dist/esm/errors.js +28 -24
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/file-diff.js +151 -135
- package/dist/esm/file-diff.js.map +1 -1
- package/dist/esm/git-exec.js +51 -62
- package/dist/esm/git-exec.js.map +1 -1
- package/dist/esm/harness-cwd.js +24 -19
- package/dist/esm/harness-cwd.js.map +1 -1
- package/dist/esm/index.d.ts +30 -8
- package/dist/esm/index.js +23 -91
- package/dist/esm/instance-store.d.ts +88 -0
- package/dist/esm/instance-store.js +67 -0
- package/dist/esm/instance-store.js.map +1 -0
- package/dist/esm/journal-bytes.d.ts +67 -0
- package/dist/esm/journal-bytes.js +110 -0
- package/dist/esm/journal-bytes.js.map +1 -0
- package/dist/esm/journal-reader.d.ts +66 -0
- package/dist/esm/journal-reader.js +228 -0
- package/dist/esm/journal-reader.js.map +1 -0
- package/dist/esm/journal-sweep.d.ts +113 -0
- package/dist/esm/journal-sweep.js +309 -0
- package/dist/esm/journal-sweep.js.map +1 -0
- package/dist/esm/journal.d.ts +542 -0
- package/dist/esm/journal.js +679 -0
- package/dist/esm/journal.js.map +1 -0
- package/dist/esm/key.js +36 -33
- package/dist/esm/key.js.map +1 -1
- package/dist/esm/middleware.d.ts +50 -2
- package/dist/esm/middleware.js +335 -208
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.js +75 -49
- package/dist/esm/ngrok.js.map +1 -1
- package/dist/esm/policy.js +43 -34
- package/dist/esm/policy.js.map +1 -1
- package/dist/esm/projection.js +16 -8
- package/dist/esm/projection.js.map +1 -1
- package/dist/esm/reap.d.ts +238 -0
- package/dist/esm/reap.js +355 -0
- package/dist/esm/reap.js.map +1 -0
- package/dist/esm/reclaim.d.ts +84 -0
- package/dist/esm/reclaim.js +106 -0
- package/dist/esm/reclaim.js.map +1 -0
- package/dist/esm/remote-tools.js +73 -62
- package/dist/esm/remote-tools.js.map +1 -1
- package/dist/esm/run.d.ts +93 -25
- package/dist/esm/run.js +274 -79
- package/dist/esm/run.js.map +1 -1
- package/dist/esm/runner.d.ts +119 -2
- package/dist/esm/runner.js +270 -51
- package/dist/esm/runner.js.map +1 -1
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +139 -123
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/secrets.js +39 -47
- package/dist/esm/secrets.js.map +1 -1
- package/dist/esm/setup-plan.js +22 -14
- package/dist/esm/setup-plan.js.map +1 -1
- package/dist/esm/shell.d.ts +8 -0
- package/dist/esm/shell.js +197 -158
- package/dist/esm/shell.js.map +1 -1
- package/dist/esm/testkit/conformance.d.ts +16 -0
- package/dist/esm/testkit/conformance.js +97 -0
- package/dist/esm/testkit/conformance.js.map +1 -0
- package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
- package/dist/esm/testkit/journal-conformance.d.ts +51 -0
- package/dist/esm/testkit/journal-conformance.js +378 -0
- package/dist/esm/testkit/journal-conformance.js.map +1 -0
- package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
- package/dist/esm/testkit/reaper-conformance.js +847 -0
- package/dist/esm/testkit/reaper-conformance.js.map +1 -0
- package/dist/esm/testkit/shell-spawn.d.ts +2 -0
- package/dist/esm/testkit/shell-spawn.js +60 -0
- package/dist/esm/testkit/shell-spawn.js.map +1 -0
- package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
- package/dist/esm/testkit/takeover-conformance.js +685 -0
- package/dist/esm/testkit/takeover-conformance.js.map +1 -0
- package/dist/esm/tool-bridge.js +227 -180
- package/dist/esm/tool-bridge.js.map +1 -1
- package/dist/esm/tool-history.d.ts +62 -0
- package/dist/esm/tool-history.js +171 -0
- package/dist/esm/tool-history.js.map +1 -0
- package/dist/esm/watch.js +310 -236
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workspace.d.ts +1 -1
- package/dist/esm/workspace.js +49 -28
- package/dist/esm/workspace.js.map +1 -1
- package/package.json +16 -6
- package/skills/ai-sandbox/SKILL.md +658 -20
- package/src/align.ts +297 -0
- package/src/attach-preflight.ts +292 -0
- package/src/capabilities.ts +4 -13
- package/src/chunk-identity.ts +154 -0
- package/src/claim.ts +479 -0
- package/src/contracts.ts +13 -0
- package/src/driver.ts +205 -0
- package/src/durability.ts +380 -0
- package/src/index.ts +212 -27
- package/src/instance-store.ts +122 -0
- package/src/journal-bytes.ts +136 -0
- package/src/journal-reader.ts +359 -0
- package/src/journal-sweep.ts +406 -0
- package/src/journal.ts +875 -0
- package/src/middleware.ts +470 -30
- package/src/reap.ts +723 -0
- package/src/reclaim.ts +191 -0
- package/src/run.ts +365 -75
- package/src/runner.ts +347 -3
- package/src/sandbox.ts +38 -8
- package/src/shell.ts +106 -38
- package/src/testkit/conformance.ts +117 -0
- package/src/testkit/durable-run-fields-conformance.ts +147 -0
- package/src/testkit/journal-conformance.ts +676 -0
- package/src/testkit/reaper-conformance.ts +1201 -0
- package/src/testkit/shell-spawn.ts +67 -0
- package/src/testkit/takeover-conformance.ts +1040 -0
- package/src/tool-history.ts +245 -0
- package/src/workspace.ts +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/run-log.d.ts +0 -81
- package/dist/esm/run-log.js +0 -107
- package/dist/esm/run-log.js.map +0 -1
- package/dist/esm/store.d.ts +0 -53
- package/dist/esm/store.js +0 -34
- package/dist/esm/store.js.map +0 -1
- package/src/run-log.ts +0 -224
- package/src/store.ts +0 -83
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance suite for a {@link SandboxInstanceStore} implementation.
|
|
3
|
+
*
|
|
4
|
+
* Run this against a fresh BYO store (or the in-memory reference) to prove it
|
|
5
|
+
* satisfies the get / upsert / delete contract `@tanstack/ai-sandbox`'s ensure
|
|
6
|
+
* algorithm relies on — including insert-vs-overwrite and optional-field
|
|
7
|
+
* handling (full replace must clear omitted optionals).
|
|
8
|
+
*
|
|
9
|
+
* Vitest is an OPTIONAL peer dependency: this module is imported only from test
|
|
10
|
+
* files, which already run under Vitest.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from 'vitest'
|
|
13
|
+
import type {
|
|
14
|
+
SandboxInstanceRecord,
|
|
15
|
+
SandboxInstanceStore,
|
|
16
|
+
} from '../instance-store'
|
|
17
|
+
|
|
18
|
+
// The `./testkit` package.json subpath resolves to THIS module's build output
|
|
19
|
+
// only (`dist/esm/testkit/conformance.js`), not a barrel over `src/testkit/`.
|
|
20
|
+
// Re-export the journal, takeover, and reaper conformance suites here so they
|
|
21
|
+
// are reachable from `@tanstack/ai-sandbox/testkit` without changing that
|
|
22
|
+
// mapping.
|
|
23
|
+
export { runJournalConformance } from './journal-conformance'
|
|
24
|
+
export type { JournalConformanceConfig } from './journal-conformance'
|
|
25
|
+
export { runTakeoverConformance } from './takeover-conformance'
|
|
26
|
+
export type { TakeoverConformanceConfig } from './takeover-conformance'
|
|
27
|
+
export { runReaperConformance } from './reaper-conformance'
|
|
28
|
+
export type { ReaperConformanceConfig } from './reaper-conformance'
|
|
29
|
+
export { runDurableRunFieldsConformance } from './durable-run-fields-conformance'
|
|
30
|
+
export type { MakeRunStore } from './durable-run-fields-conformance'
|
|
31
|
+
export { makeFakeShellSpawn } from './shell-spawn'
|
|
32
|
+
|
|
33
|
+
function makeRecord(
|
|
34
|
+
overrides?: Partial<SandboxInstanceRecord>,
|
|
35
|
+
): SandboxInstanceRecord {
|
|
36
|
+
return {
|
|
37
|
+
key: 'thread-1',
|
|
38
|
+
provider: 'fake',
|
|
39
|
+
providerSandboxId: 'sb-1',
|
|
40
|
+
threadId: 'thread-1',
|
|
41
|
+
updatedAt: 1,
|
|
42
|
+
...overrides,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Assert `makeStore()` produces a spec-compliant {@link SandboxInstanceStore}. Each
|
|
48
|
+
* `it` gets a fresh store, so implementations may share process state across
|
|
49
|
+
* calls without cross-test bleed only if `makeStore` returns an isolated store.
|
|
50
|
+
*/
|
|
51
|
+
export function runSandboxInstanceStoreConformance(
|
|
52
|
+
name: string,
|
|
53
|
+
makeStore: () => SandboxInstanceStore | Promise<SandboxInstanceStore>,
|
|
54
|
+
): void {
|
|
55
|
+
describe(`SandboxInstanceStore conformance: ${name}`, () => {
|
|
56
|
+
it('returns null for a missing key', async () => {
|
|
57
|
+
const store = await makeStore()
|
|
58
|
+
expect(await store.get('absent')).toBeNull()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('round-trips an upserted record with all fields', async () => {
|
|
62
|
+
const store = await makeStore()
|
|
63
|
+
const record = makeRecord({
|
|
64
|
+
latestSnapshotId: 'snap-1',
|
|
65
|
+
latestRunId: 'run-1',
|
|
66
|
+
})
|
|
67
|
+
await store.upsert(record)
|
|
68
|
+
expect(await store.get(record.key)).toEqual(record)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('omits absent optional fields on read', async () => {
|
|
72
|
+
const store = await makeStore()
|
|
73
|
+
const record = makeRecord()
|
|
74
|
+
await store.upsert(record)
|
|
75
|
+
const loaded = await store.get(record.key)
|
|
76
|
+
expect(loaded).toEqual(record)
|
|
77
|
+
expect(loaded && 'latestSnapshotId' in loaded).toBe(false)
|
|
78
|
+
expect(loaded && 'latestRunId' in loaded).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('overwrites an existing record on re-upsert', async () => {
|
|
82
|
+
const store = await makeStore()
|
|
83
|
+
await store.upsert(makeRecord({ latestSnapshotId: 'snap-1' }))
|
|
84
|
+
await store.upsert(
|
|
85
|
+
makeRecord({ providerSandboxId: 'sb-2', updatedAt: 2 }),
|
|
86
|
+
)
|
|
87
|
+
const loaded = await store.get('thread-1')
|
|
88
|
+
expect(loaded?.providerSandboxId).toBe('sb-2')
|
|
89
|
+
expect(loaded?.updatedAt).toBe(2)
|
|
90
|
+
// The overwrite dropped latestSnapshotId — a durable store must clear it,
|
|
91
|
+
// not retain the prior value.
|
|
92
|
+
expect(loaded && 'latestSnapshotId' in loaded).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('isolates records by key', async () => {
|
|
96
|
+
const store = await makeStore()
|
|
97
|
+
await store.upsert(makeRecord({ key: 'a', threadId: 'a' }))
|
|
98
|
+
await store.upsert(
|
|
99
|
+
makeRecord({ key: 'b', threadId: 'b', providerSandboxId: 'sb-b' }),
|
|
100
|
+
)
|
|
101
|
+
expect((await store.get('a'))?.providerSandboxId).toBe('sb-1')
|
|
102
|
+
expect((await store.get('b'))?.providerSandboxId).toBe('sb-b')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('deletes a record', async () => {
|
|
106
|
+
const store = await makeStore()
|
|
107
|
+
await store.upsert(makeRecord())
|
|
108
|
+
await store.delete('thread-1')
|
|
109
|
+
expect(await store.get('thread-1')).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('delete of a missing key is a no-op', async () => {
|
|
113
|
+
const store = await makeStore()
|
|
114
|
+
await expect(store.delete('absent')).resolves.toBeUndefined()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance for the DURABLE-RUN fields on a `RunStore`.
|
|
3
|
+
*
|
|
4
|
+
* These four fields (`sandboxKey`, `detachedSince`, `cancelRequested`,
|
|
5
|
+
* `driverEpoch`) exist for durable sandboxed runs: detach on disconnect, takeover
|
|
6
|
+
* by a later host, and the reaper. A chat-only app never writes them, so proving
|
|
7
|
+
* them is NOT part of `runPersistenceConformance` in `@tanstack/ai-persistence`.
|
|
8
|
+
* They live here, next to the takeover and reaper suites that depend on them.
|
|
9
|
+
*
|
|
10
|
+
* Run this when your app wires `withSandbox(sandbox, { runs, durability })`. The
|
|
11
|
+
* fields round-trip through the REQUIRED `update`/`get` pair, so a backend can
|
|
12
|
+
* pass every persistence case while silently dropping one of them, and the
|
|
13
|
+
* failure then shows up as a run that looks permanently detached or a takeover
|
|
14
|
+
* that cannot fence a superseded host.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { runDurableRunFieldsConformance } from '@tanstack/ai-sandbox/testkit'
|
|
18
|
+
* import { myPersistence } from './persistence'
|
|
19
|
+
*
|
|
20
|
+
* runDurableRunFieldsConformance('my postgres runs', () => myPersistence().stores.runs)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { describe, expect, it } from 'vitest'
|
|
24
|
+
import type { RunStore } from '@tanstack/ai'
|
|
25
|
+
|
|
26
|
+
/** Factory for the store under test. A fresh one per case keeps them isolated. */
|
|
27
|
+
export type MakeRunStore = () => RunStore | Promise<RunStore>
|
|
28
|
+
|
|
29
|
+
export function runDurableRunFieldsConformance(
|
|
30
|
+
name: string,
|
|
31
|
+
makeStore: MakeRunStore,
|
|
32
|
+
): void {
|
|
33
|
+
describe(`durable run fields conformance: ${name}`, () => {
|
|
34
|
+
// One case, because the four fields share one failure mode: a backend that
|
|
35
|
+
// filters `undefined` out of its `SET` clause, or coerces an absent column to
|
|
36
|
+
// a falsy default, passes every other assertion while breaking detach and
|
|
37
|
+
// takeover. Splitting it per field would hide that they must all behave the
|
|
38
|
+
// same way through one `update`.
|
|
39
|
+
it('round-trips the durable run fields, overwrites driverEpoch, and clears every one of them on explicit undefined', async () => {
|
|
40
|
+
const store = await makeStore()
|
|
41
|
+
|
|
42
|
+
await store.createOrResume({
|
|
43
|
+
runId: 'fc-1',
|
|
44
|
+
threadId: 'fc-t',
|
|
45
|
+
startedAt: 1,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 0. A fresh run that was never patched with these fields must read
|
|
49
|
+
// back as undefined -- not null, not false, not 0. A backend that
|
|
50
|
+
// coerces a NULL/absent column to a falsy default (e.g.
|
|
51
|
+
// `cancelRequested: false`) is claiming knowledge ("explicitly not
|
|
52
|
+
// cancelled") it does not have, and `toBeFalsy()` would not catch
|
|
53
|
+
// it since `false` is falsy too.
|
|
54
|
+
const fresh = await store.get('fc-1')
|
|
55
|
+
expect(fresh?.cancelRequested).toBeUndefined()
|
|
56
|
+
expect(fresh?.detachedSince).toBeUndefined()
|
|
57
|
+
expect(fresh?.sandboxKey).toBeUndefined()
|
|
58
|
+
expect(fresh?.driverEpoch).toBeUndefined()
|
|
59
|
+
|
|
60
|
+
// 1. All four fields round-trip through update -> get.
|
|
61
|
+
await store.update('fc-1', {
|
|
62
|
+
sandboxKey: 'sandbox-abc',
|
|
63
|
+
detachedSince: 500,
|
|
64
|
+
cancelRequested: true,
|
|
65
|
+
driverEpoch: 1,
|
|
66
|
+
})
|
|
67
|
+
const afterFirstUpdate = await store.get('fc-1')
|
|
68
|
+
expect(afterFirstUpdate?.sandboxKey).toBe('sandbox-abc')
|
|
69
|
+
expect(afterFirstUpdate?.detachedSince).toBe(500)
|
|
70
|
+
expect(afterFirstUpdate?.cancelRequested).toBe(true)
|
|
71
|
+
expect(afterFirstUpdate?.driverEpoch).toBe(1)
|
|
72
|
+
|
|
73
|
+
// 2. A monotonic driverEpoch bump overwrites, it is not ignored (a
|
|
74
|
+
// takeover host bumping the fencing token must actually stick).
|
|
75
|
+
await store.update('fc-1', { driverEpoch: 2 })
|
|
76
|
+
const afterEpochBump = await store.get('fc-1')
|
|
77
|
+
expect(afterEpochBump?.driverEpoch).toBe(2)
|
|
78
|
+
// Sibling fields untouched by an update that only names driverEpoch.
|
|
79
|
+
expect(afterEpochBump?.sandboxKey).toBe('sandbox-abc')
|
|
80
|
+
expect(afterEpochBump?.cancelRequested).toBe(true)
|
|
81
|
+
|
|
82
|
+
// 3. update({ detachedSince: undefined }) actually CLEARS the field.
|
|
83
|
+
// A backend whose SQL adapter filters `undefined` out of its `SET`
|
|
84
|
+
// clause leaves the old value, and every re-attached run then looks
|
|
85
|
+
// permanently detached to the reaper.
|
|
86
|
+
await store.update('fc-1', { detachedSince: undefined })
|
|
87
|
+
const afterClear = await store.get('fc-1')
|
|
88
|
+
expect(afterClear?.detachedSince).toBeUndefined()
|
|
89
|
+
// Clearing detachedSince must not clobber the other durable fields.
|
|
90
|
+
expect(afterClear?.sandboxKey).toBe('sandbox-abc')
|
|
91
|
+
expect(afterClear?.cancelRequested).toBe(true)
|
|
92
|
+
expect(afterClear?.driverEpoch).toBe(2)
|
|
93
|
+
|
|
94
|
+
// 4. cancelRequested: false written EXPLICITLY must round-trip as
|
|
95
|
+
// `false`, distinct from the fresh-run `undefined` checked in step 0.
|
|
96
|
+
// A backend storing this boolean in an integer/NULL-able column has
|
|
97
|
+
// to preserve the false/undefined distinction in both directions,
|
|
98
|
+
// not just collapse both to falsy.
|
|
99
|
+
await store.update('fc-1', { cancelRequested: false })
|
|
100
|
+
const afterExplicitFalse = await store.get('fc-1')
|
|
101
|
+
expect(afterExplicitFalse?.cancelRequested).toBe(false)
|
|
102
|
+
expect(afterExplicitFalse?.cancelRequested).not.toBeUndefined()
|
|
103
|
+
|
|
104
|
+
// 5. An explicit `undefined` clears EVERY durable field, not just
|
|
105
|
+
// `detachedSince`. Step 3 only exercised one of the four, so a backend
|
|
106
|
+
// half-converted to `'field' in patch` -- `in` for `detachedSince`,
|
|
107
|
+
// still `patch.field !== undefined` for the rest -- passed the whole
|
|
108
|
+
// suite while its clears silently no-opped. Step 4's explicit `false`
|
|
109
|
+
// also survives a `!== undefined` guard, so nothing else here bites
|
|
110
|
+
// either. Re-populate first, so each clear has a value to remove and
|
|
111
|
+
// an assertion that fails when the clear is dropped.
|
|
112
|
+
await store.update('fc-1', {
|
|
113
|
+
sandboxKey: 'sandbox-xyz',
|
|
114
|
+
detachedSince: 900,
|
|
115
|
+
cancelRequested: true,
|
|
116
|
+
driverEpoch: 3,
|
|
117
|
+
})
|
|
118
|
+
const beforeFullClear = await store.get('fc-1')
|
|
119
|
+
expect(beforeFullClear?.sandboxKey).toBe('sandbox-xyz')
|
|
120
|
+
expect(beforeFullClear?.detachedSince).toBe(900)
|
|
121
|
+
expect(beforeFullClear?.cancelRequested).toBe(true)
|
|
122
|
+
expect(beforeFullClear?.driverEpoch).toBe(3)
|
|
123
|
+
|
|
124
|
+
await store.update('fc-1', {
|
|
125
|
+
sandboxKey: undefined,
|
|
126
|
+
detachedSince: undefined,
|
|
127
|
+
cancelRequested: undefined,
|
|
128
|
+
driverEpoch: undefined,
|
|
129
|
+
})
|
|
130
|
+
const afterFullClear = await store.get('fc-1')
|
|
131
|
+
expect(afterFullClear?.sandboxKey).toBeUndefined()
|
|
132
|
+
expect(afterFullClear?.detachedSince).toBeUndefined()
|
|
133
|
+
expect(afterFullClear?.cancelRequested).toBeUndefined()
|
|
134
|
+
expect(afterFullClear?.driverEpoch).toBeUndefined()
|
|
135
|
+
// Clearing the durable fields is not a delete: the run row survives,
|
|
136
|
+
// and the fields the patch never named keep their values.
|
|
137
|
+
expect(afterFullClear?.status).toBe('running')
|
|
138
|
+
expect(afterFullClear?.startedAt).toBe(1)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// `findActiveRun` is REQUIRED on the RunStore contract — every backend that
|
|
142
|
+
// provides a `runs` store must satisfy these invariants (most-recent-running
|
|
143
|
+
// wins, thread-scoped, null when idle). Reconnect is built on it, and a
|
|
144
|
+
// backend that always answers `null` disables reconnect indistinguishably
|
|
145
|
+
// from one that is merely idle, so this must never degrade to a skip.
|
|
146
|
+
})
|
|
147
|
+
}
|