@tanstack/ai-sandbox 0.2.3 → 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.
Files changed (158) hide show
  1. package/dist/esm/agents-file.js +53 -34
  2. package/dist/esm/agents-file.js.map +1 -1
  3. package/dist/esm/align.d.ts +121 -0
  4. package/dist/esm/align.js +197 -0
  5. package/dist/esm/align.js.map +1 -0
  6. package/dist/esm/approvals.js +63 -29
  7. package/dist/esm/approvals.js.map +1 -1
  8. package/dist/esm/attach-preflight.d.ts +85 -0
  9. package/dist/esm/attach-preflight.js +189 -0
  10. package/dist/esm/attach-preflight.js.map +1 -0
  11. package/dist/esm/bootstrap.js +103 -117
  12. package/dist/esm/bootstrap.js.map +1 -1
  13. package/dist/esm/bridge-events.js +96 -71
  14. package/dist/esm/bridge-events.js.map +1 -1
  15. package/dist/esm/capabilities.d.ts +0 -5
  16. package/dist/esm/capabilities.js +32 -28
  17. package/dist/esm/capabilities.js.map +1 -1
  18. package/dist/esm/chunk-identity.d.ts +52 -0
  19. package/dist/esm/chunk-identity.js +102 -0
  20. package/dist/esm/chunk-identity.js.map +1 -0
  21. package/dist/esm/claim.d.ts +187 -0
  22. package/dist/esm/claim.js +349 -0
  23. package/dist/esm/claim.js.map +1 -0
  24. package/dist/esm/contracts.d.ts +13 -0
  25. package/dist/esm/driver.d.ts +83 -0
  26. package/dist/esm/driver.js +138 -0
  27. package/dist/esm/driver.js.map +1 -0
  28. package/dist/esm/durability.d.ts +263 -0
  29. package/dist/esm/durability.js +230 -0
  30. package/dist/esm/durability.js.map +1 -0
  31. package/dist/esm/errors.js +28 -24
  32. package/dist/esm/errors.js.map +1 -1
  33. package/dist/esm/file-diff.js +151 -135
  34. package/dist/esm/file-diff.js.map +1 -1
  35. package/dist/esm/git-exec.js +51 -62
  36. package/dist/esm/git-exec.js.map +1 -1
  37. package/dist/esm/harness-cwd.js +24 -19
  38. package/dist/esm/harness-cwd.js.map +1 -1
  39. package/dist/esm/index.d.ts +30 -8
  40. package/dist/esm/index.js +23 -91
  41. package/dist/esm/instance-store.d.ts +88 -0
  42. package/dist/esm/instance-store.js +67 -0
  43. package/dist/esm/instance-store.js.map +1 -0
  44. package/dist/esm/journal-bytes.d.ts +67 -0
  45. package/dist/esm/journal-bytes.js +110 -0
  46. package/dist/esm/journal-bytes.js.map +1 -0
  47. package/dist/esm/journal-reader.d.ts +66 -0
  48. package/dist/esm/journal-reader.js +228 -0
  49. package/dist/esm/journal-reader.js.map +1 -0
  50. package/dist/esm/journal-sweep.d.ts +113 -0
  51. package/dist/esm/journal-sweep.js +309 -0
  52. package/dist/esm/journal-sweep.js.map +1 -0
  53. package/dist/esm/journal.d.ts +542 -0
  54. package/dist/esm/journal.js +679 -0
  55. package/dist/esm/journal.js.map +1 -0
  56. package/dist/esm/key.js +36 -33
  57. package/dist/esm/key.js.map +1 -1
  58. package/dist/esm/middleware.d.ts +50 -2
  59. package/dist/esm/middleware.js +335 -208
  60. package/dist/esm/middleware.js.map +1 -1
  61. package/dist/esm/ngrok.js +75 -49
  62. package/dist/esm/ngrok.js.map +1 -1
  63. package/dist/esm/policy.js +43 -34
  64. package/dist/esm/policy.js.map +1 -1
  65. package/dist/esm/projection.js +16 -8
  66. package/dist/esm/projection.js.map +1 -1
  67. package/dist/esm/reap.d.ts +238 -0
  68. package/dist/esm/reap.js +355 -0
  69. package/dist/esm/reap.js.map +1 -0
  70. package/dist/esm/reclaim.d.ts +84 -0
  71. package/dist/esm/reclaim.js +106 -0
  72. package/dist/esm/reclaim.js.map +1 -0
  73. package/dist/esm/remote-tools.js +73 -62
  74. package/dist/esm/remote-tools.js.map +1 -1
  75. package/dist/esm/run.d.ts +93 -25
  76. package/dist/esm/run.js +274 -79
  77. package/dist/esm/run.js.map +1 -1
  78. package/dist/esm/runner.d.ts +119 -2
  79. package/dist/esm/runner.js +270 -51
  80. package/dist/esm/runner.js.map +1 -1
  81. package/dist/esm/sandbox.d.ts +3 -2
  82. package/dist/esm/sandbox.js +139 -123
  83. package/dist/esm/sandbox.js.map +1 -1
  84. package/dist/esm/secrets.js +39 -47
  85. package/dist/esm/secrets.js.map +1 -1
  86. package/dist/esm/setup-plan.js +22 -14
  87. package/dist/esm/setup-plan.js.map +1 -1
  88. package/dist/esm/shell.d.ts +8 -0
  89. package/dist/esm/shell.js +197 -158
  90. package/dist/esm/shell.js.map +1 -1
  91. package/dist/esm/testkit/conformance.d.ts +16 -0
  92. package/dist/esm/testkit/conformance.js +97 -0
  93. package/dist/esm/testkit/conformance.js.map +1 -0
  94. package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
  95. package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
  96. package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
  97. package/dist/esm/testkit/journal-conformance.d.ts +51 -0
  98. package/dist/esm/testkit/journal-conformance.js +378 -0
  99. package/dist/esm/testkit/journal-conformance.js.map +1 -0
  100. package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
  101. package/dist/esm/testkit/reaper-conformance.js +847 -0
  102. package/dist/esm/testkit/reaper-conformance.js.map +1 -0
  103. package/dist/esm/testkit/shell-spawn.d.ts +2 -0
  104. package/dist/esm/testkit/shell-spawn.js +60 -0
  105. package/dist/esm/testkit/shell-spawn.js.map +1 -0
  106. package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
  107. package/dist/esm/testkit/takeover-conformance.js +685 -0
  108. package/dist/esm/testkit/takeover-conformance.js.map +1 -0
  109. package/dist/esm/tool-bridge.js +227 -180
  110. package/dist/esm/tool-bridge.js.map +1 -1
  111. package/dist/esm/tool-history.d.ts +62 -0
  112. package/dist/esm/tool-history.js +171 -0
  113. package/dist/esm/tool-history.js.map +1 -0
  114. package/dist/esm/watch.js +310 -236
  115. package/dist/esm/watch.js.map +1 -1
  116. package/dist/esm/workspace.d.ts +1 -1
  117. package/dist/esm/workspace.js +49 -28
  118. package/dist/esm/workspace.js.map +1 -1
  119. package/package.json +16 -6
  120. package/skills/ai-sandbox/SKILL.md +658 -20
  121. package/src/align.ts +297 -0
  122. package/src/attach-preflight.ts +292 -0
  123. package/src/capabilities.ts +4 -13
  124. package/src/chunk-identity.ts +154 -0
  125. package/src/claim.ts +479 -0
  126. package/src/contracts.ts +13 -0
  127. package/src/driver.ts +205 -0
  128. package/src/durability.ts +380 -0
  129. package/src/index.ts +212 -27
  130. package/src/instance-store.ts +122 -0
  131. package/src/journal-bytes.ts +136 -0
  132. package/src/journal-reader.ts +359 -0
  133. package/src/journal-sweep.ts +406 -0
  134. package/src/journal.ts +875 -0
  135. package/src/middleware.ts +470 -30
  136. package/src/reap.ts +723 -0
  137. package/src/reclaim.ts +191 -0
  138. package/src/run.ts +365 -75
  139. package/src/runner.ts +347 -3
  140. package/src/sandbox.ts +38 -8
  141. package/src/shell.ts +106 -38
  142. package/src/testkit/conformance.ts +117 -0
  143. package/src/testkit/durable-run-fields-conformance.ts +147 -0
  144. package/src/testkit/journal-conformance.ts +676 -0
  145. package/src/testkit/reaper-conformance.ts +1201 -0
  146. package/src/testkit/shell-spawn.ts +67 -0
  147. package/src/testkit/takeover-conformance.ts +1040 -0
  148. package/src/tool-history.ts +245 -0
  149. package/src/workspace.ts +1 -1
  150. package/dist/esm/index.js.map +0 -1
  151. package/dist/esm/run-log.d.ts +0 -81
  152. package/dist/esm/run-log.js +0 -107
  153. package/dist/esm/run-log.js.map +0 -1
  154. package/dist/esm/store.d.ts +0 -53
  155. package/dist/esm/store.js +0 -34
  156. package/dist/esm/store.js.map +0 -1
  157. package/src/run-log.ts +0 -224
  158. package/src/store.ts +0 -83
package/dist/esm/shell.js CHANGED
@@ -1,164 +1,203 @@
1
+ //#region src/shell.ts
2
+ /**
3
+ * Parse the output of `export -p` (or `declare -x`) into a plain env map.
4
+ * Shared by the stdin shell's `forkState` and the exec-backed shell.
5
+ */
1
6
  function parseExports(output) {
2
- const env = {};
3
- for (const line of output.split("\n")) {
4
- const trimmed = line.trim();
5
- const match = /^(?:declare\s+-x\s+|export\s+)([A-Za-z_][A-Za-z0-9_]*)(?:="((?:[^"\\]|\\.)*)")?$/.exec(
6
- trimmed
7
- );
8
- if (match === null) continue;
9
- const key = match[1];
10
- if (key === void 0) continue;
11
- const raw = match[2];
12
- if (raw === void 0) continue;
13
- env[key] = raw.replace(/\\(.)/g, "$1");
14
- }
15
- return env;
7
+ const env = {};
8
+ for (const line of output.split("\n")) {
9
+ const trimmed = line.trim();
10
+ const match = /^(?:declare\s+-x\s+|export\s+)([A-Za-z_][A-Za-z0-9_]*)(?:="((?:[^"\\]|\\.)*)")?$/.exec(trimmed);
11
+ if (match === null) continue;
12
+ const key = match[1];
13
+ if (key === void 0) continue;
14
+ const raw = match[2];
15
+ if (raw === void 0) continue;
16
+ env[key] = raw.replace(/\\(.)/g, "$1");
17
+ }
18
+ return env;
16
19
  }
20
+ /** Default {@link BootstrapShellOptions.commandTimeoutMs} — 30 minutes. */
21
+ var DEFAULT_COMMAND_TIMEOUT_MS = 1800 * 1e3;
22
+ /** Race marker for the per-command deadline. A symbol cannot collide with a
23
+ * literal stdout line (a line of text `'timeout'` would). */
24
+ var TIMED_OUT = Symbol("bootstrap-shell-timeout");
25
+ /**
26
+ * Spawn one `sh` process and return a {@link BootstrapShell} that drives it
27
+ * via the sentinel-echo protocol.
28
+ *
29
+ * Protocol: for each `run(cmd)` call, we write
30
+ * `<cmd>; printf "\n__BSSH_<N>__ $?\n"` to stdin, then read stdout lines
31
+ * until we see a line matching `__BSSH_<N>__ <exitCode>`. Everything before
32
+ * that line is the command's stdout; the trailing integer is the exit code.
33
+ * The counter `N` is a module-level monotonic integer — no Date.now / random.
34
+ */
17
35
  async function createBootstrapShell(handle, opts = {}) {
18
- if (!handle.capabilities.writableStdin) {
19
- return createExecBootstrapShell(handle, opts);
20
- }
21
- const proc = await handle.process.spawn("sh", { cwd: opts.cwd });
22
- const lineBuffer = [];
23
- let pending = [];
24
- let streamDone = false;
25
- async function drainStdout() {
26
- let partial = "";
27
- for await (const chunk of proc.stdout) {
28
- partial += chunk;
29
- const parts = partial.split("\n");
30
- for (let i = 0; i < parts.length - 1; i++) {
31
- const line = parts[i];
32
- const resolver = pending.shift();
33
- if (resolver !== void 0) {
34
- resolver(line);
35
- } else {
36
- lineBuffer.push(line);
37
- }
38
- }
39
- partial = parts[parts.length - 1];
40
- }
41
- if (partial.length > 0) {
42
- const line = partial;
43
- const resolver = pending.shift();
44
- if (resolver !== void 0) {
45
- resolver(line);
46
- } else {
47
- lineBuffer.push(line);
48
- }
49
- }
50
- streamDone = true;
51
- for (const resolver of pending) {
52
- resolver("");
53
- }
54
- pending = [];
55
- }
56
- const drainPromise = drainStdout();
57
- function nextLine() {
58
- const buffered = lineBuffer.shift();
59
- if (buffered !== void 0) {
60
- return Promise.resolve(buffered);
61
- }
62
- if (streamDone) {
63
- return Promise.resolve("");
64
- }
65
- return new Promise((resolve) => {
66
- pending.push(resolve);
67
- });
68
- }
69
- let counter = 0;
70
- async function run(command) {
71
- const id = counter;
72
- counter += 1;
73
- const sentinel = `__BSSH_${id}__`;
74
- await proc.stdin.write(
75
- `{ ${command} ; } 2>&1; printf "\\n${sentinel} $?\\n"
76
- `
77
- );
78
- const outputLines = [];
79
- for (; ; ) {
80
- const line = await nextLine();
81
- if (line.startsWith(`${sentinel} `)) {
82
- const codeStr = line.slice(sentinel.length + 1).trim();
83
- const exitCode = parseInt(codeStr, 10);
84
- return {
85
- exitCode: Number.isFinite(exitCode) ? exitCode : 1,
86
- stdout: outputLines.join("\n")
87
- };
88
- }
89
- outputLines.push(line);
90
- }
91
- }
92
- async function forkState() {
93
- const pwdResult = await run("pwd");
94
- const cwd = pwdResult.stdout.trim();
95
- const exportResult = await run("export -p");
96
- return { cwd, env: parseExports(exportResult.stdout) };
97
- }
98
- async function dispose() {
99
- await proc.stdin.end();
100
- await proc.kill();
101
- await drainPromise;
102
- }
103
- return { run, forkState, dispose };
36
+ if (!handle.capabilities.writableStdin) return createExecBootstrapShell(handle, opts);
37
+ const proc = await handle.process.spawn("sh", { cwd: opts.cwd });
38
+ const lineBuffer = [];
39
+ let pending = [];
40
+ let streamDone = false;
41
+ let streamError;
42
+ /** Feed the stdout async-iterable into the shared line queue. */
43
+ async function drainStdout() {
44
+ let partial = "";
45
+ try {
46
+ for await (const chunk of proc.stdout) {
47
+ partial += chunk;
48
+ const parts = partial.split("\n");
49
+ for (let i = 0; i < parts.length - 1; i++) {
50
+ const line = parts[i];
51
+ const resolver = pending.shift();
52
+ if (resolver !== void 0) resolver(line);
53
+ else lineBuffer.push(line);
54
+ }
55
+ partial = parts[parts.length - 1];
56
+ }
57
+ if (partial.length > 0) {
58
+ const line = partial;
59
+ const resolver = pending.shift();
60
+ if (resolver !== void 0) resolver(line);
61
+ else lineBuffer.push(line);
62
+ }
63
+ } catch (error) {
64
+ streamError = error;
65
+ } finally {
66
+ streamDone = true;
67
+ for (const resolver of pending) resolver(null);
68
+ pending = [];
69
+ }
70
+ }
71
+ const drainPromise = drainStdout();
72
+ /** Read the next line from the shared queue, or `null` once stdout ended. */
73
+ function nextLine() {
74
+ const buffered = lineBuffer.shift();
75
+ if (buffered !== void 0) return Promise.resolve(buffered);
76
+ if (streamDone) return Promise.resolve(null);
77
+ return new Promise((resolve) => {
78
+ pending.push(resolve);
79
+ });
80
+ }
81
+ let counter = 0;
82
+ async function run(command) {
83
+ const id = counter;
84
+ counter += 1;
85
+ const sentinel = `__BSSH_${id}__`;
86
+ await proc.stdin.write(`{ ${command} ; } 2>&1; printf "\\n${sentinel} $?\\n"\n`);
87
+ const outputLines = [];
88
+ let timer;
89
+ const deadline = new Promise((resolve) => {
90
+ timer = setTimeout(() => resolve(TIMED_OUT), opts.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);
91
+ });
92
+ try {
93
+ for (;;) {
94
+ const line = await Promise.race([nextLine(), deadline]);
95
+ if (line === TIMED_OUT) throw new Error(`bootstrap shell: timed out after ${opts.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS}ms waiting for the sentinel of command: ${command}`);
96
+ if (line === null) throw new Error(`bootstrap shell: the shell exited before the sentinel was printed; command: ${command}`, streamError === void 0 ? void 0 : { cause: streamError });
97
+ if (line.startsWith(`${sentinel} `)) {
98
+ const codeStr = line.slice(sentinel.length + 1).trim();
99
+ const exitCode = parseInt(codeStr, 10);
100
+ return {
101
+ exitCode: Number.isFinite(exitCode) ? exitCode : 1,
102
+ stdout: outputLines.join("\n")
103
+ };
104
+ }
105
+ outputLines.push(line);
106
+ }
107
+ } finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
111
+ async function forkState() {
112
+ return {
113
+ cwd: (await run("pwd")).stdout.trim(),
114
+ env: parseExports((await run("export -p")).stdout)
115
+ };
116
+ }
117
+ async function dispose() {
118
+ await proc.stdin.end();
119
+ await proc.kill();
120
+ await drainPromise;
121
+ }
122
+ return {
123
+ run,
124
+ forkState,
125
+ dispose
126
+ };
104
127
  }
128
+ /**
129
+ * Exec-backed {@link BootstrapShell} for providers WITHOUT a writable stdin.
130
+ *
131
+ * There is no persistent process to feed commands into, so persistence of `cd`
132
+ * and exported variables is reproduced by threading state across discrete
133
+ * {@link SandboxHandle.process.exec} calls: each `run()` executes the command in
134
+ * the tracked cwd+env, then captures the resulting `pwd` and `export -p` (via
135
+ * marker lines) so the NEXT command inherits any directory change or exports.
136
+ */
105
137
  function createExecBootstrapShell(handle, opts = {}) {
106
- let cwd = opts.cwd ?? "/";
107
- let env = {};
108
- let counter = 0;
109
- async function run(command) {
110
- const id = counter;
111
- counter += 1;
112
- const sentinel = `__BSSH_${id}__`;
113
- const script = [
114
- command,
115
- `__bssh_rc=$?`,
116
- `printf '\\n%s %s\\n' '${sentinel}' "$__bssh_rc"`,
117
- `printf '%s\\n' '${sentinel}_CWD'`,
118
- `pwd`,
119
- `printf '%s\\n' '${sentinel}_ENV'`,
120
- `export -p`
121
- ].join("\n");
122
- const res = await handle.process.exec(script, { cwd, env });
123
- const cmdOut = [];
124
- const cwdLines = [];
125
- const envLines = [];
126
- let exitCode = res.exitCode;
127
- let phase = "cmd";
128
- for (const line of res.stdout.split("\n")) {
129
- if (phase === "cmd") {
130
- if (line.startsWith(`${sentinel} `)) {
131
- const parsed = parseInt(line.slice(sentinel.length + 1).trim(), 10);
132
- exitCode = Number.isFinite(parsed) ? parsed : res.exitCode;
133
- phase = "await-cwd";
134
- continue;
135
- }
136
- cmdOut.push(line);
137
- } else if (phase === "await-cwd") {
138
- if (line === `${sentinel}_CWD`) phase = "cwd";
139
- } else if (phase === "cwd") {
140
- if (line === `${sentinel}_ENV`) phase = "env";
141
- else cwdLines.push(line);
142
- } else {
143
- envLines.push(line);
144
- }
145
- }
146
- const newCwd = cwdLines.map((l) => l.trim()).filter(Boolean).pop();
147
- if (newCwd) cwd = newCwd;
148
- const newEnv = parseExports(envLines.join("\n"));
149
- if (Object.keys(newEnv).length > 0) env = newEnv;
150
- return { exitCode, stdout: cmdOut.join("\n") };
151
- }
152
- function forkState() {
153
- return Promise.resolve({ cwd, env: { ...env } });
154
- }
155
- function dispose() {
156
- return Promise.resolve();
157
- }
158
- return { run, forkState, dispose };
138
+ let cwd = opts.cwd ?? "/";
139
+ let env = {};
140
+ let counter = 0;
141
+ async function run(command) {
142
+ const id = counter;
143
+ counter += 1;
144
+ const sentinel = `__BSSH_${id}__`;
145
+ const script = [
146
+ command,
147
+ `__bssh_rc=$?`,
148
+ `printf '\\n%s %s\\n' '${sentinel}' "$__bssh_rc"`,
149
+ `printf '%s\\n' '${sentinel}_CWD'`,
150
+ `pwd`,
151
+ `printf '%s\\n' '${sentinel}_ENV'`,
152
+ `export -p`
153
+ ].join("\n");
154
+ const res = await handle.process.exec(script, {
155
+ cwd,
156
+ env
157
+ });
158
+ const cmdOut = [];
159
+ const cwdLines = [];
160
+ const envLines = [];
161
+ let exitCode = res.exitCode;
162
+ let phase = "cmd";
163
+ for (const line of res.stdout.split("\n")) if (phase === "cmd") {
164
+ if (line.startsWith(`${sentinel} `)) {
165
+ const parsed = parseInt(line.slice(sentinel.length + 1).trim(), 10);
166
+ exitCode = Number.isFinite(parsed) ? parsed : res.exitCode;
167
+ phase = "await-cwd";
168
+ continue;
169
+ }
170
+ cmdOut.push(line);
171
+ } else if (phase === "await-cwd") {
172
+ if (line === `${sentinel}_CWD`) phase = "cwd";
173
+ } else if (phase === "cwd") if (line === `${sentinel}_ENV`) phase = "env";
174
+ else cwdLines.push(line);
175
+ else envLines.push(line);
176
+ const newCwd = cwdLines.map((l) => l.trim()).filter(Boolean).pop();
177
+ if (newCwd) cwd = newCwd;
178
+ const newEnv = parseExports(envLines.join("\n"));
179
+ if (Object.keys(newEnv).length > 0) env = newEnv;
180
+ return {
181
+ exitCode,
182
+ stdout: cmdOut.join("\n")
183
+ };
184
+ }
185
+ function forkState() {
186
+ return Promise.resolve({
187
+ cwd,
188
+ env: { ...env }
189
+ });
190
+ }
191
+ function dispose() {
192
+ return Promise.resolve();
193
+ }
194
+ return {
195
+ run,
196
+ forkState,
197
+ dispose
198
+ };
159
199
  }
160
- export {
161
- createBootstrapShell,
162
- createExecBootstrapShell
163
- };
164
- //# sourceMappingURL=shell.js.map
200
+ //#endregion
201
+ export { createBootstrapShell, createExecBootstrapShell };
202
+
203
+ //# sourceMappingURL=shell.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shell.js","sources":["../../src/shell.ts"],"sourcesContent":["/**\n * Internal persistent bootstrap shell.\n *\n * Spawns a single `sh` process via {@link SandboxHandle.process.spawn} and\n * drives it over stdin/stdout with a sentinel-echo protocol. Commands run\n * sequentially inside the same shell so `cd`, exported variables, etc. persist\n * across calls — exactly the exec model the bootstrap setup plan needs.\n *\n * Providers WITHOUT a writable host→process stdin (`capabilities.writableStdin\n * === false`, e.g. Cloudflare / Daytona / Vercel) can't be driven over stdin, so\n * {@link createBootstrapShell} transparently falls back to an exec-backed shell\n * ({@link createExecBootstrapShell}) that threads `cwd`/env across `exec` calls\n * to reproduce the same persistent-shell semantics.\n *\n * This module is internal-only and must NOT be re-exported from\n * `packages/ai-sandbox/src/index.ts`.\n */\nimport type { SandboxHandle } from './contracts'\n\n/**\n * Parse the output of `export -p` (or `declare -x`) into a plain env map.\n * Shared by the stdin shell's `forkState` and the exec-backed shell.\n */\nfunction parseExports(output: string): Record<string, string> {\n const env: Record<string, string> = {}\n for (const line of output.split('\\n')) {\n const trimmed = line.trim()\n // Match `declare -x KEY=...` or `export KEY=...` forms.\n const match =\n /^(?:declare\\s+-x\\s+|export\\s+)([A-Za-z_][A-Za-z0-9_]*)(?:=\"((?:[^\"\\\\]|\\\\.)*)\")?$/.exec(\n trimmed,\n )\n if (match === null) continue\n const key = match[1]\n if (key === undefined) continue\n // Value may be absent for exported-but-unset vars; skip those.\n const raw = match[2]\n if (raw === undefined) continue\n // Unescape backslash-escaped chars inside double quotes.\n env[key] = raw.replace(/\\\\(.)/g, '$1')\n }\n return env\n}\n\n/** The surface the bootstrap engine uses. */\nexport interface BootstrapShell {\n /** Run a shell command and capture its stdout + exit code. */\n run: (command: string) => Promise<{ exitCode: number; stdout: string }>\n /**\n * Snapshot the shell's current working directory and exported environment.\n * Used to fork parallel exec calls that inherit the serial shell's state.\n */\n forkState: () => Promise<{ cwd: string; env: Record<string, string> }>\n /** End the shell session (closes stdin, kills the process). */\n dispose: () => Promise<void>\n}\n\n/** Options for {@link createBootstrapShell}. */\nexport interface BootstrapShellOptions {\n /** Working directory to start the shell in (passed as ProcessOptions.cwd). */\n cwd?: string\n}\n\n/**\n * Spawn one `sh` process and return a {@link BootstrapShell} that drives it\n * via the sentinel-echo protocol.\n *\n * Protocol: for each `run(cmd)` call, we write\n * `<cmd>; printf \"\\n__BSSH_<N>__ $?\\n\"` to stdin, then read stdout lines\n * until we see a line matching `__BSSH_<N>__ <exitCode>`. Everything before\n * that line is the command's stdout; the trailing integer is the exit code.\n * The counter `N` is a module-level monotonic integer — no Date.now / random.\n */\nexport async function createBootstrapShell(\n handle: SandboxHandle,\n opts: BootstrapShellOptions = {},\n): Promise<BootstrapShell> {\n // Providers without a writable host→process stdin can't run the sentinel-echo\n // protocol below (it feeds commands over stdin), so use the exec-backed shell.\n if (!handle.capabilities.writableStdin) {\n return createExecBootstrapShell(handle, opts)\n }\n const proc = await handle.process.spawn('sh', { cwd: opts.cwd })\n\n /*\n * We need to read stdout lines across multiple run() calls while keeping\n * the iterator open. Buffer chunks into lines manually.\n */\n const lineBuffer: Array<string> = []\n let pending: Array<(line: string) => void> = []\n let streamDone = false\n\n /** Feed the stdout async-iterable into the shared line queue. */\n async function drainStdout(): Promise<void> {\n let partial = ''\n for await (const chunk of proc.stdout) {\n partial += chunk\n const parts = partial.split('\\n')\n // All but the last element are complete lines.\n for (let i = 0; i < parts.length - 1; i++) {\n const line = parts[i] as string\n const resolver = pending.shift()\n if (resolver !== undefined) {\n resolver(line)\n } else {\n lineBuffer.push(line)\n }\n }\n partial = parts[parts.length - 1] as string\n }\n // Flush any trailing partial line.\n if (partial.length > 0) {\n const line = partial\n const resolver = pending.shift()\n if (resolver !== undefined) {\n resolver(line)\n } else {\n lineBuffer.push(line)\n }\n }\n streamDone = true\n // Resolve any remaining waiters with an empty sentinel so they unblock.\n for (const resolver of pending) {\n resolver('')\n }\n pending = []\n }\n\n // Start draining immediately; do NOT await — runs concurrently.\n const drainPromise = drainStdout()\n\n /** Read the next line from the shared queue. */\n function nextLine(): Promise<string> {\n const buffered = lineBuffer.shift()\n if (buffered !== undefined) {\n return Promise.resolve(buffered)\n }\n if (streamDone) {\n return Promise.resolve('')\n }\n return new Promise<string>((resolve) => {\n pending.push(resolve)\n })\n }\n\n let counter = 0\n\n async function run(\n command: string,\n ): Promise<{ exitCode: number; stdout: string }> {\n const id = counter\n counter += 1\n const sentinel = `__BSSH_${id}__`\n\n // Write the command followed by a sentinel printf to stdin. Merge the\n // command's stderr into stdout (`{ … ; } 2>&1`) so a failing setup step's\n // error text is captured and can be surfaced — otherwise only the exit code\n // is visible. `$?` after the group is still the command's own exit code.\n await proc.stdin.write(\n `{ ${command} ; } 2>&1; printf \"\\\\n${sentinel} $?\\\\n\"\\n`,\n )\n\n const outputLines: Array<string> = []\n\n // Read lines until we find the sentinel.\n for (;;) {\n const line = await nextLine()\n if (line.startsWith(`${sentinel} `)) {\n const codeStr = line.slice(sentinel.length + 1).trim()\n const exitCode = parseInt(codeStr, 10)\n return {\n exitCode: Number.isFinite(exitCode) ? exitCode : 1,\n stdout: outputLines.join('\\n'),\n }\n }\n outputLines.push(line)\n }\n }\n\n async function forkState(): Promise<{\n cwd: string\n env: Record<string, string>\n }> {\n const pwdResult = await run('pwd')\n const cwd = pwdResult.stdout.trim()\n\n const exportResult = await run('export -p')\n return { cwd, env: parseExports(exportResult.stdout) }\n }\n\n async function dispose(): Promise<void> {\n await proc.stdin.end()\n await proc.kill()\n // Drain the stdout iterator to completion so there are no dangling promises.\n await drainPromise\n }\n\n return { run, forkState, dispose }\n}\n\n/**\n * Exec-backed {@link BootstrapShell} for providers WITHOUT a writable stdin.\n *\n * There is no persistent process to feed commands into, so persistence of `cd`\n * and exported variables is reproduced by threading state across discrete\n * {@link SandboxHandle.process.exec} calls: each `run()` executes the command in\n * the tracked cwd+env, then captures the resulting `pwd` and `export -p` (via\n * marker lines) so the NEXT command inherits any directory change or exports.\n */\nexport function createExecBootstrapShell(\n handle: SandboxHandle,\n opts: BootstrapShellOptions = {},\n): BootstrapShell {\n let cwd = opts.cwd ?? '/'\n let env: Record<string, string> = {}\n let counter = 0\n\n async function run(\n command: string,\n ): Promise<{ exitCode: number; stdout: string }> {\n const id = counter\n counter += 1\n const sentinel = `__BSSH_${id}__`\n\n // Run the command, then emit its exit code, cwd and exported env behind\n // marker lines so we can recover state even when the command itself fails\n // (no `set -e`). Capturing `$?` immediately after the command keeps the\n // reported exit code the command's own, not the trailing introspection's.\n const script = [\n command,\n `__bssh_rc=$?`,\n `printf '\\\\n%s %s\\\\n' '${sentinel}' \"$__bssh_rc\"`,\n `printf '%s\\\\n' '${sentinel}_CWD'`,\n `pwd`,\n `printf '%s\\\\n' '${sentinel}_ENV'`,\n `export -p`,\n ].join('\\n')\n\n const res = await handle.process.exec(script, { cwd, env })\n\n const cmdOut: Array<string> = []\n const cwdLines: Array<string> = []\n const envLines: Array<string> = []\n let exitCode = res.exitCode\n let phase: 'cmd' | 'await-cwd' | 'cwd' | 'env' = 'cmd'\n\n for (const line of res.stdout.split('\\n')) {\n if (phase === 'cmd') {\n if (line.startsWith(`${sentinel} `)) {\n const parsed = parseInt(line.slice(sentinel.length + 1).trim(), 10)\n exitCode = Number.isFinite(parsed) ? parsed : res.exitCode\n phase = 'await-cwd'\n continue\n }\n cmdOut.push(line)\n } else if (phase === 'await-cwd') {\n if (line === `${sentinel}_CWD`) phase = 'cwd'\n } else if (phase === 'cwd') {\n if (line === `${sentinel}_ENV`) phase = 'env'\n else cwdLines.push(line)\n } else {\n envLines.push(line)\n }\n }\n\n // `pwd` prints a single line; the last non-empty one is the new cwd.\n const newCwd = cwdLines\n .map((l) => l.trim())\n .filter(Boolean)\n .pop()\n if (newCwd) cwd = newCwd\n const newEnv = parseExports(envLines.join('\\n'))\n if (Object.keys(newEnv).length > 0) env = newEnv\n\n return { exitCode, stdout: cmdOut.join('\\n') }\n }\n\n function forkState(): Promise<{ cwd: string; env: Record<string, string> }> {\n return Promise.resolve({ cwd, env: { ...env } })\n }\n\n function dispose(): Promise<void> {\n // Nothing to tear down — there is no persistent process.\n return Promise.resolve()\n }\n\n return { run, forkState, dispose }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shell.js","names":[],"sources":["../../src/shell.ts"],"sourcesContent":["/**\n * Internal persistent bootstrap shell.\n *\n * Spawns a single `sh` process via {@link SandboxHandle.process.spawn} and\n * drives it over stdin/stdout with a sentinel-echo protocol. Commands run\n * sequentially inside the same shell so `cd`, exported variables, etc. persist\n * across calls — exactly the exec model the bootstrap setup plan needs.\n *\n * Providers WITHOUT a writable host→process stdin (`capabilities.writableStdin\n * === false`, e.g. Cloudflare / Daytona / Vercel) can't be driven over stdin, so\n * {@link createBootstrapShell} transparently falls back to an exec-backed shell\n * ({@link createExecBootstrapShell}) that threads `cwd`/env across `exec` calls\n * to reproduce the same persistent-shell semantics.\n *\n * This module is internal-only and must NOT be re-exported from\n * `packages/ai-sandbox/src/index.ts`.\n */\nimport type { SandboxHandle } from './contracts'\n\n/**\n * Parse the output of `export -p` (or `declare -x`) into a plain env map.\n * Shared by the stdin shell's `forkState` and the exec-backed shell.\n */\nfunction parseExports(output: string): Record<string, string> {\n const env: Record<string, string> = {}\n for (const line of output.split('\\n')) {\n const trimmed = line.trim()\n // Match `declare -x KEY=...` or `export KEY=...` forms.\n const match =\n /^(?:declare\\s+-x\\s+|export\\s+)([A-Za-z_][A-Za-z0-9_]*)(?:=\"((?:[^\"\\\\]|\\\\.)*)\")?$/.exec(\n trimmed,\n )\n if (match === null) continue\n const key = match[1]\n if (key === undefined) continue\n // Value may be absent for exported-but-unset vars; skip those.\n const raw = match[2]\n if (raw === undefined) continue\n // Unescape backslash-escaped chars inside double quotes.\n env[key] = raw.replace(/\\\\(.)/g, '$1')\n }\n return env\n}\n\n/** The surface the bootstrap engine uses. */\nexport interface BootstrapShell {\n /** Run a shell command and capture its stdout + exit code. */\n run: (command: string) => Promise<{ exitCode: number; stdout: string }>\n /**\n * Snapshot the shell's current working directory and exported environment.\n * Used to fork parallel exec calls that inherit the serial shell's state.\n */\n forkState: () => Promise<{ cwd: string; env: Record<string, string> }>\n /** End the shell session (closes stdin, kills the process). */\n dispose: () => Promise<void>\n}\n\n/** Options for {@link createBootstrapShell}. */\nexport interface BootstrapShellOptions {\n /** Working directory to start the shell in (passed as ProcessOptions.cwd). */\n cwd?: string\n /**\n * Belt-and-braces deadline for a single `run()` to see its sentinel. The\n * primary termination condition is the stdout stream ending (see\n * {@link createBootstrapShell}); this only catches a shell that is alive,\n * silent, and never going to answer. Generous by default because setup steps\n * legitimately run for a long time (`npm install`, image pulls).\n */\n commandTimeoutMs?: number\n}\n\n/** Default {@link BootstrapShellOptions.commandTimeoutMs} — 30 minutes. */\nconst DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60 * 1000\n\n/** Race marker for the per-command deadline. A symbol cannot collide with a\n * literal stdout line (a line of text `'timeout'` would). */\nconst TIMED_OUT = Symbol('bootstrap-shell-timeout')\n\n/**\n * Spawn one `sh` process and return a {@link BootstrapShell} that drives it\n * via the sentinel-echo protocol.\n *\n * Protocol: for each `run(cmd)` call, we write\n * `<cmd>; printf \"\\n__BSSH_<N>__ $?\\n\"` to stdin, then read stdout lines\n * until we see a line matching `__BSSH_<N>__ <exitCode>`. Everything before\n * that line is the command's stdout; the trailing integer is the exit code.\n * The counter `N` is a module-level monotonic integer — no Date.now / random.\n */\nexport async function createBootstrapShell(\n handle: SandboxHandle,\n opts: BootstrapShellOptions = {},\n): Promise<BootstrapShell> {\n // Providers without a writable host→process stdin can't run the sentinel-echo\n // protocol below (it feeds commands over stdin), so use the exec-backed shell.\n if (!handle.capabilities.writableStdin) {\n return createExecBootstrapShell(handle, opts)\n }\n const proc = await handle.process.spawn('sh', { cwd: opts.cwd })\n\n /*\n * We need to read stdout lines across multiple run() calls while keeping\n * the iterator open. Buffer chunks into lines manually.\n */\n const lineBuffer: Array<string> = []\n // `null` means \"the stdout stream ended\" — distinct from an empty line, which\n // `sh` emits constantly. Collapsing the two is what let a dead shell feed an\n // infinite supply of `''` into a sentinel-hunting loop.\n let pending: Array<(line: string | null) => void> = []\n let streamDone = false\n let streamError: unknown\n\n /** Feed the stdout async-iterable into the shared line queue. */\n async function drainStdout(): Promise<void> {\n let partial = ''\n try {\n for await (const chunk of proc.stdout) {\n partial += chunk\n const parts = partial.split('\\n')\n // All but the last element are complete lines.\n for (let i = 0; i < parts.length - 1; i++) {\n const line = parts[i] as string\n const resolver = pending.shift()\n if (resolver !== undefined) {\n resolver(line)\n } else {\n lineBuffer.push(line)\n }\n }\n partial = parts[parts.length - 1] as string\n }\n // Flush any trailing partial line.\n if (partial.length > 0) {\n const line = partial\n const resolver = pending.shift()\n if (resolver !== undefined) {\n resolver(line)\n } else {\n lineBuffer.push(line)\n }\n }\n } catch (error) {\n // A throw while iterating stdout (transport reset, provider stream error)\n // must NOT leave waiters parked on a promise nobody resolves. Record it so\n // `run()` can name the cause, and fall through to the `finally` that\n // unblocks everyone.\n streamError = error\n } finally {\n streamDone = true\n // Unblock any remaining waiters with the end-of-stream marker.\n for (const resolver of pending) {\n resolver(null)\n }\n pending = []\n }\n }\n\n /*\n * Start draining immediately; do NOT await — runs concurrently. The `try/catch`\n * inside `drainStdout` means this promise never rejects, so there is no\n * unhandled rejection while nothing is awaiting it, and `dispose()` can await\n * it unconditionally.\n */\n const drainPromise = drainStdout()\n\n /** Read the next line from the shared queue, or `null` once stdout ended. */\n function nextLine(): Promise<string | null> {\n const buffered = lineBuffer.shift()\n if (buffered !== undefined) {\n return Promise.resolve(buffered)\n }\n if (streamDone) {\n return Promise.resolve(null)\n }\n return new Promise<string | null>((resolve) => {\n pending.push(resolve)\n })\n }\n\n let counter = 0\n\n async function run(\n command: string,\n ): Promise<{ exitCode: number; stdout: string }> {\n const id = counter\n counter += 1\n const sentinel = `__BSSH_${id}__`\n\n // Write the command followed by a sentinel printf to stdin. Merge the\n // command's stderr into stdout (`{ … ; } 2>&1`) so a failing setup step's\n // error text is captured and can be surfaced — otherwise only the exit code\n // is visible. `$?` after the group is still the command's own exit code.\n await proc.stdin.write(\n `{ ${command} ; } 2>&1; printf \"\\\\n${sentinel} $?\\\\n\"\\n`,\n )\n\n const outputLines: Array<string> = []\n\n /*\n * Read lines until we find the sentinel — but the wait MUST be able to end\n * without one. `sh` can exit before it ever prints the sentinel (a missing\n * binary, an OOM kill, the provider reaping the sandbox mid-bootstrap), and\n * a loop whose only exit is the sentinel then spins on end-of-stream\n * forever, pushing into `outputLines` until the host process dies of memory\n * exhaustion. Two independent terminators:\n * 1. `nextLine()` yields `null` the moment stdout is done — the real fix,\n * it fires as soon as the shell is gone.\n * 2. A deadline, for a shell that stays alive and simply never answers.\n */\n let timer: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(\n () => resolve(TIMED_OUT),\n opts.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,\n )\n })\n\n try {\n for (;;) {\n const line = await Promise.race([nextLine(), deadline])\n if (line === TIMED_OUT) {\n throw new Error(\n `bootstrap shell: timed out after ${\n opts.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS\n }ms waiting for the sentinel of command: ${command}`,\n )\n }\n if (line === null) {\n throw new Error(\n `bootstrap shell: the shell exited before the sentinel was printed; command: ${command}`,\n streamError === undefined ? undefined : { cause: streamError },\n )\n }\n if (line.startsWith(`${sentinel} `)) {\n const codeStr = line.slice(sentinel.length + 1).trim()\n const exitCode = parseInt(codeStr, 10)\n return {\n exitCode: Number.isFinite(exitCode) ? exitCode : 1,\n stdout: outputLines.join('\\n'),\n }\n }\n outputLines.push(line)\n }\n } finally {\n clearTimeout(timer)\n }\n }\n\n async function forkState(): Promise<{\n cwd: string\n env: Record<string, string>\n }> {\n const pwdResult = await run('pwd')\n const cwd = pwdResult.stdout.trim()\n\n const exportResult = await run('export -p')\n return { cwd, env: parseExports(exportResult.stdout) }\n }\n\n async function dispose(): Promise<void> {\n await proc.stdin.end()\n await proc.kill()\n // Drain the stdout iterator to completion so there are no dangling promises.\n await drainPromise\n }\n\n return { run, forkState, dispose }\n}\n\n/**\n * Exec-backed {@link BootstrapShell} for providers WITHOUT a writable stdin.\n *\n * There is no persistent process to feed commands into, so persistence of `cd`\n * and exported variables is reproduced by threading state across discrete\n * {@link SandboxHandle.process.exec} calls: each `run()` executes the command in\n * the tracked cwd+env, then captures the resulting `pwd` and `export -p` (via\n * marker lines) so the NEXT command inherits any directory change or exports.\n */\nexport function createExecBootstrapShell(\n handle: SandboxHandle,\n opts: BootstrapShellOptions = {},\n): BootstrapShell {\n let cwd = opts.cwd ?? '/'\n let env: Record<string, string> = {}\n let counter = 0\n\n async function run(\n command: string,\n ): Promise<{ exitCode: number; stdout: string }> {\n const id = counter\n counter += 1\n const sentinel = `__BSSH_${id}__`\n\n // Run the command, then emit its exit code, cwd and exported env behind\n // marker lines so we can recover state even when the command itself fails\n // (no `set -e`). Capturing `$?` immediately after the command keeps the\n // reported exit code the command's own, not the trailing introspection's.\n const script = [\n command,\n `__bssh_rc=$?`,\n `printf '\\\\n%s %s\\\\n' '${sentinel}' \"$__bssh_rc\"`,\n `printf '%s\\\\n' '${sentinel}_CWD'`,\n `pwd`,\n `printf '%s\\\\n' '${sentinel}_ENV'`,\n `export -p`,\n ].join('\\n')\n\n const res = await handle.process.exec(script, { cwd, env })\n\n const cmdOut: Array<string> = []\n const cwdLines: Array<string> = []\n const envLines: Array<string> = []\n let exitCode = res.exitCode\n let phase: 'cmd' | 'await-cwd' | 'cwd' | 'env' = 'cmd'\n\n for (const line of res.stdout.split('\\n')) {\n if (phase === 'cmd') {\n if (line.startsWith(`${sentinel} `)) {\n const parsed = parseInt(line.slice(sentinel.length + 1).trim(), 10)\n exitCode = Number.isFinite(parsed) ? parsed : res.exitCode\n phase = 'await-cwd'\n continue\n }\n cmdOut.push(line)\n } else if (phase === 'await-cwd') {\n if (line === `${sentinel}_CWD`) phase = 'cwd'\n } else if (phase === 'cwd') {\n if (line === `${sentinel}_ENV`) phase = 'env'\n else cwdLines.push(line)\n } else {\n envLines.push(line)\n }\n }\n\n // `pwd` prints a single line; the last non-empty one is the new cwd.\n const newCwd = cwdLines\n .map((l) => l.trim())\n .filter(Boolean)\n .pop()\n if (newCwd) cwd = newCwd\n const newEnv = parseExports(envLines.join('\\n'))\n if (Object.keys(newEnv).length > 0) env = newEnv\n\n return { exitCode, stdout: cmdOut.join('\\n') }\n }\n\n function forkState(): Promise<{ cwd: string; env: Record<string, string> }> {\n return Promise.resolve({ cwd, env: { ...env } })\n }\n\n function dispose(): Promise<void> {\n // Nothing to tear down — there is no persistent process.\n return Promise.resolve()\n }\n\n return { run, forkState, dispose }\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { SandboxInstanceStore } from '../instance-store.js';
2
+ export { runJournalConformance } from './journal-conformance.js';
3
+ export type { JournalConformanceConfig } from './journal-conformance.js';
4
+ export { runTakeoverConformance } from './takeover-conformance.js';
5
+ export type { TakeoverConformanceConfig } from './takeover-conformance.js';
6
+ export { runReaperConformance } from './reaper-conformance.js';
7
+ export type { ReaperConformanceConfig } from './reaper-conformance.js';
8
+ export { runDurableRunFieldsConformance } from './durable-run-fields-conformance.js';
9
+ export type { MakeRunStore } from './durable-run-fields-conformance.js';
10
+ export { makeFakeShellSpawn } from './shell-spawn.js';
11
+ /**
12
+ * Assert `makeStore()` produces a spec-compliant {@link SandboxInstanceStore}. Each
13
+ * `it` gets a fresh store, so implementations may share process state across
14
+ * calls without cross-test bleed only if `makeStore` returns an isolated store.
15
+ */
16
+ export declare function runSandboxInstanceStoreConformance(name: string, makeStore: () => SandboxInstanceStore | Promise<SandboxInstanceStore>): void;
@@ -0,0 +1,97 @@
1
+ import { runJournalConformance } from "./journal-conformance.js";
2
+ import { runTakeoverConformance } from "./takeover-conformance.js";
3
+ import { runReaperConformance } from "./reaper-conformance.js";
4
+ import { runDurableRunFieldsConformance } from "./durable-run-fields-conformance.js";
5
+ import { makeFakeShellSpawn } from "./shell-spawn.js";
6
+ import { describe, expect, it } from "vitest";
7
+ //#region src/testkit/conformance.ts
8
+ /**
9
+ * Conformance suite for a {@link SandboxInstanceStore} implementation.
10
+ *
11
+ * Run this against a fresh BYO store (or the in-memory reference) to prove it
12
+ * satisfies the get / upsert / delete contract `@tanstack/ai-sandbox`'s ensure
13
+ * algorithm relies on — including insert-vs-overwrite and optional-field
14
+ * handling (full replace must clear omitted optionals).
15
+ *
16
+ * Vitest is an OPTIONAL peer dependency: this module is imported only from test
17
+ * files, which already run under Vitest.
18
+ */
19
+ function makeRecord(overrides) {
20
+ return {
21
+ key: "thread-1",
22
+ provider: "fake",
23
+ providerSandboxId: "sb-1",
24
+ threadId: "thread-1",
25
+ updatedAt: 1,
26
+ ...overrides
27
+ };
28
+ }
29
+ /**
30
+ * Assert `makeStore()` produces a spec-compliant {@link SandboxInstanceStore}. Each
31
+ * `it` gets a fresh store, so implementations may share process state across
32
+ * calls without cross-test bleed only if `makeStore` returns an isolated store.
33
+ */
34
+ function runSandboxInstanceStoreConformance(name, makeStore) {
35
+ describe(`SandboxInstanceStore conformance: ${name}`, () => {
36
+ it("returns null for a missing key", async () => {
37
+ expect(await (await makeStore()).get("absent")).toBeNull();
38
+ });
39
+ it("round-trips an upserted record with all fields", async () => {
40
+ const store = await makeStore();
41
+ const record = makeRecord({
42
+ latestSnapshotId: "snap-1",
43
+ latestRunId: "run-1"
44
+ });
45
+ await store.upsert(record);
46
+ expect(await store.get(record.key)).toEqual(record);
47
+ });
48
+ it("omits absent optional fields on read", async () => {
49
+ const store = await makeStore();
50
+ const record = makeRecord();
51
+ await store.upsert(record);
52
+ const loaded = await store.get(record.key);
53
+ expect(loaded).toEqual(record);
54
+ expect(loaded && "latestSnapshotId" in loaded).toBe(false);
55
+ expect(loaded && "latestRunId" in loaded).toBe(false);
56
+ });
57
+ it("overwrites an existing record on re-upsert", async () => {
58
+ const store = await makeStore();
59
+ await store.upsert(makeRecord({ latestSnapshotId: "snap-1" }));
60
+ await store.upsert(makeRecord({
61
+ providerSandboxId: "sb-2",
62
+ updatedAt: 2
63
+ }));
64
+ const loaded = await store.get("thread-1");
65
+ expect(loaded?.providerSandboxId).toBe("sb-2");
66
+ expect(loaded?.updatedAt).toBe(2);
67
+ expect(loaded && "latestSnapshotId" in loaded).toBe(false);
68
+ });
69
+ it("isolates records by key", async () => {
70
+ const store = await makeStore();
71
+ await store.upsert(makeRecord({
72
+ key: "a",
73
+ threadId: "a"
74
+ }));
75
+ await store.upsert(makeRecord({
76
+ key: "b",
77
+ threadId: "b",
78
+ providerSandboxId: "sb-b"
79
+ }));
80
+ expect((await store.get("a"))?.providerSandboxId).toBe("sb-1");
81
+ expect((await store.get("b"))?.providerSandboxId).toBe("sb-b");
82
+ });
83
+ it("deletes a record", async () => {
84
+ const store = await makeStore();
85
+ await store.upsert(makeRecord());
86
+ await store.delete("thread-1");
87
+ expect(await store.get("thread-1")).toBeNull();
88
+ });
89
+ it("delete of a missing key is a no-op", async () => {
90
+ await expect((await makeStore()).delete("absent")).resolves.toBeUndefined();
91
+ });
92
+ });
93
+ }
94
+ //#endregion
95
+ export { makeFakeShellSpawn, runDurableRunFieldsConformance, runJournalConformance, runReaperConformance, runSandboxInstanceStoreConformance, runTakeoverConformance };
96
+
97
+ //# sourceMappingURL=conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.js","names":[],"sources":["../../../src/testkit/conformance.ts"],"sourcesContent":["/**\n * Conformance suite for a {@link SandboxInstanceStore} implementation.\n *\n * Run this against a fresh BYO store (or the in-memory reference) to prove it\n * satisfies the get / upsert / delete contract `@tanstack/ai-sandbox`'s ensure\n * algorithm relies on — including insert-vs-overwrite and optional-field\n * handling (full replace must clear omitted optionals).\n *\n * Vitest is an OPTIONAL peer dependency: this module is imported only from test\n * files, which already run under Vitest.\n */\nimport { describe, expect, it } from 'vitest'\nimport type {\n SandboxInstanceRecord,\n SandboxInstanceStore,\n} from '../instance-store'\n\n// The `./testkit` package.json subpath resolves to THIS module's build output\n// only (`dist/esm/testkit/conformance.js`), not a barrel over `src/testkit/`.\n// Re-export the journal, takeover, and reaper conformance suites here so they\n// are reachable from `@tanstack/ai-sandbox/testkit` without changing that\n// mapping.\nexport { runJournalConformance } from './journal-conformance'\nexport type { JournalConformanceConfig } from './journal-conformance'\nexport { runTakeoverConformance } from './takeover-conformance'\nexport type { TakeoverConformanceConfig } from './takeover-conformance'\nexport { runReaperConformance } from './reaper-conformance'\nexport type { ReaperConformanceConfig } from './reaper-conformance'\nexport { runDurableRunFieldsConformance } from './durable-run-fields-conformance'\nexport type { MakeRunStore } from './durable-run-fields-conformance'\nexport { makeFakeShellSpawn } from './shell-spawn'\n\nfunction makeRecord(\n overrides?: Partial<SandboxInstanceRecord>,\n): SandboxInstanceRecord {\n return {\n key: 'thread-1',\n provider: 'fake',\n providerSandboxId: 'sb-1',\n threadId: 'thread-1',\n updatedAt: 1,\n ...overrides,\n }\n}\n\n/**\n * Assert `makeStore()` produces a spec-compliant {@link SandboxInstanceStore}. Each\n * `it` gets a fresh store, so implementations may share process state across\n * calls without cross-test bleed only if `makeStore` returns an isolated store.\n */\nexport function runSandboxInstanceStoreConformance(\n name: string,\n makeStore: () => SandboxInstanceStore | Promise<SandboxInstanceStore>,\n): void {\n describe(`SandboxInstanceStore conformance: ${name}`, () => {\n it('returns null for a missing key', async () => {\n const store = await makeStore()\n expect(await store.get('absent')).toBeNull()\n })\n\n it('round-trips an upserted record with all fields', async () => {\n const store = await makeStore()\n const record = makeRecord({\n latestSnapshotId: 'snap-1',\n latestRunId: 'run-1',\n })\n await store.upsert(record)\n expect(await store.get(record.key)).toEqual(record)\n })\n\n it('omits absent optional fields on read', async () => {\n const store = await makeStore()\n const record = makeRecord()\n await store.upsert(record)\n const loaded = await store.get(record.key)\n expect(loaded).toEqual(record)\n expect(loaded && 'latestSnapshotId' in loaded).toBe(false)\n expect(loaded && 'latestRunId' in loaded).toBe(false)\n })\n\n it('overwrites an existing record on re-upsert', async () => {\n const store = await makeStore()\n await store.upsert(makeRecord({ latestSnapshotId: 'snap-1' }))\n await store.upsert(\n makeRecord({ providerSandboxId: 'sb-2', updatedAt: 2 }),\n )\n const loaded = await store.get('thread-1')\n expect(loaded?.providerSandboxId).toBe('sb-2')\n expect(loaded?.updatedAt).toBe(2)\n // The overwrite dropped latestSnapshotId — a durable store must clear it,\n // not retain the prior value.\n expect(loaded && 'latestSnapshotId' in loaded).toBe(false)\n })\n\n it('isolates records by key', async () => {\n const store = await makeStore()\n await store.upsert(makeRecord({ key: 'a', threadId: 'a' }))\n await store.upsert(\n makeRecord({ key: 'b', threadId: 'b', providerSandboxId: 'sb-b' }),\n )\n expect((await store.get('a'))?.providerSandboxId).toBe('sb-1')\n expect((await store.get('b'))?.providerSandboxId).toBe('sb-b')\n })\n\n it('deletes a record', async () => {\n const store = await makeStore()\n await store.upsert(makeRecord())\n await store.delete('thread-1')\n expect(await store.get('thread-1')).toBeNull()\n })\n\n it('delete of a missing key is a no-op', async () => {\n const store = await makeStore()\n await expect(store.delete('absent')).resolves.toBeUndefined()\n })\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgCA,SAAS,WACP,WACuB;CACvB,OAAO;EACL,KAAK;EACL,UAAU;EACV,mBAAmB;EACnB,UAAU;EACV,WAAW;EACX,GAAG;CACL;AACF;;;;;;AAOA,SAAgB,mCACd,MACA,WACM;CACN,SAAS,qCAAqC,cAAc;EAC1D,GAAG,kCAAkC,YAAY;GAE/C,OAAO,OAAM,MADO,UAAU,EAAA,CACX,IAAI,QAAQ,CAAC,CAAC,CAAC,SAAS;EAC7C,CAAC;EAED,GAAG,kDAAkD,YAAY;GAC/D,MAAM,QAAQ,MAAM,UAAU;GAC9B,MAAM,SAAS,WAAW;IACxB,kBAAkB;IAClB,aAAa;GACf,CAAC;GACD,MAAM,MAAM,OAAO,MAAM;GACzB,OAAO,MAAM,MAAM,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,QAAQ,MAAM;EACpD,CAAC;EAED,GAAG,wCAAwC,YAAY;GACrD,MAAM,QAAQ,MAAM,UAAU;GAC9B,MAAM,SAAS,WAAW;GAC1B,MAAM,MAAM,OAAO,MAAM;GACzB,MAAM,SAAS,MAAM,MAAM,IAAI,OAAO,GAAG;GACzC,OAAO,MAAM,CAAC,CAAC,QAAQ,MAAM;GAC7B,OAAO,UAAU,sBAAsB,MAAM,CAAC,CAAC,KAAK,KAAK;GACzD,OAAO,UAAU,iBAAiB,MAAM,CAAC,CAAC,KAAK,KAAK;EACtD,CAAC;EAED,GAAG,8CAA8C,YAAY;GAC3D,MAAM,QAAQ,MAAM,UAAU;GAC9B,MAAM,MAAM,OAAO,WAAW,EAAE,kBAAkB,SAAS,CAAC,CAAC;GAC7D,MAAM,MAAM,OACV,WAAW;IAAE,mBAAmB;IAAQ,WAAW;GAAE,CAAC,CACxD;GACA,MAAM,SAAS,MAAM,MAAM,IAAI,UAAU;GACzC,OAAO,QAAQ,iBAAiB,CAAC,CAAC,KAAK,MAAM;GAC7C,OAAO,QAAQ,SAAS,CAAC,CAAC,KAAK,CAAC;GAGhC,OAAO,UAAU,sBAAsB,MAAM,CAAC,CAAC,KAAK,KAAK;EAC3D,CAAC;EAED,GAAG,2BAA2B,YAAY;GACxC,MAAM,QAAQ,MAAM,UAAU;GAC9B,MAAM,MAAM,OAAO,WAAW;IAAE,KAAK;IAAK,UAAU;GAAI,CAAC,CAAC;GAC1D,MAAM,MAAM,OACV,WAAW;IAAE,KAAK;IAAK,UAAU;IAAK,mBAAmB;GAAO,CAAC,CACnE;GACA,QAAQ,MAAM,MAAM,IAAI,GAAG,EAAA,EAAI,iBAAiB,CAAC,CAAC,KAAK,MAAM;GAC7D,QAAQ,MAAM,MAAM,IAAI,GAAG,EAAA,EAAI,iBAAiB,CAAC,CAAC,KAAK,MAAM;EAC/D,CAAC;EAED,GAAG,oBAAoB,YAAY;GACjC,MAAM,QAAQ,MAAM,UAAU;GAC9B,MAAM,MAAM,OAAO,WAAW,CAAC;GAC/B,MAAM,MAAM,OAAO,UAAU;GAC7B,OAAO,MAAM,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,SAAS;EAC/C,CAAC;EAED,GAAG,sCAAsC,YAAY;GAEnD,MAAM,QAAO,MADO,UAAU,EAAA,CACX,OAAO,QAAQ,CAAC,CAAC,CAAC,SAAS,cAAc;EAC9D,CAAC;CACH,CAAC;AACH"}
@@ -0,0 +1,4 @@
1
+ import { RunStore } from '@tanstack/ai';
2
+ /** Factory for the store under test. A fresh one per case keeps them isolated. */
3
+ export type MakeRunStore = () => RunStore | Promise<RunStore>;
4
+ export declare function runDurableRunFieldsConformance(name: string, makeStore: MakeRunStore): void;