@namzu/sandbox 2.0.2 → 2.0.3

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 (72) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +12 -2
  3. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.d.ts +0 -2
  4. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.d.ts.map +0 -1
  5. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.js +0 -61
  6. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.js.map +0 -1
  7. package/dist/backends/docker/__tests__/hardening.test.d.ts +0 -2
  8. package/dist/backends/docker/__tests__/hardening.test.d.ts.map +0 -1
  9. package/dist/backends/docker/__tests__/hardening.test.js +0 -32
  10. package/dist/backends/docker/__tests__/hardening.test.js.map +0 -1
  11. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts +0 -40
  12. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts.map +0 -1
  13. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js +0 -157
  14. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js.map +0 -1
  15. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.d.ts +0 -16
  16. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.d.ts.map +0 -1
  17. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.js +0 -37
  18. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.js.map +0 -1
  19. package/dist/backends/firecracker/__tests__/backend.test.d.ts +0 -13
  20. package/dist/backends/firecracker/__tests__/backend.test.d.ts.map +0 -1
  21. package/dist/backends/firecracker/__tests__/backend.test.js +0 -361
  22. package/dist/backends/firecracker/__tests__/backend.test.js.map +0 -1
  23. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts +0 -19
  24. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts.map +0 -1
  25. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js +0 -209
  26. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js.map +0 -1
  27. package/dist/backends/firecracker/__tests__/egress-policy.test.d.ts +0 -2
  28. package/dist/backends/firecracker/__tests__/egress-policy.test.d.ts.map +0 -1
  29. package/dist/backends/firecracker/__tests__/egress-policy.test.js +0 -67
  30. package/dist/backends/firecracker/__tests__/egress-policy.test.js.map +0 -1
  31. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.d.ts +0 -21
  32. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.d.ts.map +0 -1
  33. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.js +0 -30
  34. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.js.map +0 -1
  35. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts +0 -39
  36. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts.map +0 -1
  37. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js +0 -149
  38. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js.map +0 -1
  39. package/dist/backends/firecracker/__tests__/protocol.test.d.ts +0 -6
  40. package/dist/backends/firecracker/__tests__/protocol.test.d.ts.map +0 -1
  41. package/dist/backends/firecracker/__tests__/protocol.test.js +0 -67
  42. package/dist/backends/firecracker/__tests__/protocol.test.js.map +0 -1
  43. package/dist/backends/firecracker/__tests__/transport.test.d.ts +0 -20
  44. package/dist/backends/firecracker/__tests__/transport.test.d.ts.map +0 -1
  45. package/dist/backends/firecracker/__tests__/transport.test.js +0 -457
  46. package/dist/backends/firecracker/__tests__/transport.test.js.map +0 -1
  47. package/dist/egress/__tests__/allowlist.test.d.ts +0 -2
  48. package/dist/egress/__tests__/allowlist.test.d.ts.map +0 -1
  49. package/dist/egress/__tests__/allowlist.test.js +0 -85
  50. package/dist/egress/__tests__/allowlist.test.js.map +0 -1
  51. package/dist/egress/__tests__/proxy.test.d.ts +0 -2
  52. package/dist/egress/__tests__/proxy.test.d.ts.map +0 -1
  53. package/dist/egress/__tests__/proxy.test.js +0 -177
  54. package/dist/egress/__tests__/proxy.test.js.map +0 -1
  55. package/dist/index.test.d.ts +0 -28
  56. package/dist/index.test.d.ts.map +0 -1
  57. package/dist/index.test.js +0 -649
  58. package/dist/index.test.js.map +0 -1
  59. package/src/backends/aci-standby-pool/__tests__/unenforceable-controls.test.ts +0 -69
  60. package/src/backends/docker/__tests__/hardening.test.ts +0 -43
  61. package/src/backends/docker/__tests__/leaf-permissions.smoke.test.ts +0 -169
  62. package/src/backends/firecracker/__tests__/agent-timeout-clamp.test.ts +0 -48
  63. package/src/backends/firecracker/__tests__/backend.test.ts +0 -429
  64. package/src/backends/firecracker/__tests__/control-plane-mtls.test.ts +0 -261
  65. package/src/backends/firecracker/__tests__/egress-policy.test.ts +0 -91
  66. package/src/backends/firecracker/__tests__/fixtures/ipc-path.ts +0 -31
  67. package/src/backends/firecracker/__tests__/fixtures/mtls-pki.ts +0 -166
  68. package/src/backends/firecracker/__tests__/protocol.test.ts +0 -75
  69. package/src/backends/firecracker/__tests__/transport.test.ts +0 -534
  70. package/src/egress/__tests__/allowlist.test.ts +0 -103
  71. package/src/egress/__tests__/proxy.test.ts +0 -212
  72. package/src/index.test.ts +0 -709
@@ -1,91 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
-
3
- import type { SandboxBackendOptions } from '../../../index.js'
4
- import { resolveEgressAllowlist } from '../index.js'
5
-
6
- /**
7
- * Omitting the allowlist means "no allowlist to apply", which the
8
- * orchestrator reads as unrestricted. That makes omission the encoding for
9
- * `allow-all` and only for `allow-all`.
10
- *
11
- * `resolver` used to be omitted too — so two opposite intentions shared one
12
- * encoding, the tenant-scoped allowlist the variant exists for was silently
13
- * absent, and the callback that would have produced it was never called
14
- * anywhere in the repo. Whichever way the orchestrator reads an omitted
15
- * field, one of the two variants was always mis-enforced, and the one that
16
- * failed OPEN was the one whose entire purpose is restriction.
17
- */
18
-
19
- const opts = (egress?: SandboxBackendOptions['egress']): SandboxBackendOptions => ({
20
- workingDirectory: '/w',
21
- ...(egress ? { egress } : {}),
22
- })
23
-
24
- describe('each policy gets its own encoding', () => {
25
- it('allow-all omits the field', async () => {
26
- expect(await resolveEgressAllowlist(opts({ kind: 'allow-all' }))).toBeUndefined()
27
- })
28
-
29
- it('deny-all sends an explicitly empty list, not an absent one', async () => {
30
- expect(await resolveEgressAllowlist(opts({ kind: 'deny-all' }))).toEqual([])
31
- })
32
-
33
- it('static forwards the hosts as given', async () => {
34
- expect(
35
- await resolveEgressAllowlist(opts({ kind: 'static', allowedHosts: ['a.example'] })),
36
- ).toEqual(['a.example'])
37
- })
38
-
39
- it('resolver calls the callback and forwards what it returned', async () => {
40
- const resolve = vi.fn(async () => ['tenant-a.example', 'tenant-b.example'])
41
- expect(await resolveEgressAllowlist(opts({ kind: 'resolver', resolve }))).toEqual([
42
- 'tenant-a.example',
43
- 'tenant-b.example',
44
- ])
45
- // The callback IS the feature. It was declared and never invoked.
46
- expect(resolve).toHaveBeenCalledOnce()
47
- })
48
-
49
- it('never encodes resolver the same way as allow-all', async () => {
50
- const restrictive = await resolveEgressAllowlist(
51
- opts({ kind: 'resolver', resolve: async () => ['only-this.example'] }),
52
- )
53
- const unrestricted = await resolveEgressAllowlist(opts({ kind: 'allow-all' }))
54
- expect(restrictive).not.toEqual(unrestricted)
55
- })
56
-
57
- it('sends an empty resolver result as a real deny-all', async () => {
58
- // An empty allowlist is a decision, not an absence. Collapsing it
59
- // back to omission would turn "this tenant may reach nothing" into
60
- // "this tenant may reach everything".
61
- expect(
62
- await resolveEgressAllowlist(opts({ kind: 'resolver', resolve: async () => [] })),
63
- ).toEqual([])
64
- })
65
-
66
- it('omits the field when the host set no policy at all', async () => {
67
- expect(await resolveEgressAllowlist(opts())).toBeUndefined()
68
- })
69
- })
70
-
71
- describe('an unknown policy', () => {
72
- it('refuses rather than defaulting to unrestricted', async () => {
73
- await expect(resolveEgressAllowlist(opts({ kind: 'something-new' } as never))).rejects.toThrow(
74
- /Refusing rather than defaulting to unrestricted/,
75
- )
76
- })
77
-
78
- it('propagates a failing resolver instead of falling back to open', async () => {
79
- // A resolver that cannot answer must not become "allow everything".
80
- await expect(
81
- resolveEgressAllowlist(
82
- opts({
83
- kind: 'resolver',
84
- resolve: async () => {
85
- throw new Error('directory unreachable')
86
- },
87
- }),
88
- ),
89
- ).rejects.toThrow('directory unreachable')
90
- })
91
- })
@@ -1,31 +0,0 @@
1
- import { randomUUID } from 'node:crypto'
2
- import { join } from 'node:path'
3
-
4
- /**
5
- * A local IPC address Node can actually `listen()` on, for the current OS.
6
- *
7
- * These suites bind a loopback agent over a Unix domain socket in a temp
8
- * directory. Windows supports `AF_UNIX` at the OS level, but Node's `net`
9
- * server does not bind filesystem sockets there — it wants a **named
10
- * pipe**, so every one of these tests failed with
11
- * `listen EACCES: permission denied …\agent.sock` and the whole
12
- * `@namzu/sandbox` suite has been red for Windows contributors.
13
- *
14
- * The transport under test is address-agnostic: it is handed a path and
15
- * connects to it. So the fix belongs in the fixture, not the driver.
16
- */
17
- export function localIpcPath(workDir: string, name = 'agent'): string {
18
- if (process.platform === 'win32') {
19
- // Named pipes are not filesystem entries, so the temp dir cannot
20
- // namespace them — a uuid keeps concurrent test files apart.
21
- return `\\\\.\\pipe\\namzu-${name}-${randomUUID()}`
22
- }
23
- return join(workDir, `${name}.sock`)
24
- }
25
-
26
- /**
27
- * True when the socket address lives on disk and can be cleaned up with
28
- * the temp directory. Named pipes disappear with the process that owns
29
- * them, so there is nothing to unlink.
30
- */
31
- export const IPC_IS_FILESYSTEM_PATH = process.platform !== 'win32'
@@ -1,166 +0,0 @@
1
- /**
2
- * Shared Ring-0 mTLS test PKI + a loopback relay stand-in.
3
- *
4
- * A throwaway P-256 test PKI (CA + server leaf + client leaf + an unrelated
5
- * rogue client) baked in as PEM constants — no openssl/forge at test time, so
6
- * the package stays dependency-light. Minted offline:
7
- * openssl ec/x509 self-signed CA + leaves
8
- * (server SAN = DNS:sandbox.fc.internal, IP:127.0.0.1), far-future expiry.
9
- * `ROGUE_*` chains to a DIFFERENT CA to prove `rejectUnauthorized` blocks it.
10
- *
11
- * Used by both transport.test.ts (the raw `kind:'mtls'` dialer arm) and
12
- * backend.test.ts (the cert-INJECTION seam through `buildFirecrackerBackend`),
13
- * so the PKI + relay harness lives in one place instead of per-test copies.
14
- */
15
-
16
- import { connect as netConnect } from 'node:net'
17
- import { type TLSSocket, type Server as TlsServer, createServer as createTlsServer } from 'node:tls'
18
-
19
- export const CA_CRT = `-----BEGIN CERTIFICATE-----
20
- MIIBjjCCATWgAwIBAgIUGXUtG14KQr4spNGW7rpfjZGoBIIwCgYIKoZIzj0EAwIw
21
- HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
22
- MjEyNjA1MjMwODMyNTJaMBwxGjAYBgNVBAMMEXZhbmRhbC1mYy10ZXN0LWNhMFkw
23
- EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5l+vjhjm/PX02D55n56EwelgvM1jrqQM
24
- Z+ezzvMmrZEBGWInvBND3z3kc6nv7snu6fHT2HHOyhwUNoLqmrIg/qNTMFEwHQYD
25
- VR0OBBYEFP0M8/KZY1ovxyGDci5iuoYXcmZgMB8GA1UdIwQYMBaAFP0M8/KZY1ov
26
- xyGDci5iuoYXcmZgMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIg
27
- C0xtFDASnwsOYT0GiZwzYWTOBfgmjDWK7ANXQQFNTh4CICVLQwq5eLy3424drEHk
28
- 5Ol/FNKVZcGRjE96rB0mRUOp
29
- -----END CERTIFICATE-----
30
- `
31
-
32
- export const SERVER_CRT = `-----BEGIN CERTIFICATE-----
33
- MIIBsDCCAVWgAwIBAgIUYkXroqlz9blXW8llis6e2OxOc1MwCgYIKoZIzj0EAwIw
34
- HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
35
- MjEyNjA1MjMwODMyNTJaMBIxEDAOBgNVBAMMB2ZjLWhvc3QwWTATBgcqhkjOPQIB
36
- BggqhkjOPQMBBwNCAAQodCSHY8Sz5Lh/4K6+SzekkrRCeKMBfmonVb7dUhM0ZWpo
37
- ePCcRrMcJp5aNxLxEgkl2EMMMMwE3LlLSOX5LZz6o30wezAkBgNVHREEHTAbghNz
38
- YW5kYm94LmZjLmludGVybmFshwR/AAABMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0G
39
- A1UdDgQWBBRXHU1vlX7WvqzcbaNxRxV4f3gfDDAfBgNVHSMEGDAWgBT9DPPymWNa
40
- L8chg3IuYrqGF3JmYDAKBggqhkjOPQQDAgNJADBGAiEAlqzKqgBf5hxJqd26QwcY
41
- n3cvEKi4f2BSLe1Rfzr5oSoCIQD0izUOJXLNvmac9cMgV0HvkftBerDKFOcGeGTN
42
- g3Eunw==
43
- -----END CERTIFICATE-----
44
- `
45
-
46
- export const SERVER_KEY = `-----BEGIN PRIVATE KEY-----
47
- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/ROnYXIkMKnipbLt
48
- JnFEwXAifgeOv9TGLcK2RojE7jmhRANCAAQodCSHY8Sz5Lh/4K6+SzekkrRCeKMB
49
- fmonVb7dUhM0ZWpoePCcRrMcJp5aNxLxEgkl2EMMMMwE3LlLSOX5LZz6
50
- -----END PRIVATE KEY-----
51
- `
52
-
53
- export const CLIENT_CRT = `-----BEGIN CERTIFICATE-----
54
- MIIBjjCCATWgAwIBAgIUYkXroqlz9blXW8llis6e2OxOc1QwCgYIKoZIzj0EAwIw
55
- HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
56
- MjEyNjA1MjMwODMyNTJaMBgxFjAUBgNVBAMMDWNhLXZhbmRhbC1hcHAwWTATBgcq
57
- hkjOPQIBBggqhkjOPQMBBwNCAASlCMCwjtrQUicWcWsO29R5S7fzjMbbxXvDh8/K
58
- w57x/PN/uQwLHWCz1Tsyk0FnbrwGP+nPtwrUPIxrLN//euXko1cwVTATBgNVHSUE
59
- DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQU2cxDCNrlVkk4ODs6ZPrkC6GzHjkwHwYD
60
- VR0jBBgwFoAU/Qzz8pljWi/HIYNyLmK6hhdyZmAwCgYIKoZIzj0EAwIDRwAwRAIg
61
- RghqArNm3bXnWnRi+jkEEJa8mRb2z0Wj4G7SIhtMurUCIBQSllVtICgZB9hvcM0D
62
- d24fcW4nlfvaZJMJ6dRC1Wle
63
- -----END CERTIFICATE-----
64
- `
65
-
66
- export const CLIENT_KEY = `-----BEGIN PRIVATE KEY-----
67
- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaamj/ycrEuPqpiC2
68
- IhAYx/M5iN4t4P+B17V4GO54Nb2hRANCAASlCMCwjtrQUicWcWsO29R5S7fzjMbb
69
- xXvDh8/Kw57x/PN/uQwLHWCz1Tsyk0FnbrwGP+nPtwrUPIxrLN//euXk
70
- -----END PRIVATE KEY-----
71
- `
72
-
73
- export const ROGUE_CLIENT_CRT = `-----BEGIN CERTIFICATE-----
74
- MIIBhjCCASugAwIBAgIUbAYOlW4PQGyXjjRsjCPy62EHUmIwCgYIKoZIzj0EAwIw
75
- EzERMA8GA1UEAwwIcm9ndWUtY2EwIBcNMjYwNjE2MDgzMjUyWhgPMjEyNjA1MjMw
76
- ODMyNTJaMBcxFTATBgNVBAMMDHJvZ3VlLWNsaWVudDBZMBMGByqGSM49AgEGCCqG
77
- SM49AwEHA0IABOPSzdzQTQz3M5CDHLVPguvHd10ncaDV9t4zKUy/PCE+U7GTJwN2
78
- TtypGnmYEahbcl45j94hwu495P5VbR5ohb6jVzBVMBMGA1UdJQQMMAoGCCsGAQUF
79
- BwMCMB0GA1UdDgQWBBTf0woauniche3/ps77+Kd0dsbbSDAfBgNVHSMEGDAWgBSX
80
- Yu0IJlPxi79Gyi5hR1IfS/sqyzAKBggqhkjOPQQDAgNJADBGAiEAxD4N4XWtaHPJ
81
- yaCOzQP0e5RRyNst3QrhH0NSMyPw89wCIQC+bezLX97TuV1sId53Bo1l9j1TSOMa
82
- EZotN/Wsq6L+qw==
83
- -----END CERTIFICATE-----
84
- `
85
-
86
- export const ROGUE_CLIENT_KEY = `-----BEGIN PRIVATE KEY-----
87
- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR6fGq27O9eT/hi0Q
88
- c+L5+lSm5fbDEghVJCo6k7HrG62hRANCAATj0s3c0E0M9zOQgxy1T4Lrx3ddJ3Gg
89
- 1fbeMylMvzwhPlOxkycDdk7cqRp5mBGoW3JeOY/eIcLuPeT+VW0eaIW+
90
- -----END PRIVATE KEY-----
91
- `
92
-
93
- export interface RelayHandle {
94
- readonly server: TlsServer
95
- readonly port: number
96
- /** sandboxId from the first connection's `SANDBOX <id>` preamble. */
97
- preamble(): Promise<string>
98
- }
99
-
100
- /**
101
- * A loopback stand-in for the per-FC-host mTLS relay.
102
- *
103
- * Terminates mTLS (requestCert + rejectUnauthorized + CA pin), reads the
104
- * `SANDBOX <id>\n` routing preamble line, then bridges the rest of the TLS
105
- * stream verbatim to a FRESH `net.connect` to `agentSockPath` (the
106
- * unix-socket-backed real agent) — exactly the dumb byte-pump the production
107
- * relay is. It does NOT speak framing itself and does NOT send an ack line.
108
- */
109
- export function startMtlsRelay(agentSockPath: string, listenPort = 0): Promise<RelayHandle> {
110
- let resolvePreamble: (id: string) => void
111
- const preamble = new Promise<string>((r) => {
112
- resolvePreamble = r
113
- })
114
-
115
- return new Promise<RelayHandle>((resolve, reject) => {
116
- const server = createTlsServer(
117
- {
118
- requestCert: true,
119
- rejectUnauthorized: true,
120
- ca: CA_CRT,
121
- cert: SERVER_CRT,
122
- key: SERVER_KEY,
123
- minVersion: 'TLSv1.3',
124
- },
125
- (tlsSock: TLSSocket) => {
126
- let pre = Buffer.alloc(0)
127
- const onPreambleData = (chunk: Buffer) => {
128
- pre = Buffer.concat([pre, chunk])
129
- const nl = pre.indexOf(0x0a)
130
- if (nl < 0) return
131
- tlsSock.removeListener('data', onPreambleData)
132
- tlsSock.pause()
133
- const line = pre.subarray(0, nl).toString('utf8')
134
- const rest = pre.subarray(nl + 1)
135
- if (rest.length > 0) tlsSock.unshift(rest)
136
- const m = /^SANDBOX (.+)$/.exec(line)
137
- const id = m?.[1]
138
- if (id === undefined) {
139
- tlsSock.destroy()
140
- return
141
- }
142
- resolvePreamble(id)
143
- // 1 inbound mTLS conn -> 1 FRESH local agent connect (the
144
- // resume-survival invariant the relay must preserve).
145
- const upstream = netConnect({ path: agentSockPath })
146
- upstream.on('error', () => tlsSock.destroy())
147
- tlsSock.on('error', () => upstream.destroy())
148
- upstream.once('connect', () => {
149
- tlsSock.pipe(upstream)
150
- upstream.pipe(tlsSock)
151
- })
152
- }
153
- tlsSock.on('data', onPreambleData)
154
- },
155
- )
156
- server.on('error', reject)
157
- server.listen(listenPort, '127.0.0.1', () => {
158
- const addr = server.address()
159
- if (addr === null || typeof addr === 'string') {
160
- reject(new Error('relay: no TCP port'))
161
- return
162
- }
163
- resolve({ server, port: addr.port, preamble: () => preamble })
164
- })
165
- })
166
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * Unit-pins the shared NDJSON codec the vsock + HTTP transports both
3
- * speak, and the `pickBackend` routing for `microvm:self-hosted`.
4
- */
5
-
6
- import { describe, expect, it } from 'vitest'
7
-
8
- import { SandboxBackendNotImplementedError, createSandboxProvider } from '../../../index.js'
9
- import { ExecResultAccumulator, parseExecLine } from '../protocol.js'
10
-
11
- describe('ExecResultAccumulator', () => {
12
- it('accumulates stdout/stderr deltas and captures the terminal result', () => {
13
- const acc = new ExecResultAccumulator(0)
14
- expect(acc.push({ type: 'stdout_delta', data: 'a' })).toBe(false)
15
- expect(acc.push({ type: 'stdout_delta', data: 'b' })).toBe(false)
16
- expect(acc.push({ type: 'stderr_delta', data: 'E' })).toBe(false)
17
- expect(acc.done).toBe(false)
18
- expect(acc.push({ type: 'result', exitCode: 0, timedOut: false })).toBe(true)
19
- expect(acc.done).toBe(true)
20
- const r = acc.finish()
21
- expect(r.stdout).toBe('ab')
22
- expect(r.stderr).toBe('E')
23
- expect(r.exitCode).toBe(0)
24
- expect(r.timedOut).toBe(false)
25
- })
26
-
27
- it('throws on an error event (docker loop parity)', () => {
28
- const acc = new ExecResultAccumulator(0)
29
- expect(() => acc.push({ type: 'error', error: 'boom' })).toThrow('boom')
30
- })
31
- })
32
-
33
- describe('parseExecLine', () => {
34
- it('parses a valid NDJSON line', () => {
35
- expect(parseExecLine('{"type":"stdout_delta","data":"x"}')).toEqual({
36
- type: 'stdout_delta',
37
- data: 'x',
38
- })
39
- })
40
- it('returns undefined for blank or malformed lines (SyntaxError swallow)', () => {
41
- expect(parseExecLine('')).toBeUndefined()
42
- expect(parseExecLine(' ')).toBeUndefined()
43
- expect(parseExecLine('{not json')).toBeUndefined()
44
- })
45
- })
46
-
47
- describe('pickBackend — microvm:self-hosted', () => {
48
- it('builds the firecracker backend when orchestratorEndpoint + getToken are present', () => {
49
- const provider = createSandboxProvider({
50
- backend: {
51
- tier: 'microvm',
52
- service: 'self-hosted',
53
- orchestratorEndpoint: 'https://orchestrator.test',
54
- getToken: async () => 'tok',
55
- template: 'golden-rev-1',
56
- },
57
- })
58
- expect(provider.id).toContain('microvm')
59
- expect(provider.id).toContain('firecracker')
60
- expect(provider.name).toContain('microvm:self-hosted')
61
- })
62
-
63
- it('refuses the shape that reaches no orchestrator', () => {
64
- // The control-plane endpoint and its bearer are required now. They
65
- // used to be optional beside three REQUIRED fields belonging to a
66
- // local-daemon path that was never written, so the only working
67
- // configuration had to supply three values nothing reads — and
68
- // omitting these two type-checked its way to a runtime throw.
69
- expect(() =>
70
- createSandboxProvider({
71
- backend: { tier: 'microvm', service: 'self-hosted' },
72
- } as never),
73
- ).toThrow(SandboxBackendNotImplementedError)
74
- })
75
- })