Skip to content
File

Blob: src/workerd/server/tests/container-client/test.js

javascript3017 lines
1// Copyright (c) 2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
5import assert from 'node:assert';
6import { scheduler } from 'node:timers/promises';
7 
8// 5s timeout for some of the requests going to the container.
9// We can get to have a stack trace with an
10// abort signal.
11const DEFAULT_TIMEOUT_DURATION = 10_000;
12 
13// Use a unique DO name per test invocation because different test flavors may
14// run concurrently, and this avoids them accidentally sharing the same object.
15function getRandomDurableObjectName(name) {
16 return `${name}-${crypto.randomUUID()}`;
17}
18 
19// **IMPORTANT NOTE**
20//
21// When writing a test, don't forget to call waitUntilContainerIsHealthy
22// before testing the behaviour with your container.
23//
24// Don't forget to call monitor() after calling start(), as there
25// is an issue with not calling monitor() in Durable Objects where
26// we might lose track of the container lifetime.
27//
28 
29export class DurableObjectExample extends DurableObject {
30 async testExitCode() {
31 const container = this.ctx.container;
32 if (container.running) {
33 let monitor = container.monitor().catch((_err) => {});
34 await container.destroy();
35 await monitor;
36 }
37 assert.strictEqual(container.running, false);
38 
39 // Start container with invalid entrypoint
40 {
41 container.start({
42 entrypoint: ['node', 'nonexistant.js'],
43 });
44 
45 let exitCode = undefined;
46 await container.monitor().catch((err) => {
47 exitCode = err.exitCode;
48 });
49 
50 assert.strictEqual(typeof exitCode, 'number');
51 assert.notEqual(0, exitCode);
52 }
53 
54 // Start container with valid entrypoint and stop it
55 {
56 container.start();
57 
58 await scheduler.wait(500);
59 
60 let exitCode = undefined;
61 const monitor = container.monitor().catch((err) => {
62 exitCode = err.exitCode;
63 });
64 await container.destroy();
65 await monitor;
66 
67 assert.strictEqual(typeof exitCode, 'number');
68 assert.equal(137, exitCode);
69 }
70 }
71 
72 async testBasics() {
73 const container = this.ctx.container;
74 if (container.running) {
75 let monitor = container.monitor().catch((_err) => {});
76 
77 await container.destroy();
78 await monitor;
79 }
80 
81 assert.strictEqual(container.running, false);
82 
83 // Start container with valid configuration
84 container.start({
85 env: { A: 'B', C: 'D', L: 'F' },
86 enableInternet: true,
87 });
88 
89 const monitor = container.monitor().catch((_err) => {});
90 
91 await this.waitUntilContainerIsHealthy();
92 
93 await container.destroy();
94 
95 await monitor;
96 assert.strictEqual(container.running, false);
97 }
98 
99 async testExec() {
100 const container = this.ctx.container;
101 if (container.running) {
102 const monitor = container.monitor().catch((_err) => {});
103 await container.destroy();
104 await monitor;
105 }
106 
107 assert.strictEqual(container.running, false);
108 
109 container.start({
110 env: { EXEC_BASE: 'from-start' },
111 enableInternet: true,
112 });
113 
114 const monitor = container.monitor().catch((_err) => {});
115 const textEncoder = new TextEncoder();
116 const textDecoder = new TextDecoder();
117 const decode = (buffer) => textDecoder.decode(buffer);
118 const countStreamBytes = async (stream) => {
119 assert.ok(stream);
120 
121 const reader = stream.getReader();
122 let total = 0;
123 try {
124 for (;;) {
125 const { value, done } = await reader.read();
126 if (done) {
127 return total;
128 }
129 
130 total += value.byteLength;
131 }
132 } finally {
133 reader.releaseLock();
134 }
135 };
136 
137 await this.waitUntilContainerIsHealthy();
138 
139 // 1. Read stdout directly as a stream.
140 {
141 const proc = await container.exec(['cat', '/etc/hostname']);
142 assert.ok(proc.pid > 0);
143 const stdout = await new Response(proc.stdout).text();
144 assert.ok(stdout.trim().length > 0);
145 assert.strictEqual(await proc.exitCode, 0);
146 }
147 
148 // 2. Create a file from a ReadableStream stdin using tee.
149 {
150 const content = '{"hello":"world","kind":"stream"}\n';
151 const proc = await container.exec(['tee', '/tmp/exec-stream.json'], {
152 stdin: new ReadableStream({
153 start(controller) {
154 controller.enqueue(textEncoder.encode(content));
155 controller.close();
156 },
157 }),
158 stdout: 'ignore',
159 });
160 assert.strictEqual(await proc.exitCode, 0);
161 
162 const verify = await (
163 await container.exec(['cat', '/tmp/exec-stream.json'])
164 ).output();
165 
166 assert.strictEqual(decode(verify.stdout), content);
167 assert.strictEqual(verify.exitCode, 0);
168 }
169 
170 // 3. Feed stdin interactively through the exposed WritableStream.
171 {
172 const proc = await container.exec(
173 ['sh', '-lc', 'cat > /tmp/exec-pipe.txt'],
174 {
175 stdin: 'pipe',
176 stdout: 'ignore',
177 }
178 );
179 assert.ok(proc.stdin);
180 
181 const writer = proc.stdin.getWriter();
182 await writer.write(textEncoder.encode('alpha\n'));
183 await writer.write(textEncoder.encode('beta\n'));
184 await writer.close();
185 
186 assert.strictEqual(await proc.exitCode, 0);
187 
188 const verify = await (
189 await container.exec(['cat', '/tmp/exec-pipe.txt'])
190 ).output();
191 assert.strictEqual(decode(verify.stdout), 'alpha\nbeta\n');
192 }
193 
194 // 4. Override working directory for commands that rely on relative paths.
195 {
196 const proc = await container.exec(['pwd'], { cwd: '/tmp' });
197 const output = await proc.output();
198 assert.strictEqual(decode(output.stdout).trim(), '/tmp');
199 assert.strictEqual(output.exitCode, 0);
200 }
201 
202 // 5. Merge container env with per-exec overrides.
203 {
204 const proc = await container.exec(
205 ['sh', '-lc', 'printf "%s|%s" "$EXEC_BASE" "$EXEC_EXTRA"'],
206 {
207 env: {
208 EXEC_BASE: 'overridden',
209 EXEC_EXTRA: 'per-exec',
210 },
211 }
212 );
213 const output = await proc.output();
214 assert.strictEqual(decode(output.stdout), 'overridden|per-exec');
215 assert.strictEqual(output.exitCode, 0);
216 }
217 
218 // 6. Capture stdout and stderr separately.
219 {
220 const proc = await container.exec([
221 'sh',
222 '-lc',
223 'printf "out"; printf "err" >&2',
224 ]);
225 const output = await proc.output();
226 assert.strictEqual(decode(output.stdout), 'out');
227 assert.strictEqual(decode(output.stderr), 'err');
228 assert.strictEqual(output.exitCode, 0);
229 }
230 
231 // 7. Combine stderr into stdout for shell-style command output.
232 {
233 const proc = await container.exec(
234 ['sh', '-lc', 'printf "out"; printf "err" >&2'],
235 { stderr: 'combined' }
236 );
237 const output = await proc.output();
238 const stdout = decode(output.stdout);
239 if (stdout !== 'outerr') {
240 assert.strictEqual(decode(output.stdout), 'errout');
241 }
242 
243 assert.strictEqual(decode(output.stderr), '');
244 assert.strictEqual(output.exitCode, 0);
245 }
246 
247 // 8. Ignore stdout when only success/failure matters.
248 {
249 const proc = await container.exec(['sh', '-lc', 'printf "ignore-me"'], {
250 stdout: 'ignore',
251 });
252 const output = await proc.output();
253 assert.strictEqual(decode(output.stdout), '');
254 assert.strictEqual(decode(output.stderr), '');
255 assert.strictEqual(output.exitCode, 0);
256 }
257 
258 // 9. Preserve stderr and non-zero exit codes for failures.
259 {
260 const proc = await container.exec([
261 'sh',
262 '-lc',
263 'printf "boom" >&2; exit 7',
264 ]);
265 const output = await proc.output();
266 assert.strictEqual(decode(output.stdout), '');
267 assert.strictEqual(decode(output.stderr), 'boom');
268 assert.strictEqual(output.exitCode, 7);
269 }
270 
271 // 10. Stream-consume large stdout and stderr payloads concurrently without buffering them in
272 // JS memory.
273 {
274 const expectedBytes = 64 * 1024 * 1024;
275 const proc = await container.exec([
276 'sh',
277 '-lc',
278 `head -c ${expectedBytes} /dev/zero & head -c ${expectedBytes} /dev/zero >&2 & wait`,
279 ]);
280 
281 const [stdoutBytes, stderrBytes, exitCode] = await Promise.all([
282 countStreamBytes(proc.stdout),
283 countStreamBytes(proc.stderr),
284 proc.exitCode,
285 ]);
286 
287 assert.strictEqual(stdoutBytes, expectedBytes);
288 assert.strictEqual(stderrBytes, expectedBytes);
289 assert.strictEqual(exitCode, 0);
290 }
291 
292 // 11. Check we throw an error when calling output() after reading from stdout
293 {
294 const proc = await container.exec(['echo', 'hello']);
295 await proc.stdout.getReader().read();
296 assert.rejects(() => proc.output(), {
297 name: 'TypeError',
298 message:
299 'Cannot call output() after stdout has started being consumed.',
300 });
301 }
302 
303 // 12. Make sure Stdin EOF's by default if not set
304 {
305 await container.exec(['cat']).then((p) => p.output());
306 }
307 
308 await container.destroy();
309 await monitor;
310 assert.strictEqual(container.running, false);
311 }
312 
313 async testSetInactivityTimeout(timeout) {
314 const container = this.ctx.container;
315 if (container.running) {
316 let monitor = container.monitor().catch((_err) => {});
317 await container.destroy();
318 await monitor;
319 }
320 assert.strictEqual(container.running, false);
321 
322 container.start();
323 
324 assert.strictEqual(container.running, true);
325 
326 // Wait for container to be running
327 await scheduler.wait(500);
328 
329 try {
330 await container.setInactivityTimeout(0);
331 } catch (err) {
332 assert.strictEqual(err.name, 'TypeError');
333 assert.match(
334 err.message,
335 /setInactivityTimeout\(\) cannot be called with a durationMs <= 0/
336 );
337 }
338 
339 if (timeout > 0) {
340 await container.setInactivityTimeout(timeout);
341 }
342 }
343 
344 async start() {
345 assert.strictEqual(this.ctx.container.running, false);
346 this.ctx.container.start();
347 assert.strictEqual(this.ctx.container.running, true);
348 
349 // Wait for container to be running
350 await scheduler.wait(500);
351 }
352 
353 // Assert that the container is running
354 async expectRunning(running) {
355 assert.strictEqual(this.ctx.container.running, running);
356 await this.ctx.container.destroy();
357 }
358 
359 async abort() {
360 await this.ctx.storage.put('aborted', true);
361 await this.ctx.storage.sync();
362 this.ctx.abort();
363 }
364 
365 async alarm() {
366 const alarmValue = (await this.ctx.storage.get('alarm')) ?? 0;
367 
368 const aborted = await this.ctx.storage.get('aborted');
369 assert.strictEqual(!!this.ctx.container, true);
370 if (aborted) {
371 await this.ctx.storage.put('aborted-confirmed', true);
372 }
373 
374 await this.ctx.storage.put('alarm', alarmValue + 1);
375 }
376 
377 async getAlarmIndex() {
378 return (await this.ctx.storage.get('alarm')) ?? 0;
379 }
380 
381 async startAlarm(start, ms) {
382 if (start && !this.ctx.container.running) {
383 this.ctx.container.start();
384 }
385 await this.ctx.storage.setAlarm(Date.now() + ms);
386 }
387 
388 async checkAlarmAbortConfirmation() {
389 const abortConfirmation = await this.ctx.storage.get('aborted-confirmed');
390 if (!abortConfirmation) {
391 throw new Error(
392 `Abort confirmation did not get inserted: ${abortConfirmation}`
393 );
394 }
395 }
396 
397 async testWs() {
398 const { container } = this.ctx;
399 
400 if (!container.running) {
401 container.start({
402 env: { WS_ENABLED: 'true' },
403 enableInternet: true,
404 });
405 }
406 
407 await this.waitUntilContainerIsHealthy();
408 
409 const res = await container.getTcpPort(8080).fetch('http://foo/ws', {
410 headers: {
411 Upgrade: 'websocket',
412 Connection: 'Upgrade',
413 'Sec-WebSocket-Key': 'x3JJHMbDL1EzLkh9GBhXDw==',
414 'Sec-WebSocket-Version': '13',
415 },
416 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
417 });
418 
419 // Should get WebSocket upgrade response
420 assert.strictEqual(res.status, 101);
421 assert.strictEqual(res.headers.get('upgrade'), 'websocket');
422 assert.strictEqual(!!res.webSocket, true);
423 
424 // Test basic WebSocket communication
425 const ws = res.webSocket;
426 ws.accept();
427 
428 // Listen for response
429 const messagePromise = new Promise((resolve) => {
430 ws.addEventListener(
431 'message',
432 (event) => {
433 resolve(event.data);
434 },
435 { once: true }
436 );
437 });
438 
439 // Send a test message
440 ws.send('Hello WebSocket!');
441 
442 assert.strictEqual(await messagePromise, 'Echo: Hello WebSocket!');
443 
444 ws.close();
445 await container.destroy();
446 }
447 
448 getStatus() {
449 return this.ctx.container.running;
450 }
451 
452 async waitUntilContainerIsHealthy() {
453 const container = this.ctx.container;
454 {
455 let resp;
456 // The retry count here is arbitrary. Can increase it if necessary.
457 const maxRetries = 15;
458 for (let i = 1; i <= maxRetries; i++) {
459 try {
460 resp = await container.getTcpPort(8080).fetch('http://foo/bar/baz', {
461 method: 'POST',
462 body: 'hello',
463 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
464 });
465 break;
466 } catch (e) {
467 if (!e.message.includes('Container is not listening to port 8080')) {
468 console.error(
469 'Error querying getTcpPort().fetch() that is not related to the container not listening yet',
470 e.message
471 );
472 
473 throw e;
474 }
475 
476 if (i === maxRetries) {
477 console.error(
478 `Failed to connect to container ${container.id}. Retried ${i} times`
479 );
480 throw e;
481 }
482 
483 await scheduler.wait(500);
484 }
485 }
486 
487 assert.equal(resp.status, 200);
488 assert.equal(resp.statusText, 'OK');
489 assert.strictEqual(await resp.text(), 'Hello World!');
490 }
491 }
492 
493 async fetchIntercept(host) {
494 return await this.ctx.container
495 .getTcpPort(8080)
496 .fetch('http://foo/intercept', {
497 headers: { 'x-host': host },
498 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
499 });
500 }
501 
502 async expectIntercept(host, expectedStatus, expectedBody) {
503 const response = await this.fetchIntercept(host);
504 assert.equal(response.status, expectedStatus);
505 assert.equal(await response.text(), expectedBody);
506 }
507 
508 fetchHttpsIntercept(host) {
509 return this.ctx.container
510 .getTcpPort(8080)
511 .fetch('http://foo/intercept-https', {
512 headers: { 'x-host': host },
513 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
514 });
515 }
516 
517 async expectHttpsIntercept(host, expectedStatus, expectedBody) {
518 const response = await this.fetchHttpsIntercept(host);
519 assert.equal(response.status, expectedStatus);
520 assert.equal(await response.text(), expectedBody);
521 }
522 
523 async testPortNotListening() {
524 const container = this.ctx.container;
525 if (container.running) {
526 const monitor = container.monitor().catch((_err) => {});
527 await container.destroy();
528 await monitor;
529 }
530 
531 container.start();
532 const monitor = container.monitor().catch((_err) => {});
533 await this.waitUntilContainerIsHealthy();
534 
535 await assert.rejects(
536 container.getTcpPort(8081).fetch('http://foo/bar', {
537 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
538 }),
539 /Container is not listening to port 8081/
540 );
541 
542 await container.destroy();
543 await monitor;
544 }
545 
546 async testLabels() {
547 const container = this.ctx.container;
548 if (container.running) {
549 let monitor = container.monitor().catch((_err) => {});
550 await container.destroy();
551 await monitor;
552 }
553 
554 assert.strictEqual(container.running, false);
555 
556 const labels = {
557 team: 'workers',
558 environment: 'testing',
559 'my.label/key': 'value',
560 'workerd-foo': 'bar',
561 kv: 'a=b=c',
562 emoji: '🧪',
563 };
564 container.start({ enableInternet: true, labels });
565 
566 const monitor = container.monitor().catch((_err) => {});
567 await this.waitUntilContainerIsHealthy();
568 
569 assert.strictEqual(container.running, true);
570 
571 const info = await container.inspect();
572 assert.deepStrictEqual(info.labels, labels);
573 
574 await container.destroy();
575 await monitor;
576 assert.strictEqual(container.running, false);
577 }
578 
579 async testInspectBeforeStart() {
580 const container = this.ctx.container;
581 if (container.running) {
582 let monitor = container.monitor().catch((_err) => {});
583 await container.destroy();
584 await monitor;
585 }
586 
587 assert.strictEqual(container.running, false);
588 
589 const info = await container.inspect();
590 assert.strictEqual(info, null);
591 }
592 
593 async testInspectEmptyLabels() {
594 const container = this.ctx.container;
595 if (container.running) {
596 let monitor = container.monitor().catch((_err) => {});
597 await container.destroy();
598 await monitor;
599 }
600 
601 assert.strictEqual(container.running, false);
602 
603 container.start({ enableInternet: true });
604 const monitor = container.monitor().catch((_err) => {});
605 await this.waitUntilContainerIsHealthy();
606 
607 const info = await container.inspect();
608 assert.deepStrictEqual(info.labels, {});
609 
610 await container.destroy();
611 await monitor;
612 }
613 
614 async testInspectAfterDestroy() {
615 const container = this.ctx.container;
616 if (container.running) {
617 let monitor = container.monitor().catch((_err) => {});
618 await container.destroy();
619 await monitor;
620 }
621 
622 assert.strictEqual(container.running, false);
623 
624 container.start({
625 enableInternet: true,
626 labels: { foo: 'bar' },
627 });
628 const monitor = container.monitor().catch((_err) => {});
629 await this.waitUntilContainerIsHealthy();
630 await container.destroy();
631 await monitor;
632 
633 const info = await container.inspect();
634 assert.strictEqual(info, null);
635 }
636 
637 async testLabelValidation() {
638 const container = this.ctx.container;
639 if (container.running) {
640 let monitor = container.monitor().catch((_err) => {});
641 await container.destroy();
642 await monitor;
643 }
644 
645 assert.strictEqual(container.running, false);
646 
647 // Empty label name
648 assert.throws(() => container.start({ labels: { '': 'value' } }), {
649 message: /Label names cannot be empty/,
650 });
651 
652 // Label name with control character
653 assert.throws(
654 () => container.start({ labels: { 'bad\x01name': 'value' } }),
655 { message: /Label names cannot contain control characters \(index 0\)/ }
656 );
657 
658 // Label value with control character
659 assert.throws(() => container.start({ labels: { name: 'bad\x01value' } }), {
660 message: /Label values cannot contain control characters \(index 0\)/,
661 });
662 }
663 
664 async testPidNamespace() {
665 const container = this.ctx.container;
666 if (container.running) {
667 let monitor = container.monitor().catch((_err) => {});
668 await container.destroy();
669 await monitor;
670 }
671 
672 assert.strictEqual(container.running, false);
673 
674 container.start({
675 enableInternet: true,
676 });
677 
678 const monitor = container.monitor().catch((_err) => {});
679 await this.waitUntilContainerIsHealthy();
680 
681 const resp = await container
682 .getTcpPort(8080)
683 .fetch('http://foo/pid-namespace', {
684 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
685 });
686 
687 assert.equal(resp.status, 200);
688 const data = await resp.json();
689 
690 await container.destroy();
691 await monitor;
692 assert.strictEqual(container.running, false);
693 
694 return data;
695 }
696 
697 async testSetEgressHttpWithInternet() {
698 const container = this.ctx.container;
699 if (container.running) {
700 let monitor = container.monitor().catch((_err) => {});
701 await container.destroy();
702 await monitor;
703 }
704 
705 container.start({ enableInternet: true });
706 
707 await this.waitUntilContainerIsHealthy();
708 
709 await container.interceptOutboundHttp(
710 'googlefakedomain.com',
711 this.ctx.exports.TestService({ props: { id: 2 } })
712 );
713 
714 await this.expectIntercept(
715 'googlefakedomain.com',
716 200,
717 'hello binding: 2 http://googlefakedomain.com/'
718 );
719 
720 await this.expectIntercept(
721 'googlefakedomainother.com',
722 500,
723 'googlefakedomainother.com fetch failed'
724 );
725 
726 await container.interceptAllOutboundHttp(
727 this.ctx.exports.TestService({ props: { id: 5 } })
728 );
729 
730 await this.expectIntercept(
731 'google.com',
732 200,
733 'hello binding: 5 http://google.com/'
734 );
735 }
736 
737 async testSetEgressHttpNoInternet() {
738 const container = this.ctx.container;
739 
740 if (!container.running) container.start();
741 
742 // wait for container to be available
743 await this.waitUntilContainerIsHealthy();
744 
745 await container.interceptOutboundHttp(
746 'google.com',
747 this.ctx.exports.TestService({ props: { id: 2 } })
748 );
749 
750 await this.expectIntercept(
751 'google.com',
752 200,
753 'hello binding: 2 http://google.com/'
754 );
755 
756 // This should fail as there is no hostname that matches it.
757 await this.expectIntercept('google2.com', 500, 'google2.com fetch failed');
758 
759 await container.interceptOutboundHttp(
760 'google2.com',
761 this.ctx.exports.TestService({ props: { id: 4 } })
762 );
763 
764 await this.expectIntercept(
765 'google.com',
766 200,
767 'hello binding: 2 http://google.com/'
768 );
769 await this.expectIntercept(
770 'google2.com',
771 200,
772 'hello binding: 4 http://google2.com/'
773 );
774 
775 // From now on, all hostnames resolve to Workerd.
776 await container.interceptAllOutboundHttp(
777 this.ctx.exports.TestService({ props: { id: 6 } })
778 );
779 
780 await this.expectIntercept(
781 'google.com',
782 200,
783 'hello binding: 2 http://google.com/'
784 );
785 
786 await this.expectIntercept(
787 'google2.com',
788 200,
789 'hello binding: 4 http://google2.com/'
790 );
791 
792 await this.expectIntercept(
793 'google3.com',
794 200,
795 'hello binding: 6 http://google3.com/'
796 );
797 
798 await this.expectIntercept(
799 '1.1.1.1',
800 200,
801 'hello binding: 6 http://1.1.1.1/'
802 );
803 
804 await this.expectIntercept('1.1.1.1:90', 500, '1.1.1.1:90 fetch failed');
805 await this.expectIntercept(
806 'google.com:9000',
807 500,
808 'google.com:9000 fetch failed'
809 );
810 }
811 
812 async createSnapshotForTransfer() {
813 const container = this.ctx.container;
814 if (container.running) {
815 const monitor = container.monitor().catch((_err) => {});
816 await container.destroy();
817 await monitor;
818 }
819 
820 container.start({ enableInternet: true });
821 const monitor = container.monitor().catch((_err) => {});
822 await this.waitUntilContainerIsHealthy();
823 
824 const writeResp = await container
825 .getTcpPort(8080)
826 .fetch('http://foo/write-file?path=/app/data/cross-do.txt', {
827 method: 'POST',
828 body: 'cross-do-snapshot',
829 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
830 });
831 assert.equal(writeResp.status, 200);
832 
833 const snapshot = await container.snapshotDirectory({
834 dir: '/app/data',
835 name: 'cross-do-snapshot',
836 });
837 
838 await container.destroy();
839 await monitor;
840 
841 return snapshot;
842 }
843 
844 async restoreTransferredSnapshot(snapshot) {
845 assert.ok(snapshot.id, 'snapshot must have a non-empty id');
846 assert.ok(snapshot.size > 0, 'snapshot must have a positive size');
847 assert.strictEqual(snapshot.dir, '/app/data');
848 
849 const container = this.ctx.container;
850 if (container.running) {
851 const monitor = container.monitor().catch((_err) => {});
852 await container.destroy();
853 await monitor;
854 }
855 
856 container.start({
857 enableInternet: true,
858 directorySnapshots: [{ snapshot }],
859 });
860 const monitor = container.monitor().catch((_err) => {});
861 await this.waitUntilContainerIsHealthy();
862 
863 const readResp = await container
864 .getTcpPort(8080)
865 .fetch('http://foo/read-file?path=/app/data/cross-do.txt', {
866 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
867 });
868 assert.equal(readResp.status, 200);
869 assert.strictEqual(await readResp.text(), 'cross-do-snapshot');
870 
871 await container.destroy();
872 await monitor;
873 }
874 
875 async createContainerSnapshotForTransfer() {
876 const container = this.ctx.container;
877 if (container.running) {
878 const monitor = container.monitor().catch((_err) => {});
879 await container.destroy();
880 await monitor;
881 }
882 
883 container.start({ enableInternet: true });
884 const monitor = container.monitor().catch((_err) => {});
885 await this.waitUntilContainerIsHealthy();
886 
887 const writeResp = await container
888 .getTcpPort(8080)
889 .fetch('http://foo/write-file?path=/app/data/full-cross-do.txt', {
890 method: 'POST',
891 body: 'cross-do-container-snapshot',
892 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
893 });
894 assert.equal(writeResp.status, 200);
895 
896 const snapshot = await container.snapshotContainer({
897 name: 'cross-do-container-snapshot',
898 });
899 
900 await container.destroy();
901 await monitor;
902 
903 return snapshot;
904 }
905 
906 async restoreTransferredContainerSnapshot(snapshot) {
907 assert.ok(snapshot.id, 'snapshot must have a non-empty id');
908 assert.ok(snapshot.size > 0, 'snapshot must have a positive size');
909 
910 const container = this.ctx.container;
911 if (container.running) {
912 const monitor = container.monitor().catch((_err) => {});
913 await container.destroy();
914 await monitor;
915 }
916 
917 container.start({
918 enableInternet: true,
919 containerSnapshot: snapshot,
920 });
921 const monitor = container.monitor().catch((_err) => {});
922 await this.waitUntilContainerIsHealthy();
923 
924 const readResp = await container
925 .getTcpPort(8080)
926 .fetch('http://foo/read-file?path=/app/data/full-cross-do.txt', {
927 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
928 });
929 assert.equal(readResp.status, 200);
930 assert.strictEqual(await readResp.text(), 'cross-do-container-snapshot');
931 
932 await container.destroy();
933 await monitor;
934 }
935 
936 async testSetEgressHttp() {
937 const container = this.ctx.container;
938 
939 // Set up egress TCP mapping to route requests to the binding
940 // We can configure this even before the container starts.
941 await container.interceptOutboundHttp(
942 '1.2.3.4',
943 this.ctx.exports.TestService({ props: { id: 1234 } })
944 );
945 
946 if (!container.running) container.start();
947 
948 // Keep container alive after abort();
949 container.monitor().catch((err) => {
950 console.error('Container exited with an error:', err.message);
951 });
952 
953 // wait for container to be available
954 await this.waitUntilContainerIsHealthy();
955 
956 // Set up egress TCP mapping to route requests to the binding
957 // This registers the binding's channel token with the container runtime
958 await container.interceptOutboundHttp(
959 '11.0.0.1:9999',
960 this.ctx.exports.TestService({ props: { id: 1 } })
961 );
962 
963 await container.interceptOutboundHttp(
964 '11.0.0.2:9999',
965 this.ctx.exports.TestService({ props: { id: 2 } })
966 );
967 
968 // we catch all http requests to port 80
969 await container.interceptAllOutboundHttp(
970 this.ctx.exports.TestService({ props: { id: 3 } })
971 );
972 
973 {
974 const response = await container
975 .getTcpPort(8080)
976 .fetch('http://foo/intercept', {
977 headers: { 'x-host': '1.2.3.4:80' },
978 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
979 });
980 assert.equal(response.status, 200);
981 assert.equal(
982 await response.text(),
983 'hello binding: 1234 http://1.2.3.4/'
984 );
985 }
986 
987 {
988 const response = await container
989 .getTcpPort(8080)
990 .fetch('http://foo/intercept', {
991 headers: { 'x-host': '11.0.0.1:9999' },
992 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
993 });
994 assert.equal(response.status, 200);
995 assert.equal(
996 await response.text(),
997 'hello binding: 1 http://11.0.0.1:9999/'
998 );
999 }
1000 
1001 {
1002 const response = await container
1003 .getTcpPort(8080)
1004 .fetch('http://foo/intercept', {
1005 headers: { 'x-host': '11.0.0.2:9999' },
1006 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1007 });
1008 assert.equal(response.status, 200);
1009 assert.equal(
1010 await response.text(),
1011 'hello binding: 2 http://11.0.0.2:9999/'
1012 );
1013 }
1014 
1015 {
1016 const response = await container
1017 .getTcpPort(8080)
1018 .fetch('http://foo/intercept', {
1019 headers: { 'x-host': '15.0.0.2:80' },
1020 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1021 });
1022 assert.equal(response.status, 200);
1023 assert.equal(await response.text(), 'hello binding: 3 http://15.0.0.2/');
1024 }
1025 
1026 {
1027 const response = await container
1028 .getTcpPort(8080)
1029 .fetch('http://foo/intercept', {
1030 headers: { 'x-host': '[111::]:80' },
1031 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1032 });
1033 assert.equal(response.status, 200);
1034 assert.equal(await response.text(), 'hello binding: 3 http://[111::]/');
1035 }
1036 
1037 {
1038 const response = await container
1039 .getTcpPort(8080)
1040 .fetch('http://foo/intercept', {
1041 headers: { 'x-host': 'google.com/hello/world' },
1042 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1043 });
1044 assert.equal(response.status, 200);
1045 assert.equal(
1046 await response.text(),
1047 'hello binding: 3 http://google.com/hello/world'
1048 );
1049 }
1050 
1051 // test we can set another TestService
1052 await container.interceptAllOutboundHttp(
1053 this.ctx.exports.TestService({ props: { id: 1212 } })
1054 );
1055 
1056 {
1057 // We preserved the order...
1058 const response = await container
1059 .getTcpPort(8080)
1060 .fetch('http://foo/intercept', {
1061 headers: { 'x-host': '11.0.0.2:9999' },
1062 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1063 });
1064 assert.equal(response.status, 200);
1065 assert.equal(
1066 await response.text(),
1067 'hello binding: 2 http://11.0.0.2:9999/'
1068 );
1069 }
1070 
1071 {
1072 // and we updated the id, even for existing connections
1073 const response = await container
1074 .getTcpPort(8080)
1075 .fetch('http://foo/intercept', {
1076 headers: { 'x-host': '15.0.0.2:80' },
1077 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1078 });
1079 assert.equal(response.status, 200);
1080 assert.equal(
1081 await response.text(),
1082 'hello binding: 1212 http://15.0.0.2/'
1083 );
1084 }
1085 
1086 {
1087 // and we updated the id for new connections
1088 const response = await container
1089 .getTcpPort(8080)
1090 .fetch('http://foo/intercept', {
1091 headers: { 'x-host': '15.0.0.55:80' },
1092 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1093 });
1094 assert.equal(response.status, 200);
1095 assert.equal(
1096 await response.text(),
1097 'hello binding: 1212 http://15.0.0.55/'
1098 );
1099 }
1100 }
1101 
1102 async testSetEgressHttps() {
1103 const container = this.ctx.container;
1104 if (container.running) {
1105 const monitor = container.monitor().catch((_err) => {});
1106 await container.destroy();
1107 await monitor;
1108 }
1109 
1110 assert.strictEqual(container.running, false);
1111 
1112 await container.interceptOutboundHttps(
1113 'example.com:443',
1114 this.ctx.exports.TestService({ props: { id: 1000 } })
1115 );
1116 
1117 container.start({
1118 env: {
1119 NODE_EXTRA_CA_CERTS:
1120 '/etc/cloudflare/certs/cloudflare-containers-ca.crt',
1121 },
1122 });
1123 
1124 container.monitor().catch((err) => {
1125 console.error('Container exited with an error:', err.message);
1126 });
1127 
1128 await this.waitUntilContainerIsHealthy();
1129 
1130 await container.interceptOutboundHttps(
1131 '*.cloudflare.com:443',
1132 this.ctx.exports.TestService({ props: { id: 2000 } })
1133 );
1134 
1135 await container.interceptOutboundHttps(
1136 '*',
1137 this.ctx.exports.TestService({ props: { id: 3000 } })
1138 );
1139 
1140 await this.expectHttpsIntercept(
1141 'example.com',
1142 200,
1143 'hello binding: 1000 https://example.com/'
1144 );
1145 
1146 await this.expectHttpsIntercept(
1147 'www.cloudflare.com',
1148 200,
1149 'hello binding: 2000 https://www.cloudflare.com/'
1150 );
1151 
1152 await this.expectHttpsIntercept(
1153 'google.com',
1154 200,
1155 'hello binding: 3000 https://google.com/'
1156 );
1157 
1158 await container.interceptOutboundHttps(
1159 '*',
1160 this.ctx.exports.TestService({ props: { id: 4000 } })
1161 );
1162 
1163 await this.expectHttpsIntercept(
1164 'example.com',
1165 200,
1166 'hello binding: 1000 https://example.com/'
1167 );
1168 
1169 await this.expectHttpsIntercept(
1170 'github.com',
1171 200,
1172 'hello binding: 4000 https://github.com/'
1173 );
1174 }
1175 
1176 async testSetEgressTcp() {
1177 const container = this.ctx.container;
1178 if (container.running) {
1179 const monitor = container.monitor().catch((_err) => {});
1180 await container.destroy();
1181 await monitor;
1182 }
1183 
1184 assert.strictEqual(container.running, false);
1185 
1186 // Register a TCP egress mapping before the container starts.
1187 // When the container connects to 11.0.0.1:7777 via raw TCP, the
1188 // sidecar forwards the stream to our TestService.connect() handler.
1189 await container.interceptOutboundTcp(
1190 '11.0.0.1:7777',
1191 this.ctx.exports.TestService({ props: { id: 500 } })
1192 );
1193 
1194 container.start();
1195 
1196 container.monitor().catch((err) => {
1197 console.error('Container exited with an error:', err.message);
1198 });
1199 
1200 await this.waitUntilContainerIsHealthy();
1201 
1202 // Also register another mapping after the container is running.
1203 await container.interceptOutboundTcp(
1204 '11.0.0.2:7777',
1205 this.ctx.exports.TestService({ props: { id: 600 } })
1206 );
1207 
1208 // Ask the container to open a raw TCP connection to 11.0.0.1:7777.
1209 // The container's /intercept-tcp endpoint sends "ping\n" over the
1210 // socket. The sidecar intercepts it and routes it to
1211 // TestService.connect(), which reads the data and echoes it back
1212 // prefixed with the binding id.
1213 {
1214 const response = await container
1215 .getTcpPort(8080)
1216 .fetch('http://foo/intercept-tcp', {
1217 headers: { 'x-tcp-target': '11.0.0.1:7777' },
1218 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1219 });
1220 assert.equal(response.status, 200);
1221 const body = await response.text();
1222 assert.equal(body, 'tcp binding: 500 got: ping');
1223 }
1224 
1225 // Verify the second mapping works too.
1226 {
1227 const response = await container
1228 .getTcpPort(8080)
1229 .fetch('http://foo/intercept-tcp', {
1230 headers: { 'x-tcp-target': '11.0.0.2:7777' },
1231 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1232 });
1233 assert.equal(response.status, 200);
1234 const body = await response.text();
1235 assert.equal(body, 'tcp binding: 600 got: ping');
1236 }
1237 }
1238 
1239 async testInterceptWebSocket() {
1240 const container = this.ctx.container;
1241 if (container.running) {
1242 const monitor = container.monitor().catch((_err) => {});
1243 await container.destroy();
1244 await monitor;
1245 }
1246 
1247 assert.strictEqual(container.running, false);
1248 // Set up egress mapping to route WebSocket requests to the binding
1249 await container.interceptOutboundHttp(
1250 '11.0.0.1:9999',
1251 this.ctx.exports.TestService({ props: { id: 42 } })
1252 );
1253 
1254 // Start container with WebSocket proxy mode enabled
1255 container.start({
1256 env: { WS_ENABLED: 'true', WS_PROXY_TARGET: '11.0.0.1:9999' },
1257 });
1258 
1259 container.monitor().catch((_err) => {});
1260 
1261 // Wait for container to be available
1262 await this.waitUntilContainerIsHealthy();
1263 
1264 assert.strictEqual(container.running, true);
1265 
1266 // Connect to container's /ws endpoint which proxies to the intercepted address
1267 // Flow: DO -> container:8080/ws -> container connects to 11.0.0.1:9999/ws
1268 // -> sidecar intercepts -> workerd -> TestService worker binding
1269 const res = await container.getTcpPort(8080).fetch('http://foo/ws', {
1270 headers: {
1271 Upgrade: 'websocket',
1272 Connection: 'Upgrade',
1273 'Sec-WebSocket-Key': 'x3JJHMbDL1EzLkh9GBhXDw==',
1274 'Sec-WebSocket-Version': '13',
1275 },
1276 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1277 });
1278 
1279 // Should get WebSocket upgrade response
1280 assert.strictEqual(res.status, 101);
1281 assert.strictEqual(res.headers.get('upgrade'), 'websocket');
1282 assert.strictEqual(!!res.webSocket, true);
1283 
1284 const ws = res.webSocket;
1285 ws.binaryType = 'arraybuffer';
1286 ws.accept();
1287 
1288 // Listen for response
1289 const { promise, resolve, reject } = Promise.withResolvers();
1290 
1291 ws.addEventListener(
1292 'message',
1293 (event) => {
1294 resolve(event.data);
1295 },
1296 { once: true }
1297 );
1298 
1299 const timeout = setTimeout(() => {
1300 reject(new Error('Websocket message not received within 5 seconds'));
1301 }, 5_000);
1302 
1303 // Send a test message - should go through the whole chain and come back
1304 ws.send('Hello through intercept!');
1305 
1306 // Should receive response from TestService binding with id 42
1307 const response = new TextDecoder().decode(await promise);
1308 clearTimeout(timeout);
1309 assert.strictEqual(response, 'Binding 42: Hello through intercept!');
1310 
1311 ws.close();
1312 await container.destroy();
1313 }
1314 
1315 async testInterceptWebSocketHttps() {
1316 const container = this.ctx.container;
1317 if (container.running) {
1318 const monitor = container.monitor().catch((_err) => {});
1319 await container.destroy();
1320 await monitor;
1321 }
1322 
1323 assert.strictEqual(container.running, false);
1324 
1325 await container.interceptOutboundHttps(
1326 'example.com:443',
1327 this.ctx.exports.TestService({ props: { id: 99 } })
1328 );
1329 
1330 container.start({
1331 env: {
1332 WS_ENABLED: 'true',
1333 WS_PROXY_TARGET: 'example.com',
1334 WS_PROXY_SECURE: 'true',
1335 NODE_EXTRA_CA_CERTS:
1336 '/etc/cloudflare/certs/cloudflare-containers-ca.crt',
1337 },
1338 });
1339 
1340 container.monitor().finally(() => {
1341 console.log('Container exited');
1342 });
1343 
1344 await this.waitUntilContainerIsHealthy();
1345 
1346 assert.strictEqual(container.running, true);
1347 
1348 const res = await container.getTcpPort(8080).fetch('http://foo/ws', {
1349 headers: {
1350 Upgrade: 'websocket',
1351 Connection: 'Upgrade',
1352 'Sec-WebSocket-Key': 'x3JJHMbDL1EzLkh9GBhXDw==',
1353 'Sec-WebSocket-Version': '13',
1354 },
1355 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1356 });
1357 
1358 assert.strictEqual(res.status, 101);
1359 assert.strictEqual(res.headers.get('upgrade'), 'websocket');
1360 assert.strictEqual(!!res.webSocket, true);
1361 
1362 const ws = res.webSocket;
1363 ws.binaryType = 'arraybuffer';
1364 ws.accept();
1365 
1366 const { promise, resolve, reject } = Promise.withResolvers();
1367 
1368 ws.addEventListener(
1369 'message',
1370 (event) => {
1371 resolve(event.data);
1372 },
1373 { once: true }
1374 );
1375 
1376 const timeout = setTimeout(() => {
1377 reject(new Error('Websocket message not received within 5 seconds'));
1378 }, 5_000);
1379 
1380 ws.send('Hello through WSS intercept!');
1381 
1382 const response = new TextDecoder().decode(await promise);
1383 clearTimeout(timeout);
1384 assert.strictEqual(response, 'Binding 99: Hello through WSS intercept!');
1385 
1386 ws.close();
1387 await container.destroy();
1388 }
1389 
1390 async testSnapshotRoundTrip() {
1391 const container = this.ctx.container;
1392 if (container.running) {
1393 const monitor = container.monitor().catch((_err) => {});
1394 await container.destroy();
1395 await monitor;
1396 }
1397 
1398 assert.strictEqual(container.running, false);
1399 
1400 container.start({ enableInternet: true });
1401 const monitor = container.monitor().catch((_err) => {});
1402 await this.waitUntilContainerIsHealthy();
1403 
1404 const writeResp = await container
1405 .getTcpPort(8080)
1406 .fetch('http://foo/write-file?path=/app/data/test.txt', {
1407 method: 'POST',
1408 body: 'snapshot-content-123',
1409 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1410 });
1411 assert.equal(writeResp.status, 200);
1412 
1413 const snapshot = await container.snapshotDirectory({ dir: '/app/data' });
1414 assert.strictEqual(typeof snapshot.id, 'string');
1415 assert.ok(snapshot.id.length > 0, 'snapshot id should be non-empty');
1416 assert.ok(snapshot.size > 0, 'snapshot size should be > 0');
1417 assert.strictEqual(snapshot.dir, '/app/data');
1418 assert.strictEqual(snapshot.name, undefined);
1419 
1420 await container.destroy();
1421 await monitor;
1422 assert.strictEqual(container.running, false);
1423 
1424 container.start({
1425 enableInternet: true,
1426 directorySnapshots: [{ snapshot }],
1427 });
1428 const monitor2 = container.monitor().catch((_err) => {});
1429 await this.waitUntilContainerIsHealthy();
1430 
1431 const readResp = await container
1432 .getTcpPort(8080)
1433 .fetch('http://foo/read-file?path=/app/data/test.txt', {
1434 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1435 });
1436 assert.equal(readResp.status, 200);
1437 assert.strictEqual(await readResp.text(), 'snapshot-content-123');
1438 
1439 await container.destroy();
1440 await monitor2;
1441 }
1442 
1443 async testSnapshotNamedRoundTrip() {
1444 const container = this.ctx.container;
1445 if (container.running) {
1446 const monitor = container.monitor().catch((_err) => {});
1447 await container.destroy();
1448 await monitor;
1449 }
1450 
1451 container.start({ enableInternet: true });
1452 const monitor = container.monitor().catch((_err) => {});
1453 await this.waitUntilContainerIsHealthy();
1454 
1455 await container
1456 .getTcpPort(8080)
1457 .fetch('http://foo/write-file?path=/app/data/named.txt', {
1458 method: 'POST',
1459 body: 'named-snapshot',
1460 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1461 });
1462 
1463 const snapshot = await container.snapshotDirectory({
1464 dir: '/app/data',
1465 name: 'my-snapshot',
1466 });
1467 assert.strictEqual(snapshot.name, 'my-snapshot');
1468 assert.strictEqual(snapshot.dir, '/app/data');
1469 
1470 await container.destroy();
1471 await monitor;
1472 
1473 container.start({
1474 enableInternet: true,
1475 directorySnapshots: [{ snapshot }],
1476 });
1477 const monitor2 = container.monitor().catch((_err) => {});
1478 await this.waitUntilContainerIsHealthy();
1479 
1480 const readResp = await container
1481 .getTcpPort(8080)
1482 .fetch('http://foo/read-file?path=/app/data/named.txt', {
1483 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1484 });
1485 assert.equal(readResp.status, 200);
1486 assert.strictEqual(await readResp.text(), 'named-snapshot');
1487 
1488 await container.destroy();
1489 await monitor2;
1490 }
1491 
1492 async testSnapshotMultipleDirectories() {
1493 const container = this.ctx.container;
1494 if (container.running) {
1495 const monitor = container.monitor().catch((_err) => {});
1496 await container.destroy();
1497 await monitor;
1498 }
1499 
1500 container.start({ enableInternet: true });
1501 const monitor = container.monitor().catch((_err) => {});
1502 await this.waitUntilContainerIsHealthy();
1503 
1504 await container
1505 .getTcpPort(8080)
1506 .fetch('http://foo/write-file?path=/app/dir1/file1.txt', {
1507 method: 'POST',
1508 body: 'content-dir1',
1509 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1510 });
1511 await container
1512 .getTcpPort(8080)
1513 .fetch('http://foo/write-file?path=/app/dir2/file2.txt', {
1514 method: 'POST',
1515 body: 'content-dir2',
1516 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1517 });
1518 
1519 const snap1 = await container.snapshotDirectory({ dir: '/app/dir1' });
1520 const snap2 = await container.snapshotDirectory({ dir: '/app/dir2' });
1521 
1522 await container.destroy();
1523 await monitor;
1524 
1525 container.start({
1526 enableInternet: true,
1527 directorySnapshots: [{ snapshot: snap1 }, { snapshot: snap2 }],
1528 });
1529 const monitor2 = container.monitor().catch((_err) => {});
1530 await this.waitUntilContainerIsHealthy();
1531 
1532 const r1 = await container
1533 .getTcpPort(8080)
1534 .fetch('http://foo/read-file?path=/app/dir1/file1.txt', {
1535 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1536 });
1537 assert.equal(r1.status, 200);
1538 assert.strictEqual(await r1.text(), 'content-dir1');
1539 
1540 const r2 = await container
1541 .getTcpPort(8080)
1542 .fetch('http://foo/read-file?path=/app/dir2/file2.txt', {
1543 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1544 });
1545 assert.equal(r2.status, 200);
1546 assert.strictEqual(await r2.text(), 'content-dir2');
1547 
1548 await container.destroy();
1549 await monitor2;
1550 }
1551 
1552 async testSnapshotCustomMountPoint() {
1553 const container = this.ctx.container;
1554 if (container.running) {
1555 const monitor = container.monitor().catch((_err) => {});
1556 await container.destroy();
1557 await monitor;
1558 }
1559 
1560 assert.strictEqual(container.running, false);
1561 
1562 container.start({ enableInternet: true });
1563 const monitor = container.monitor().catch((_err) => {});
1564 await this.waitUntilContainerIsHealthy();
1565 
1566 const writeResp = await container
1567 .getTcpPort(8080)
1568 .fetch('http://foo/write-file?path=/app/data/test.txt', {
1569 method: 'POST',
1570 body: 'remapped-content',
1571 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1572 });
1573 assert.equal(writeResp.status, 200);
1574 
1575 const snapshot = await container.snapshotDirectory({ dir: '/app/data' });
1576 assert.strictEqual(snapshot.dir, '/app/data');
1577 
1578 await container.destroy();
1579 await monitor;
1580 
1581 container.start({
1582 enableInternet: true,
1583 directorySnapshots: [{ snapshot, mountPoint: '/app/restored' }],
1584 });
1585 const monitor2 = container.monitor().catch((_err) => {});
1586 await this.waitUntilContainerIsHealthy();
1587 
1588 const readResp = await container
1589 .getTcpPort(8080)
1590 .fetch('http://foo/read-file?path=/app/restored/test.txt', {
1591 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1592 });
1593 assert.equal(readResp.status, 200);
1594 assert.strictEqual(await readResp.text(), 'remapped-content');
1595 
1596 // Must not exist at original path since we restored to a different mount point
1597 const origResp = await container
1598 .getTcpPort(8080)
1599 .fetch('http://foo/read-file?path=/app/data/test.txt', {
1600 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1601 });
1602 assert.equal(origResp.status, 404);
1603 
1604 await container.destroy();
1605 await monitor2;
1606 }
1607 
1608 async testSnapshotOverlappingMounts() {
1609 const container = this.ctx.container;
1610 if (container.running) {
1611 const monitor = container.monitor().catch((_err) => {});
1612 await container.destroy();
1613 await monitor;
1614 }
1615 
1616 container.start({ enableInternet: true });
1617 const monitor = container.monitor().catch((_err) => {});
1618 await this.waitUntilContainerIsHealthy();
1619 
1620 await container
1621 .getTcpPort(8080)
1622 .fetch('http://foo/write-file?path=/tmp/parent-src/root.txt', {
1623 method: 'POST',
1624 body: 'parent-root',
1625 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1626 });
1627 await container
1628 .getTcpPort(8080)
1629 .fetch(
1630 'http://foo/write-file?path=/tmp/parent-src/child/from-parent.txt',
1631 {
1632 method: 'POST',
1633 body: 'masked-by-child-mount',
1634 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1635 }
1636 );
1637 await container
1638 .getTcpPort(8080)
1639 .fetch('http://foo/write-file?path=/tmp/child-src/from-child.txt', {
1640 method: 'POST',
1641 body: 'child-wins',
1642 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1643 });
1644 
1645 const parentSnapshot = await container.snapshotDirectory({
1646 dir: '/tmp/parent-src',
1647 });
1648 const childSnapshot = await container.snapshotDirectory({
1649 dir: '/tmp/child-src',
1650 });
1651 
1652 await container.destroy();
1653 await monitor;
1654 
1655 const assertRestoredTree = async () => {
1656 const rootResp = await container
1657 .getTcpPort(8080)
1658 .fetch('http://foo/read-file?path=/tmp/restored/root.txt', {
1659 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1660 });
1661 assert.equal(rootResp.status, 200);
1662 assert.strictEqual(await rootResp.text(), 'parent-root');
1663 
1664 const childResp = await container
1665 .getTcpPort(8080)
1666 .fetch('http://foo/read-file?path=/tmp/restored/child/from-child.txt', {
1667 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1668 });
1669 assert.equal(childResp.status, 200);
1670 assert.strictEqual(await childResp.text(), 'child-wins');
1671 
1672 const maskedResp = await container
1673 .getTcpPort(8080)
1674 .fetch(
1675 'http://foo/read-file?path=/tmp/restored/child/from-parent.txt',
1676 {
1677 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1678 }
1679 );
1680 assert.equal(maskedResp.status, 404);
1681 };
1682 
1683 const startAndAssert = async (directorySnapshots) => {
1684 container.start({ enableInternet: true, directorySnapshots });
1685 const restoreMonitor = container.monitor().catch((_err) => {});
1686 await this.waitUntilContainerIsHealthy();
1687 await assertRestoredTree();
1688 await container.destroy();
1689 await restoreMonitor;
1690 };
1691 
1692 await startAndAssert([
1693 { snapshot: childSnapshot, mountPoint: '/tmp/restored/child' },
1694 { snapshot: parentSnapshot, mountPoint: '/tmp/restored' },
1695 ]);
1696 
1697 await startAndAssert([
1698 { snapshot: parentSnapshot, mountPoint: '/tmp/restored' },
1699 { snapshot: childSnapshot, mountPoint: '/tmp/restored/child' },
1700 ]);
1701 }
1702 
1703 async testSnapshotDuplicateRestoreDirsRejected() {
1704 const container = this.ctx.container;
1705 if (container.running) {
1706 const monitor = container.monitor().catch((_err) => {});
1707 await container.destroy();
1708 await monitor;
1709 }
1710 
1711 container.start({ enableInternet: true });
1712 const monitor = container.monitor().catch((_err) => {});
1713 await this.waitUntilContainerIsHealthy();
1714 
1715 await container
1716 .getTcpPort(8080)
1717 .fetch('http://foo/write-file?path=/tmp/dup-a/file-a.txt', {
1718 method: 'POST',
1719 body: 'dup-a',
1720 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1721 });
1722 await container
1723 .getTcpPort(8080)
1724 .fetch('http://foo/write-file?path=/tmp/dup-b/file-b.txt', {
1725 method: 'POST',
1726 body: 'dup-b',
1727 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1728 });
1729 
1730 const firstSnapshot = await container.snapshotDirectory({
1731 dir: '/tmp/dup-a',
1732 });
1733 const secondSnapshot = await container.snapshotDirectory({
1734 dir: '/tmp/dup-b',
1735 });
1736 
1737 await container.destroy();
1738 await monitor;
1739 
1740 await assert.rejects(
1741 () =>
1742 new Promise((resolve, reject) => {
1743 try {
1744 container.start({
1745 enableInternet: true,
1746 directorySnapshots: [
1747 { snapshot: firstSnapshot, mountPoint: '/tmp/duplicate' },
1748 { snapshot: secondSnapshot, mountPoint: '/tmp/duplicate/' },
1749 ],
1750 });
1751 } catch (err) {
1752 return reject(err);
1753 }
1754 container.monitor().then(resolve).catch(reject);
1755 }),
1756 (err) => {
1757 assert.strictEqual(err.message, 'Container failed to start');
1758 return true;
1759 }
1760 );
1761 
1762 assert.strictEqual(container.running, false);
1763 }
1764 
1765 async testSnapshotRestoreToRoot() {
1766 const container = this.ctx.container;
1767 if (container.running) {
1768 const monitor = container.monitor().catch((_err) => {});
1769 await container.destroy();
1770 await monitor;
1771 }
1772 
1773 assert.strictEqual(container.running, false);
1774 
1775 const fakeSnapshot = {
1776 id: '01234567-89ab-cdef-0123-456789abcdef',
1777 size: 1024,
1778 dir: '/app/data',
1779 };
1780 
1781 assert.throws(
1782 () =>
1783 container.start({
1784 enableInternet: true,
1785 directorySnapshots: [{ snapshot: fakeSnapshot, mountPoint: '/' }],
1786 }),
1787 { message: /Directory snapshot cannot be restored to root directory\./ }
1788 );
1789 
1790 assert.strictEqual(container.running, false);
1791 }
1792 
1793 async testSnapshotRestoreImplicitRootRejected() {
1794 const container = this.ctx.container;
1795 if (container.running) {
1796 const monitor = container.monitor().catch((_err) => {});
1797 await container.destroy();
1798 await monitor;
1799 }
1800 
1801 assert.strictEqual(container.running, false);
1802 
1803 const fakeSnapshot = {
1804 id: '11111111-2222-3333-4444-555555555555',
1805 size: 1024,
1806 dir: '/',
1807 };
1808 
1809 assert.throws(
1810 () =>
1811 container.start({
1812 enableInternet: true,
1813 directorySnapshots: [{ snapshot: fakeSnapshot }],
1814 }),
1815 { message: /Directory snapshot cannot be restored to root directory\./ }
1816 );
1817 
1818 assert.strictEqual(container.running, false);
1819 }
1820 
1821 async testSnapshotRestoreRelativeMountPointRejected() {
1822 const container = this.ctx.container;
1823 if (container.running) {
1824 const monitor = container.monitor().catch((_err) => {});
1825 await container.destroy();
1826 await monitor;
1827 }
1828 
1829 assert.strictEqual(container.running, false);
1830 
1831 const fakeSnapshot = {
1832 id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
1833 size: 1024,
1834 dir: '/app/data',
1835 };
1836 
1837 assert.throws(
1838 () =>
1839 container.start({
1840 enableInternet: true,
1841 directorySnapshots: [
1842 { snapshot: fakeSnapshot, mountPoint: 'tmp/restored' },
1843 ],
1844 }),
1845 {
1846 message:
1847 /Directory snapshot restore path must be absolute\. Got: tmp\/restored/,
1848 }
1849 );
1850 
1851 assert.strictEqual(container.running, false);
1852 }
1853 
1854 async testSnapshotStoppedContainer() {
1855 const container = this.ctx.container;
1856 if (container.running) {
1857 const monitor = container.monitor().catch((_err) => {});
1858 await container.destroy();
1859 await monitor;
1860 }
1861 
1862 assert.strictEqual(container.running, false);
1863 
1864 await assert.rejects(
1865 async () => container.snapshotDirectory({ dir: '/app/data' }),
1866 (err) => {
1867 assert.match(err.message, /not running/);
1868 return true;
1869 }
1870 );
1871 }
1872 
1873 async testSnapshotRestoreNonExistentId() {
1874 const container = this.ctx.container;
1875 if (container.running) {
1876 const monitor = container.monitor().catch((_err) => {});
1877 await container.destroy();
1878 await monitor;
1879 }
1880 
1881 const fakeSnapshot = {
1882 id: 'deadbeef-0000-0000-0000-000000000000',
1883 size: 1024,
1884 dir: '/app/data',
1885 };
1886 
1887 await assert.rejects(
1888 () =>
1889 new Promise((resolve, reject) => {
1890 try {
1891 container.start({
1892 enableInternet: true,
1893 directorySnapshots: [{ snapshot: fakeSnapshot }],
1894 });
1895 } catch (err) {
1896 return reject(err);
1897 }
1898 container.monitor().then(resolve).catch(reject);
1899 }),
1900 (err) => {
1901 assert.ok(err.message.length > 0, 'error should have a message');
1902 return true;
1903 }
1904 );
1905 }
1906 
1907 async testSnapshotNonExistentDirectory() {
1908 const container = this.ctx.container;
1909 if (container.running) {
1910 const monitor = container.monitor().catch((_err) => {});
1911 await container.destroy();
1912 await monitor;
1913 }
1914 
1915 container.start({ enableInternet: true });
1916 const monitor = container.monitor().catch((_err) => {});
1917 await this.waitUntilContainerIsHealthy();
1918 
1919 await assert.rejects(
1920 () => container.snapshotDirectory({ dir: '/does/not/exist' }),
1921 (err) => {
1922 assert.match(err.message, /directory not found/);
1923 return true;
1924 }
1925 );
1926 
1927 await container.destroy();
1928 await monitor;
1929 }
1930 
1931 async testContainerSnapshotRoundTrip() {
1932 const container = this.ctx.container;
1933 if (container.running) {
1934 const monitor = container.monitor().catch((_err) => {});
1935 await container.destroy();
1936 await monitor;
1937 }
1938 
1939 assert.strictEqual(container.running, false);
1940 
1941 container.start({ enableInternet: true });
1942 const monitor = container.monitor().catch((_err) => {});
1943 await this.waitUntilContainerIsHealthy();
1944 
1945 const writeResp = await container
1946 .getTcpPort(8080)
1947 .fetch('http://foo/write-file?path=/app/data/full-snapshot.txt', {
1948 method: 'POST',
1949 body: 'full-snapshot-content',
1950 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1951 });
1952 assert.equal(writeResp.status, 200);
1953 
1954 const tmpWriteResp = await container
1955 .getTcpPort(8080)
1956 .fetch('http://foo/write-file?path=/tmp/full-snapshot-tmp.txt', {
1957 method: 'POST',
1958 body: 'tmp-full-snapshot-content',
1959 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1960 });
1961 assert.equal(tmpWriteResp.status, 200);
1962 
1963 const snapshot = await container.snapshotContainer({});
1964 assert.strictEqual(typeof snapshot.id, 'string');
1965 assert.ok(snapshot.id.length > 0, 'snapshot id should be non-empty');
1966 assert.ok(snapshot.size > 0, 'snapshot size should be > 0');
1967 assert.strictEqual(snapshot.name, undefined);
1968 
1969 await container.destroy();
1970 await monitor;
1971 assert.strictEqual(container.running, false);
1972 
1973 container.start({
1974 enableInternet: true,
1975 containerSnapshot: snapshot,
1976 });
1977 const monitor2 = container.monitor().catch((_err) => {});
1978 await this.waitUntilContainerIsHealthy();
1979 
1980 const readResp = await container
1981 .getTcpPort(8080)
1982 .fetch('http://foo/read-file?path=/app/data/full-snapshot.txt', {
1983 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1984 });
1985 assert.equal(readResp.status, 200);
1986 assert.strictEqual(await readResp.text(), 'full-snapshot-content');
1987 
1988 const tmpReadResp = await container
1989 .getTcpPort(8080)
1990 .fetch('http://foo/read-file?path=/tmp/full-snapshot-tmp.txt', {
1991 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
1992 });
1993 assert.equal(tmpReadResp.status, 200);
1994 assert.strictEqual(await tmpReadResp.text(), 'tmp-full-snapshot-content');
1995 
1996 await container.destroy();
1997 await monitor2;
1998 }
1999 
2000 async testContainerSnapshotNamedRoundTrip() {
2001 const container = this.ctx.container;
2002 if (container.running) {
2003 const monitor = container.monitor().catch((_err) => {});
2004 await container.destroy();
2005 await monitor;
2006 }
2007 
2008 container.start({ enableInternet: true });
2009 const monitor = container.monitor().catch((_err) => {});
2010 await this.waitUntilContainerIsHealthy();
2011 
2012 await container
2013 .getTcpPort(8080)
2014 .fetch('http://foo/write-file?path=/app/data/full-named.txt', {
2015 method: 'POST',
2016 body: 'named-container-snapshot',
2017 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2018 });
2019 
2020 const snapshot = await container.snapshotContainer({
2021 name: 'named-container-snapshot',
2022 });
2023 assert.strictEqual(snapshot.name, 'named-container-snapshot');
2024 
2025 await container.destroy();
2026 await monitor;
2027 
2028 container.start({
2029 enableInternet: true,
2030 containerSnapshot: snapshot,
2031 });
2032 const monitor2 = container.monitor().catch((_err) => {});
2033 await this.waitUntilContainerIsHealthy();
2034 
2035 const readResp = await container
2036 .getTcpPort(8080)
2037 .fetch('http://foo/read-file?path=/app/data/full-named.txt', {
2038 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2039 });
2040 assert.equal(readResp.status, 200);
2041 assert.strictEqual(await readResp.text(), 'named-container-snapshot');
2042 
2043 await container.destroy();
2044 await monitor2;
2045 }
2046 
2047 async testContainerSnapshotRestoreNonExistentId() {
2048 const container = this.ctx.container;
2049 if (container.running) {
2050 const monitor = container.monitor().catch((_err) => {});
2051 await container.destroy();
2052 await monitor;
2053 }
2054 
2055 const fakeSnapshot = {
2056 id: 'feedface-0000-0000-0000-000000000000',
2057 size: 1024,
2058 };
2059 
2060 await assert.rejects(
2061 () =>
2062 new Promise((resolve, reject) => {
2063 try {
2064 container.start({
2065 enableInternet: true,
2066 containerSnapshot: fakeSnapshot,
2067 });
2068 } catch (err) {
2069 return reject(err);
2070 }
2071 container.monitor().then(resolve).catch(reject);
2072 }),
2073 (err) => {
2074 assert.strictEqual(err.message, 'Container failed to start');
2075 return true;
2076 }
2077 );
2078 
2079 assert.strictEqual(container.running, false);
2080 }
2081 
2082 async testContainerSnapshotWithDirectoryOverlay() {
2083 const container = this.ctx.container;
2084 if (container.running) {
2085 const monitor = container.monitor().catch((_err) => {});
2086 await container.destroy();
2087 await monitor;
2088 }
2089 
2090 container.start({ enableInternet: true });
2091 const monitor = container.monitor().catch((_err) => {});
2092 await this.waitUntilContainerIsHealthy();
2093 
2094 await container
2095 .getTcpPort(8080)
2096 .fetch('http://foo/write-file?path=/app/data/from-full.txt', {
2097 method: 'POST',
2098 body: 'from-full-snapshot',
2099 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2100 });
2101 await container
2102 .getTcpPort(8080)
2103 .fetch('http://foo/write-file?path=/app/overlay-target/shared.txt', {
2104 method: 'POST',
2105 body: 'from-full-layer',
2106 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2107 });
2108 await container
2109 .getTcpPort(8080)
2110 .fetch(
2111 'http://foo/write-file?path=/app/overlay-target/full-only-hidden.txt',
2112 {
2113 method: 'POST',
2114 body: 'hidden-by-overlay',
2115 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2116 }
2117 );
2118 await container
2119 .getTcpPort(8080)
2120 .fetch('http://foo/write-file?path=/tmp/overlay-source/shared.txt', {
2121 method: 'POST',
2122 body: 'from-directory-overlay',
2123 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2124 });
2125 await container
2126 .getTcpPort(8080)
2127 .fetch(
2128 'http://foo/write-file?path=/tmp/overlay-source/overlay-only.txt',
2129 {
2130 method: 'POST',
2131 body: 'overlay-only-content',
2132 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2133 }
2134 );
2135 
2136 const directorySnapshot = await container.snapshotDirectory({
2137 dir: '/tmp/overlay-source',
2138 });
2139 const containerSnapshot = await container.snapshotContainer({
2140 name: 'container-with-overlay',
2141 });
2142 
2143 await container.destroy();
2144 await monitor;
2145 
2146 container.start({
2147 enableInternet: true,
2148 containerSnapshot,
2149 directorySnapshots: [
2150 { snapshot: directorySnapshot, mountPoint: '/app/overlay-target' },
2151 ],
2152 });
2153 const monitor2 = container.monitor().catch((_err) => {});
2154 await this.waitUntilContainerIsHealthy();
2155 
2156 const fullResp = await container
2157 .getTcpPort(8080)
2158 .fetch('http://foo/read-file?path=/app/data/from-full.txt', {
2159 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2160 });
2161 assert.equal(fullResp.status, 200);
2162 assert.strictEqual(await fullResp.text(), 'from-full-snapshot');
2163 
2164 const overlayResp = await container
2165 .getTcpPort(8080)
2166 .fetch('http://foo/read-file?path=/app/overlay-target/shared.txt', {
2167 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2168 });
2169 assert.equal(overlayResp.status, 200);
2170 assert.strictEqual(await overlayResp.text(), 'from-directory-overlay');
2171 
2172 const overlayOnlyResp = await container
2173 .getTcpPort(8080)
2174 .fetch('http://foo/read-file?path=/app/overlay-target/overlay-only.txt', {
2175 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2176 });
2177 assert.equal(overlayOnlyResp.status, 200);
2178 assert.strictEqual(await overlayOnlyResp.text(), 'overlay-only-content');
2179 
2180 const hiddenFullResp = await container
2181 .getTcpPort(8080)
2182 .fetch(
2183 'http://foo/read-file?path=/app/overlay-target/full-only-hidden.txt',
2184 {
2185 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2186 }
2187 );
2188 assert.equal(hiddenFullResp.status, 404);
2189 
2190 await container.destroy();
2191 await monitor2;
2192 }
2193 
2194 async testContainerSnapshotExcludesDirectoryMounts() {
2195 const container = this.ctx.container;
2196 if (container.running) {
2197 const monitor = container.monitor().catch((_err) => {});
2198 await container.destroy();
2199 await monitor;
2200 }
2201 
2202 container.start({ enableInternet: true });
2203 const monitor = container.monitor().catch((_err) => {});
2204 await this.waitUntilContainerIsHealthy();
2205 
2206 await container
2207 .getTcpPort(8080)
2208 .fetch('http://foo/write-file?path=/tmp/mounted-source/mounted.txt', {
2209 method: 'POST',
2210 body: 'from-mounted-directory',
2211 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2212 });
2213 
2214 const directorySnapshot = await container.snapshotDirectory({
2215 dir: '/tmp/mounted-source',
2216 });
2217 
2218 await container.destroy();
2219 await monitor;
2220 
2221 container.start({
2222 enableInternet: true,
2223 directorySnapshots: [
2224 { snapshot: directorySnapshot, mountPoint: '/app/mounted' },
2225 ],
2226 });
2227 const monitor2 = container.monitor().catch((_err) => {});
2228 await this.waitUntilContainerIsHealthy();
2229 
2230 const mountedResp = await container
2231 .getTcpPort(8080)
2232 .fetch('http://foo/read-file?path=/app/mounted/mounted.txt', {
2233 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2234 });
2235 assert.equal(mountedResp.status, 200);
2236 assert.strictEqual(await mountedResp.text(), 'from-mounted-directory');
2237 
2238 await container
2239 .getTcpPort(8080)
2240 .fetch('http://foo/write-file?path=/app/data/local-after-mount.txt', {
2241 method: 'POST',
2242 body: 'container-local-state',
2243 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2244 });
2245 
2246 const containerSnapshot = await container.snapshotContainer({
2247 name: 'exclude-mounted-directory',
2248 });
2249 
2250 await container.destroy();
2251 await monitor2;
2252 
2253 container.start({
2254 enableInternet: true,
2255 containerSnapshot,
2256 });
2257 const monitor3 = container.monitor().catch((_err) => {});
2258 await this.waitUntilContainerIsHealthy();
2259 
2260 const localResp = await container
2261 .getTcpPort(8080)
2262 .fetch('http://foo/read-file?path=/app/data/local-after-mount.txt', {
2263 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2264 });
2265 assert.equal(localResp.status, 200);
2266 assert.strictEqual(await localResp.text(), 'container-local-state');
2267 
2268 const missingMountedResp = await container
2269 .getTcpPort(8080)
2270 .fetch('http://foo/read-file?path=/app/mounted/mounted.txt', {
2271 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2272 });
2273 assert.equal(missingMountedResp.status, 404);
2274 
2275 await container.destroy();
2276 await monitor3;
2277 }
2278 
2279 async testContainerSnapshotRelayerWithDirectoryMounts() {
2280 const container = this.ctx.container;
2281 if (container.running) {
2282 const monitor = container.monitor().catch((_err) => {});
2283 await container.destroy();
2284 await monitor;
2285 }
2286 
2287 container.start({ enableInternet: true });
2288 const monitor = container.monitor().catch((_err) => {});
2289 await this.waitUntilContainerIsHealthy();
2290 
2291 await container
2292 .getTcpPort(8080)
2293 .fetch('http://foo/write-file?path=/tmp/relayer-source/overlay.txt', {
2294 method: 'POST',
2295 body: 'overlay-after-relayer',
2296 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2297 });
2298 
2299 const directorySnapshot = await container.snapshotDirectory({
2300 dir: '/tmp/relayer-source',
2301 });
2302 
2303 await container.destroy();
2304 await monitor;
2305 
2306 container.start({
2307 enableInternet: true,
2308 directorySnapshots: [
2309 { snapshot: directorySnapshot, mountPoint: '/app/relayer' },
2310 ],
2311 });
2312 const monitor2 = container.monitor().catch((_err) => {});
2313 await this.waitUntilContainerIsHealthy();
2314 
2315 await container
2316 .getTcpPort(8080)
2317 .fetch('http://foo/write-file?path=/app/data/re-layered.txt', {
2318 method: 'POST',
2319 body: 'from-full-container-snapshot',
2320 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2321 });
2322 
2323 const containerSnapshot = await container.snapshotContainer({
2324 name: 'relayer-container-snapshot',
2325 });
2326 
2327 await container.destroy();
2328 await monitor2;
2329 
2330 container.start({
2331 enableInternet: true,
2332 containerSnapshot,
2333 directorySnapshots: [
2334 { snapshot: directorySnapshot, mountPoint: '/app/relayer' },
2335 ],
2336 });
2337 const monitor3 = container.monitor().catch((_err) => {});
2338 await this.waitUntilContainerIsHealthy();
2339 
2340 const relayerResp = await container
2341 .getTcpPort(8080)
2342 .fetch('http://foo/read-file?path=/app/relayer/overlay.txt', {
2343 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2344 });
2345 assert.equal(relayerResp.status, 200);
2346 assert.strictEqual(await relayerResp.text(), 'overlay-after-relayer');
2347 
2348 const fullResp = await container
2349 .getTcpPort(8080)
2350 .fetch('http://foo/read-file?path=/app/data/re-layered.txt', {
2351 signal: AbortSignal.timeout(DEFAULT_TIMEOUT_DURATION),
2352 });
2353 assert.equal(fullResp.status, 200);
2354 assert.strictEqual(await fullResp.text(), 'from-full-container-snapshot');
2355 
2356 await container.destroy();
2357 await monitor3;
2358 }
2359}
2360 
2361export class TestService extends WorkerEntrypoint {
2362 fetch(request) {
2363 // Check if this is a WebSocket upgrade request
2364 const upgradeHeader = request.headers.get('Upgrade');
2365 if (upgradeHeader && upgradeHeader.toLowerCase() === 'websocket') {
2366 // Handle WebSocket upgrade
2367 const [client, server] = Object.values(new WebSocketPair());
2368 
2369 server.binaryType = 'arraybuffer';
2370 server.accept();
2371 
2372 server.addEventListener('message', (event) => {
2373 // Echo back with binding id prefix
2374 server.send(
2375 `Binding ${this.ctx.props.id}: ${new TextDecoder().decode(event.data)}`
2376 );
2377 });
2378 
2379 return new Response(null, {
2380 status: 101,
2381 webSocket: client,
2382 });
2383 }
2384 
2385 // Regular HTTP request
2386 return new Response(
2387 'hello binding: ' + this.ctx.props.id + ' ' + request.url
2388 );
2389 }
2390 
2391 // Handle raw TCP connections forwarded by interceptOutboundTcp.
2392 // The socket has .readable / .writable streams. We read until we
2393 // see a newline delimiter, then write back a response that includes
2394 // the binding id and the received message.
2395 async connect(socket) {
2396 const id = this.ctx.props.id;
2397 const enc = new TextEncoder();
2398 const dec = new TextDecoder();
2399 
2400 // Read from the socket until we hit a newline.
2401 const reader = socket.readable.getReader();
2402 let incoming = '';
2403 while (true) {
2404 const { value, done } = await reader.read();
2405 if (done) break;
2406 incoming += dec.decode(value, { stream: true });
2407 if (incoming.includes('\n')) break;
2408 }
2409 incoming += dec.decode();
2410 reader.releaseLock();
2411 
2412 // Write response and close.
2413 const writer = socket.writable.getWriter();
2414 await writer.write(
2415 enc.encode(`tcp binding: ${id} got: ${incoming.trim()}`)
2416 );
2417 await writer.close();
2418 }
2419}
2420 
2421export class DurableObjectExample2 extends DurableObjectExample {}
2422 
2423// Test basic container status
2424export const testStatus = {
2425 async test(_ctrl, env) {
2426 for (const CONTAINER of [env.MY_CONTAINER, env.MY_DUPLICATE_CONTAINER]) {
2427 for (const name of ['testStatus', 'testStatus2']) {
2428 const id = CONTAINER.idFromName(getRandomDurableObjectName(name));
2429 const stub = CONTAINER.get(id);
2430 assert.strictEqual(await stub.getStatus(), false);
2431 }
2432 }
2433 },
2434};
2435 
2436// Test basic container functionality
2437export const testBasics = {
2438 async test(_ctrl, env) {
2439 for (const CONTAINER of [env.MY_CONTAINER, env.MY_DUPLICATE_CONTAINER]) {
2440 const id = CONTAINER.idFromName(getRandomDurableObjectName('testBasics'));
2441 const stub = CONTAINER.get(id);
2442 await stub.testBasics();
2443 }
2444 },
2445};
2446 
2447// Test a variety of common exec() workflows.
2448export const testExec = {
2449 async test(_ctrl, env) {
2450 const id = env.MY_CONTAINER.idFromName(
2451 getRandomDurableObjectName('testExec')
2452 );
2453 const stub = env.MY_CONTAINER.get(id);
2454 await stub.testExec();
2455 },
2456};
2457 
2458// Test exit code monitor functionality
2459export const testExitCode = {
2460 async test(_ctrl, env) {
2461 const id = env.MY_CONTAINER.idFromName(
2462 getRandomDurableObjectName('testExitCode')
2463 );
2464 const stub = env.MY_CONTAINER.get(id);
2465 await stub.testExitCode();
2466 },
2467};
2468 
2469// Test WebSocket functionality
2470export const testWebSockets = {
2471 async test(_ctrl, env) {
2472 const id = env.MY_CONTAINER.idFromName(
2473 getRandomDurableObjectName('testWebsockets')
2474 );
2475 const stub = env.MY_CONTAINER.get(id);
2476 await stub.testWs();
2477 },
2478};
2479 
2480export const testPortNotListening = {
2481 async test(_ctrl, env) {
2482 const id = env.MY_CONTAINER.idFromName(
2483 getRandomDurableObjectName('testPortNotListening')
2484 );
2485 const stub = env.MY_CONTAINER.get(id);
2486 await stub.testPortNotListening();
2487 },
2488};
2489 
2490// Test alarm functionality with containers
2491export const testAlarm = {
2492 async test(_ctrl, env) {
2493 // Test that we can recover the use_containers flag correctly in setAlarm
2494 // after a DO has been evicted
2495 const id = env.MY_CONTAINER.idFromName(
2496 getRandomDurableObjectName('testAlarm')
2497 );
2498 let stub = env.MY_CONTAINER.get(id);
2499 
2500 // Start immediate alarm
2501 await stub.startAlarm(true, 0);
2502 
2503 // Wait for alarm to trigger
2504 let retries = 0;
2505 while ((await stub.getAlarmIndex()) === 0 && retries < 50) {
2506 await scheduler.wait(20);
2507 retries++;
2508 }
2509 
2510 await scheduler.wait(50);
2511 
2512 // Set alarm for future and abort
2513 await stub.startAlarm(false, 1000);
2514 
2515 try {
2516 await stub.abort();
2517 } catch {
2518 // Expected to throw
2519 }
2520 
2521 stub = env.MY_CONTAINER.get(id);
2522 let confirmed = false;
2523 for (let i = 0; i < 50 && !confirmed; i++) {
2524 try {
2525 await stub.checkAlarmAbortConfirmation();
2526 confirmed = true;
2527 } catch (e) {
2528 assert.match(
2529 e.message,
2530 /Abort confirmation did not get inserted/,
2531 `Unexpected error while polling for alarm: ${e.message}`
2532 );
2533 
2534 await scheduler.wait(100);
2535 }
2536 }
2537 if (!confirmed) {
2538 await stub.checkAlarmAbortConfirmation();
2539 }
2540 },
2541};
2542 
2543export const testContainerShutdown = {
2544 async test(_, env) {
2545 const name = getRandomDurableObjectName('testContainerShutdown');
2546 
2547 {
2548 const stub = env.MY_CONTAINER.getByName(name);
2549 await stub.start();
2550 await assert.rejects(() => stub.abort(), {
2551 name: 'Error',
2552 message: 'Application called abort() to reset Durable Object.',
2553 });
2554 }
2555 
2556 // Wait for the container to be shutdown after the DO aborts
2557 await scheduler.wait(500);
2558 
2559 {
2560 const stub = env.MY_CONTAINER.getByName(name);
2561 
2562 // Container should not be running after DO exited
2563 await stub.expectRunning(false);
2564 }
2565 },
2566};
2567 
2568export const testSetInactivityTimeout = {
2569 async test(_ctrl, env) {
2570 const name = getRandomDurableObjectName('testSetInactivityTimeout');
2571 
2572 {
2573 const stub = env.MY_CONTAINER.getByName(name);
2574 
2575 await stub.testSetInactivityTimeout(10_000);
2576 
2577 await assert.rejects(() => stub.abort(), {
2578 name: 'Error',
2579 message: 'Application called abort() to reset Durable Object.',
2580 });
2581 }
2582 
2583 // Here we wait to ensure that if setInactivityTimeout *doesn't* work, the
2584 // container has enough time to shutdown after the DO is aborted. If we
2585 // don't wait then ctx.container.running will always be true, even without
2586 // setInactivityTimeout, because the container won't have stoped yet.
2587 await scheduler.wait(500);
2588 
2589 {
2590 const stub = env.MY_CONTAINER.getByName(name);
2591 
2592 // Container should still be running after DO exited
2593 await stub.expectRunning(true);
2594 }
2595 },
2596};
2597 
2598// Test that custom labels are passed through to the container
2599export const testLabels = {
2600 async test(_ctrl, env) {
2601 const id = env.MY_CONTAINER.idFromName(
2602 getRandomDurableObjectName('testLabels')
2603 );
2604 const stub = env.MY_CONTAINER.get(id);
2605 await stub.testLabels();
2606 },
2607};
2608 
2609// Test that invalid labels are rejected with clear error messages
2610export const testLabelValidation = {
2611 async test(_ctrl, env) {
2612 const id = env.MY_CONTAINER.idFromName(
2613 getRandomDurableObjectName('testLabelValidation')
2614 );
2615 const stub = env.MY_CONTAINER.get(id);
2616 await stub.testLabelValidation();
2617 },
2618};
2619 
2620export const testInspectBeforeStart = {
2621 async test(_ctrl, env) {
2622 const id = env.MY_CONTAINER.idFromName(
2623 getRandomDurableObjectName('testInspectBeforeStart')
2624 );
2625 const stub = env.MY_CONTAINER.get(id);
2626 await stub.testInspectBeforeStart();
2627 },
2628};
2629 
2630export const testInspectEmptyLabels = {
2631 async test(_ctrl, env) {
2632 const id = env.MY_CONTAINER.idFromName(
2633 getRandomDurableObjectName('testInspectEmptyLabels')
2634 );
2635 const stub = env.MY_CONTAINER.get(id);
2636 await stub.testInspectEmptyLabels();
2637 },
2638};
2639 
2640export const testInspectAfterDestroy = {
2641 async test(_ctrl, env) {
2642 const id = env.MY_CONTAINER.idFromName(
2643 getRandomDurableObjectName('testInspectAfterDestroy')
2644 );
2645 const stub = env.MY_CONTAINER.get(id);
2646 await stub.testInspectAfterDestroy();
2647 },
2648};
2649 
2650// Test PID namespace isolation behavior
2651// When containers_pid_namespace is ENABLED, the container has its own isolated PID namespace.
2652// We verify this by checking that PID 1 in the container's namespace is the container's
2653// init process, not the host's init process (systemd, launchd, etc.).
2654export const testPidNamespace = {
2655 async test(_ctrl, env) {
2656 const id = env.MY_CONTAINER.idFromName(
2657 getRandomDurableObjectName('testPidNamespace')
2658 );
2659 const stub = env.MY_CONTAINER.get(id);
2660 const data = await stub.testPidNamespace();
2661 
2662 // When using an isolated PID namespace, PID 1 should be the container's entrypoint
2663 // (a bash script that runs node), not the host's init process (systemd, launchd, init).
2664 assert.match(
2665 data.init,
2666 /container-client-test/,
2667 `Expected PID 1 to be the container entrypoint, but got: ${data.init}`
2668 );
2669 },
2670};
2671 
2672// Test setEgressHttp hostname functionality with internet (check we can establish
2673// outbound with others).
2674export const testSetEgressHttpWithInternet = {
2675 async test(_ctrl, env) {
2676 const id = env.MY_CONTAINER.idFromName(
2677 getRandomDurableObjectName('testSetEgressHttpWithInternet')
2678 );
2679 let stub = env.MY_CONTAINER.get(id);
2680 await stub.testSetEgressHttpWithInternet();
2681 },
2682};
2683 
2684// Test setEgressHttp hostname functionality with no internet
2685export const testSetEgressHttpNoInternet = {
2686 async test(_ctrl, env) {
2687 const id = env.MY_CONTAINER.idFromName(
2688 getRandomDurableObjectName('testSetEgressHttpNoInternet')
2689 );
2690 let stub = env.MY_CONTAINER.get(id);
2691 await stub.testSetEgressHttpNoInternet();
2692 },
2693};
2694 
2695// Test setEgressHttp functionality - registers a binding's channel token with the container
2696export const testSetEgressHttp = {
2697 async test(_ctrl, env) {
2698 const id = env.MY_CONTAINER.idFromName(
2699 getRandomDurableObjectName('testSetEgressHttp')
2700 );
2701 let stub = env.MY_CONTAINER.get(id);
2702 await stub.testSetEgressHttp();
2703 try {
2704 // test we recover from aborts
2705 await stub.abort();
2706 } catch {
2707 // intentionally empty
2708 }
2709 
2710 stub = env.MY_CONTAINER.get(id);
2711 // should work idempotent
2712 await stub.testSetEgressHttp();
2713 },
2714};
2715 
2716export const testSetEgressHttps = {
2717 async test(_ctrl, env) {
2718 const id = env.MY_CONTAINER.idFromName(
2719 getRandomDurableObjectName('testSetEgressHttps')
2720 );
2721 let stub = env.MY_CONTAINER.get(id);
2722 await stub.testSetEgressHttps();
2723 try {
2724 await stub.abort();
2725 } catch {
2726 // intentionally empty — abort may throw if container already stopped
2727 }
2728 
2729 stub = env.MY_CONTAINER.get(id);
2730 await stub.testSetEgressHttps();
2731 },
2732};
2733 
2734export const testSetEgressTcp = {
2735 async test(_ctrl, env) {
2736 const id = env.MY_CONTAINER.idFromName(
2737 getRandomDurableObjectName('testSetEgressTcp')
2738 );
2739 let stub = env.MY_CONTAINER.get(id);
2740 await stub.testSetEgressTcp();
2741 try {
2742 await stub.abort();
2743 } catch {
2744 // intentionally empty
2745 }
2746 
2747 stub = env.MY_CONTAINER.get(id);
2748 await stub.testSetEgressTcp();
2749 },
2750};
2751 
2752// Test WebSocket through interceptOutboundHttp - DO -> container -> worker binding via WebSocket
2753export const testInterceptWebSocket = {
2754 async test(_ctrl, env) {
2755 const id = env.MY_CONTAINER.idFromName(
2756 getRandomDurableObjectName('testInterceptWebSocket')
2757 );
2758 
2759 const stub = env.MY_CONTAINER.get(id);
2760 await stub.testInterceptWebSocket();
2761 },
2762};
2763 
2764export const testInterceptWebSocketHttps = {
2765 async test(_ctrl, env) {
2766 const id = env.MY_CONTAINER.idFromName(
2767 getRandomDurableObjectName('testInterceptWebSocketHttps')
2768 );
2769 
2770 const stub = env.MY_CONTAINER.get(id);
2771 await stub.testInterceptWebSocketHttps();
2772 },
2773};
2774 
2775// Test snapshot round-trip: write file -> snapshot -> destroy -> restore -> verify
2776export const testSnapshotRoundTrip = {
2777 async test(_ctrl, env) {
2778 const id = env.MY_CONTAINER.idFromName(
2779 getRandomDurableObjectName('testSnapshotRoundTrip')
2780 );
2781 const stub = env.MY_CONTAINER.get(id);
2782 await stub.testSnapshotRoundTrip();
2783 },
2784};
2785 
2786// Test snapshot with a human-friendly name
2787export const testSnapshotNamedRoundTrip = {
2788 async test(_ctrl, env) {
2789 const id = env.MY_CONTAINER.idFromName(
2790 getRandomDurableObjectName('testSnapshotNamedRoundTrip')
2791 );
2792 const stub = env.MY_CONTAINER.get(id);
2793 await stub.testSnapshotNamedRoundTrip();
2794 },
2795};
2796 
2797// Test snapshotting multiple directories and restoring them all
2798export const testSnapshotMultipleDirectories = {
2799 async test(_ctrl, env) {
2800 const id = env.MY_CONTAINER.idFromName(
2801 getRandomDurableObjectName('testSnapshotMultipleDirectories')
2802 );
2803 const stub = env.MY_CONTAINER.get(id);
2804 await stub.testSnapshotMultipleDirectories();
2805 },
2806};
2807 
2808// Test that snapshotting a non-existent directory gives a clear error
2809export const testSnapshotNonExistentDirectory = {
2810 async test(_ctrl, env) {
2811 const id = env.MY_CONTAINER.idFromName(
2812 getRandomDurableObjectName('testSnapshotNonExistentDirectory')
2813 );
2814 const stub = env.MY_CONTAINER.get(id);
2815 await stub.testSnapshotNonExistentDirectory();
2816 },
2817};
2818 
2819// Test restoring a snapshot to a different path than where it was captured
2820export const testSnapshotCustomMountPoint = {
2821 async test(_ctrl, env) {
2822 const id = env.MY_CONTAINER.idFromName(
2823 getRandomDurableObjectName('testSnapshotCustomMountPoint')
2824 );
2825 const stub = env.MY_CONTAINER.get(id);
2826 await stub.testSnapshotCustomMountPoint();
2827 },
2828};
2829 
2830// Test that start() rejects an explicit root restore mount point.
2831export const testSnapshotRestoreToRoot = {
2832 async test(_ctrl, env) {
2833 const id = env.MY_CONTAINER.idFromName(
2834 getRandomDurableObjectName('testSnapshotRestoreToRoot')
2835 );
2836 const stub = env.MY_CONTAINER.get(id);
2837 await stub.testSnapshotRestoreToRoot();
2838 },
2839};
2840 
2841// Test that overlapping restore paths work regardless of the user-supplied mount order.
2842export const testSnapshotOverlappingMounts = {
2843 async test(_ctrl, env) {
2844 const id = env.MY_CONTAINER.idFromName(
2845 getRandomDurableObjectName('testSnapshotOverlappingMounts')
2846 );
2847 const stub = env.MY_CONTAINER.get(id);
2848 await stub.testSnapshotOverlappingMounts();
2849 },
2850};
2851 
2852// Test that duplicate effective restore paths are rejected after normalization.
2853export const testSnapshotDuplicateRestoreDirsRejected = {
2854 async test(_ctrl, env) {
2855 const id = env.MY_CONTAINER.idFromName(
2856 getRandomDurableObjectName('testSnapshotDuplicateRestoreDirsRejected')
2857 );
2858 const stub = env.MY_CONTAINER.get(id);
2859 await stub.testSnapshotDuplicateRestoreDirsRejected();
2860 },
2861};
2862 
2863// Test that start() also rejects implicit root restore via snapshot.dir.
2864export const testSnapshotRestoreImplicitRootRejected = {
2865 async test(_ctrl, env) {
2866 const id = env.MY_CONTAINER.idFromName(
2867 getRandomDurableObjectName('testSnapshotRestoreImplicitRootRejected')
2868 );
2869 const stub = env.MY_CONTAINER.get(id);
2870 await stub.testSnapshotRestoreImplicitRootRejected();
2871 },
2872};
2873 
2874// Test that start() rejects relative restore mount points at the API boundary.
2875export const testSnapshotRestoreRelativeMountPointRejected = {
2876 async test(_ctrl, env) {
2877 const id = env.MY_CONTAINER.idFromName(
2878 getRandomDurableObjectName(
2879 'testSnapshotRestoreRelativeMountPointRejected'
2880 )
2881 );
2882 const stub = env.MY_CONTAINER.get(id);
2883 await stub.testSnapshotRestoreRelativeMountPointRejected();
2884 },
2885};
2886 
2887// Test that snapshotDirectory() on a stopped container gives a clear error
2888export const testSnapshotStoppedContainer = {
2889 async test(_ctrl, env) {
2890 const id = env.MY_CONTAINER.idFromName(
2891 getRandomDurableObjectName('testSnapshotStoppedContainer')
2892 );
2893 const stub = env.MY_CONTAINER.get(id);
2894 await stub.testSnapshotStoppedContainer();
2895 },
2896};
2897 
2898// Test that restoring a snapshot with a nonexistent ID fails
2899export const testSnapshotRestoreNonExistentId = {
2900 async test(_ctrl, env) {
2901 const id = env.MY_CONTAINER.idFromName(
2902 getRandomDurableObjectName('testSnapshotRestoreNonExistentId')
2903 );
2904 const stub = env.MY_CONTAINER.get(id);
2905 await stub.testSnapshotRestoreNonExistentId();
2906 },
2907};
2908 
2909// Test that a snapshot created by one DO can be sent to another DO and restored there.
2910export const testSnapshotCrossDoRestore = {
2911 async test(_ctrl, env) {
2912 const sourceId = env.MY_CONTAINER.idFromName(
2913 getRandomDurableObjectName('testSnapshotCrossDoRestore-source')
2914 );
2915 const targetId = env.MY_DUPLICATE_CONTAINER.idFromName(
2916 getRandomDurableObjectName('testSnapshotCrossDoRestore-target')
2917 );
2918 
2919 const source = env.MY_CONTAINER.get(sourceId);
2920 const target = env.MY_DUPLICATE_CONTAINER.get(targetId);
2921 
2922 const snapshot = await source.createSnapshotForTransfer();
2923 assert.strictEqual(snapshot.dir, '/app/data');
2924 assert.strictEqual(snapshot.name, 'cross-do-snapshot');
2925 
2926 await target.restoreTransferredSnapshot(snapshot);
2927 },
2928};
2929 
2930// Test full container snapshot round-trip: write file -> snapshot -> destroy -> restore -> verify
2931export const testContainerSnapshotRoundTrip = {
2932 async test(_ctrl, env) {
2933 const id = env.MY_CONTAINER.idFromName(
2934 getRandomDurableObjectName('testContainerSnapshotRoundTrip')
2935 );
2936 const stub = env.MY_CONTAINER.get(id);
2937 await stub.testContainerSnapshotRoundTrip();
2938 },
2939};
2940 
2941// Test full container snapshot with a human-friendly name.
2942export const testContainerSnapshotNamedRoundTrip = {
2943 async test(_ctrl, env) {
2944 const id = env.MY_CONTAINER.idFromName(
2945 getRandomDurableObjectName('testContainerSnapshotNamedRoundTrip')
2946 );
2947 const stub = env.MY_CONTAINER.get(id);
2948 await stub.testContainerSnapshotNamedRoundTrip();
2949 },
2950};
2951 
2952// Test that a full container snapshot created by one DO can be restored by another.
2953export const testContainerSnapshotCrossDoRestore = {
2954 async test(_ctrl, env) {
2955 const sourceId = env.MY_CONTAINER.idFromName(
2956 getRandomDurableObjectName('testContainerSnapshotCrossDoRestore-source')
2957 );
2958 const targetId = env.MY_DUPLICATE_CONTAINER.idFromName(
2959 getRandomDurableObjectName('testContainerSnapshotCrossDoRestore-target')
2960 );
2961 
2962 const source = env.MY_CONTAINER.get(sourceId);
2963 const target = env.MY_DUPLICATE_CONTAINER.get(targetId);
2964 
2965 const snapshot = await source.createContainerSnapshotForTransfer();
2966 assert.strictEqual(snapshot.name, 'cross-do-container-snapshot');
2967 
2968 await target.restoreTransferredContainerSnapshot(snapshot);
2969 },
2970};
2971 
2972// Test that restoring a full container snapshot with a nonexistent ID fails.
2973export const testContainerSnapshotRestoreNonExistentId = {
2974 async test(_ctrl, env) {
2975 const id = env.MY_CONTAINER.idFromName(
2976 getRandomDurableObjectName('testContainerSnapshotRestoreNonExistentId')
2977 );
2978 const stub = env.MY_CONTAINER.get(id);
2979 await stub.testContainerSnapshotRestoreNonExistentId();
2980 },
2981};
2982 
2983// Test that a full container snapshot can be layered with a directory snapshot restore.
2984export const testContainerSnapshotWithDirectoryOverlay = {
2985 async test(_ctrl, env) {
2986 const id = env.MY_CONTAINER.idFromName(
2987 getRandomDurableObjectName('testContainerSnapshotWithDirectoryOverlay')
2988 );
2989 const stub = env.MY_CONTAINER.get(id);
2990 await stub.testContainerSnapshotWithDirectoryOverlay();
2991 },
2992};
2993 
2994// Test that full container snapshots exclude active directory snapshot mounts.
2995export const testContainerSnapshotExcludesDirectoryMounts = {
2996 async test(_ctrl, env) {
2997 const id = env.MY_CONTAINER.idFromName(
2998 getRandomDurableObjectName('testContainerSnapshotExcludesDirectoryMounts')
2999 );
3000 const stub = env.MY_CONTAINER.get(id);
3001 await stub.testContainerSnapshotExcludesDirectoryMounts();
3002 },
3003};
3004 
3005// Test that a full container snapshot taken while directory snapshots are active can be re-layered.
3006export const testContainerSnapshotRelayerWithDirectoryMounts = {
3007 async test(_ctrl, env) {
3008 const id = env.MY_CONTAINER.idFromName(
3009 getRandomDurableObjectName(
3010 'testContainerSnapshotRelayerWithDirectoryMounts'
3011 )
3012 );
3013 const stub = env.MY_CONTAINER.get(id);
3014 await stub.testContainerSnapshotRelayerWithDirectoryMounts();
3015 },
3016};