@brimble/sandbox 0.1.0 → 0.1.2

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 (61) hide show
  1. package/README.md +9 -5
  2. package/dist/package.json +10 -3
  3. package/dist/src/client.d.ts +3 -3
  4. package/dist/src/client.js +3 -3
  5. package/dist/src/index.d.ts +3 -3
  6. package/dist/src/index.js +2 -2
  7. package/dist/src/resources/files.d.ts +6 -1
  8. package/dist/src/resources/files.js +36 -0
  9. package/dist/src/resources/sandbox-handle.d.ts +6 -1
  10. package/dist/src/resources/sandbox-handle.js +8 -0
  11. package/dist/src/resources/scoped-sandbox.d.ts +3 -1
  12. package/dist/src/resources/scoped-sandbox.js +4 -0
  13. package/dist/src/types/files.d.ts +16 -0
  14. package/dist/src/types/index.d.ts +1 -1
  15. package/package.json +7 -3
  16. package/CODEX.md +0 -188
  17. package/PLAN.md +0 -364
  18. package/src/client.ts +0 -61
  19. package/src/constants.ts +0 -17
  20. package/src/enums/code-language.ts +0 -4
  21. package/src/enums/destroy-reason.ts +0 -8
  22. package/src/enums/destroy-timeout.ts +0 -8
  23. package/src/enums/index.ts +0 -7
  24. package/src/enums/sandbox-status.ts +0 -9
  25. package/src/enums/snapshot-mode.ts +0 -4
  26. package/src/enums/snapshot-status.ts +0 -5
  27. package/src/enums/volume-type.ts +0 -3
  28. package/src/errors/index.ts +0 -2
  29. package/src/errors/sandbox-api-error.ts +0 -54
  30. package/src/index.ts +0 -71
  31. package/src/resources/exec.ts +0 -56
  32. package/src/resources/files.ts +0 -46
  33. package/src/resources/index.ts +0 -8
  34. package/src/resources/path.ts +0 -16
  35. package/src/resources/sandbox-handle.ts +0 -215
  36. package/src/resources/sandboxes.ts +0 -297
  37. package/src/resources/scoped-sandbox.ts +0 -65
  38. package/src/resources/snapshots.ts +0 -104
  39. package/src/resources/stats.ts +0 -30
  40. package/src/resources/volumes.ts +0 -95
  41. package/src/transport/auth.ts +0 -4
  42. package/src/transport/http.ts +0 -501
  43. package/src/transport/pagination.ts +0 -10
  44. package/src/types/exec.ts +0 -42
  45. package/src/types/files.ts +0 -1
  46. package/src/types/index.ts +0 -23
  47. package/src/types/pagination.ts +0 -16
  48. package/src/types/region.ts +0 -19
  49. package/src/types/sandbox.ts +0 -103
  50. package/src/types/snapshot.ts +0 -17
  51. package/src/types/stats.ts +0 -35
  52. package/src/types/template.ts +0 -5
  53. package/src/types/volume.ts +0 -26
  54. package/test/integration/sandbox.integration.test.ts +0 -269
  55. package/test/unit/client.test.ts +0 -87
  56. package/test/unit/sandboxes.test.ts +0 -69
  57. package/test/unit/transport.test.ts +0 -126
  58. package/test/unit/volumes.test.ts +0 -122
  59. package/tsconfig.json +0 -16
  60. package/vitest.config.ts +0 -12
  61. package/vitest.integration.config.ts +0 -15
@@ -1,103 +0,0 @@
1
- import { DestroyReason, DestroyTimeout, SandboxStatus, SnapshotMode } from '../enums';
2
- import type { RequestOptions } from '../transport/http';
3
- import type { RegionSummary } from './region';
4
- import type { CreateVolumeInput, Volume } from './volume';
5
-
6
- export type SandboxSpecs = {
7
- cpu?: number;
8
- memory?: number;
9
- disk?: number;
10
- };
11
-
12
- export type SandboxRegionInput = string | 'auto';
13
-
14
- export type CreateSandboxInput = {
15
- name?: string;
16
- template?: string;
17
- teamId?: string;
18
- environmentId?: string;
19
- region?: string;
20
- specs?: SandboxSpecs;
21
- autoDestroy?: boolean;
22
- destroyTimeout?: DestroyTimeout;
23
- oneShot?: boolean;
24
- blockOutbound?: boolean;
25
- persistent?: boolean;
26
- persistentDiskGB?: number;
27
- volumeId?: string;
28
- fromSnapshot?: string;
29
- snapshotMode?: SnapshotMode;
30
- snapshotFrequency?: string;
31
- };
32
-
33
- export type CreateSandboxRequest = Omit<CreateSandboxInput, 'region'> & {
34
- region?: SandboxRegionInput;
35
- };
36
-
37
- export type CreateSandboxResult = {
38
- id: string;
39
- name: string;
40
- template: string;
41
- status: SandboxStatus;
42
- created_at: string;
43
- expires_at: string;
44
- };
45
-
46
- export type Sandbox = {
47
- id: string;
48
- name: string;
49
- template: string;
50
- status: SandboxStatus;
51
- region: RegionSummary | string;
52
- specs: SandboxSpecs;
53
- team: string | null;
54
- project_environment: string | null;
55
- auto_destroy: boolean;
56
- destroy_timeout: DestroyTimeout | null;
57
- one_shot: boolean;
58
- block_outbound: boolean;
59
- persistent: boolean;
60
- persistent_disk_gb: number | null;
61
- paused_at: string | null;
62
- from_snapshot: string | null;
63
- snapshot_mode: SnapshotMode;
64
- snapshot_frequency: string | null;
65
- created_at: string;
66
- last_activity_at: string;
67
- expires_at: string;
68
- destroyed_at: string | null;
69
- destroy_reason: DestroyReason | null;
70
- };
71
-
72
- export type AckMessage = {
73
- message: string;
74
- };
75
-
76
- export type WaitUntilReadyOptions = {
77
- timeoutMs?: number;
78
- pollIntervalMs?: number;
79
- signal?: AbortSignal;
80
- };
81
-
82
- export type WaitPreference = boolean | WaitUntilReadyOptions;
83
-
84
- export type SandboxRuntimeOptions = RequestOptions & {
85
- waitUntilReady?: WaitPreference;
86
- };
87
-
88
- export type SandboxReadyRequestOptions = {
89
- request?: RequestOptions;
90
- wait?: WaitUntilReadyOptions;
91
- };
92
-
93
- export type CreateSandboxWithVolumeInput = {
94
- sandbox: Omit<CreateSandboxRequest, 'volumeId' | 'persistent' | 'persistentDiskGB'>;
95
- volume: Omit<CreateVolumeInput, 'region'> & {
96
- region?: string;
97
- };
98
- };
99
-
100
- export type CreateSandboxWithVolumeResult = {
101
- sandbox: CreateSandboxResult;
102
- volume: Volume;
103
- };
@@ -1,17 +0,0 @@
1
- import { SnapshotStatus } from '../enums';
2
-
3
- export type CreateSnapshotInput = {
4
- name: string;
5
- };
6
-
7
- export type Snapshot = {
8
- id: string;
9
- sandbox_id: string;
10
- name: string;
11
- image_tag: string;
12
- source_template: string;
13
- status: SnapshotStatus;
14
- failure_reason: string | null;
15
- size_bytes: number | null;
16
- created_at: string;
17
- };
@@ -1,35 +0,0 @@
1
- export type StatsAverageNumeric = {
2
- totalInPercentage: number;
3
- size: number;
4
- };
5
-
6
- export type StatsAverageNetwork = {
7
- value?: number | null;
8
- total?: number | null;
9
- totalInPercentage?: number | null;
10
- bytesPerSecond?: number | null;
11
- };
12
-
13
- export type StatsTimelinePoint = {
14
- date: string;
15
- memory: number;
16
- cpu: number;
17
- network: {
18
- bytesPerSecond: number | null;
19
- };
20
- };
21
-
22
- export type Stats = {
23
- average: {
24
- memory: StatsAverageNumeric;
25
- cpu: StatsAverageNumeric;
26
- network: StatsAverageNetwork;
27
- };
28
- replicaCount: number;
29
- results: StatsTimelinePoint[];
30
- responseTime: unknown;
31
- };
32
-
33
- export type StatsQuery = {
34
- hoursAgo?: number;
35
- };
@@ -1,5 +0,0 @@
1
- export type SandboxTemplate = {
2
- name: string;
3
- display_name: string;
4
- description: string;
5
- };
@@ -1,26 +0,0 @@
1
- import { VolumeType } from '../enums';
2
- import type { RegionSummary } from './region';
3
-
4
- export type CreateVolumeInput = {
5
- name: string;
6
- sizeGB: number;
7
- region: string;
8
- type?: VolumeType;
9
- teamId?: string;
10
- };
11
-
12
- export type Volume = {
13
- id: string;
14
- name: string;
15
- type: VolumeType;
16
- team: string | null;
17
- csi_volume_id: string | null;
18
- size: number;
19
- region: RegionSummary | null;
20
- mount_path: string | null;
21
- attached_sandbox_id: string | null;
22
- attached_project_id: string | null;
23
- last_attached_at: string | null;
24
- created_at: string | null;
25
- updated_at: string | null;
26
- };
@@ -1,269 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
-
3
- import { SandboxClient } from '../../src/client';
4
- import { MIN_VOLUME_SIZE_GB } from '../../src/constants';
5
- import { NotFoundError } from '../../src/errors';
6
- import { CodeLanguage, VolumeType } from '../../src/enums';
7
- import type { CreateSandboxRequest, ExecStreamFrame, SandboxHandle, SandboxTemplate } from '../../src/types';
8
-
9
- const apiKey = process.env.BRIMBLE_SANDBOX_KEY;
10
- const describeLive = apiKey ? describe : describe.skip;
11
-
12
- async function sleep(ms: number): Promise<void> {
13
- await new Promise((resolve) => {
14
- setTimeout(resolve, ms);
15
- });
16
- }
17
-
18
- async function readStreamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
19
- const reader = stream.getReader();
20
- const decoder = new TextDecoder('utf-8');
21
- let text = '';
22
-
23
- while (true) {
24
- const { value, done } = await reader.read();
25
- if (done) {
26
- break;
27
- }
28
-
29
- if (value) {
30
- text += decoder.decode(value, { stream: true });
31
- }
32
- }
33
-
34
- text += decoder.decode();
35
- return text;
36
- }
37
-
38
- function parseSseFrames(payload: string): ExecStreamFrame[] {
39
- const frames: ExecStreamFrame[] = [];
40
- const blocks = payload.split(/\n\n/);
41
-
42
- for (const block of blocks) {
43
- const line = block
44
- .split('\n')
45
- .map((value) => value.trim())
46
- .find((value) => value.startsWith('data:'));
47
-
48
- if (!line) {
49
- continue;
50
- }
51
-
52
- const json = line.slice('data:'.length).trim();
53
- if (!json) {
54
- continue;
55
- }
56
-
57
- frames.push(JSON.parse(json) as ExecStreamFrame);
58
- }
59
-
60
- return frames;
61
- }
62
-
63
- function chooseTemplate(templates: SandboxTemplate[]): string | undefined {
64
- const preferred = templates.find((template) => template.name === 'node-22');
65
- if (preferred) {
66
- return preferred.name;
67
- }
68
-
69
- return templates[0]?.name;
70
- }
71
-
72
- function isNotFoundError(error: unknown): boolean {
73
- if (error instanceof NotFoundError) {
74
- return true;
75
- }
76
-
77
- return error instanceof Error && error.name === 'NotFoundError';
78
- }
79
-
80
- async function createReadySandboxWithRetries(
81
- client: SandboxClient,
82
- input: CreateSandboxRequest,
83
- attempts = 3,
84
- ): Promise<SandboxHandle> {
85
- let lastError: unknown = null;
86
-
87
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
88
- const sandbox = await client.sandboxes.create(input);
89
-
90
- try {
91
- await sandbox.waitUntilReady({ timeoutMs: 180_000, pollIntervalMs: 2_000 });
92
- return sandbox;
93
- } catch (error) {
94
- lastError = error;
95
-
96
- try {
97
- await sandbox.destroy();
98
- } catch {
99
- // Ignore cleanup failure for transient provisioner attempts.
100
- }
101
-
102
- if (!isNotFoundError(error) || attempt === attempts) {
103
- throw error;
104
- }
105
-
106
- await sleep(2_000);
107
- }
108
- }
109
-
110
- throw lastError instanceof Error ? lastError : new Error('Failed to create a ready sandbox after retries.');
111
- }
112
-
113
- describeLive('Sandbox SDK live integration', () => {
114
- test('covers stable discovery endpoints', { timeout: 120_000 }, async () => {
115
- const client = new SandboxClient({ apiKey, timeoutMs: 60_000 });
116
-
117
- const templates = await client.sandboxes.listTemplates();
118
- expect(Array.isArray(templates)).toBe(true);
119
- expect(templates.length).toBeGreaterThan(0);
120
-
121
- const selectedTemplate = chooseTemplate(templates);
122
- expect(selectedTemplate).toBeTruthy();
123
-
124
- const template = await client.sandboxes.getTemplate(selectedTemplate as string);
125
- expect(template?.name).toBe(selectedTemplate);
126
-
127
- const regionsResult = await client.sandboxes.listRegions();
128
- expect(regionsResult.regions.length).toBeGreaterThan(0);
129
- expect(regionsResult.regions[0]?.id).toBeTruthy();
130
-
131
- const sandboxPage = await client.sandboxes.list({ page: 1, limit: 15 });
132
- expect(Array.isArray(sandboxPage.data)).toBe(true);
133
- expect(sandboxPage.currentPage).toBe(1);
134
- });
135
-
136
- test('covers volume lifecycle without attaching to sandbox', { timeout: 120_000 }, async () => {
137
- const client = new SandboxClient({ apiKey, timeoutMs: 60_000 });
138
-
139
- const regionsResult = await client.sandboxes.listRegions();
140
- const regionId = regionsResult.regions[0]?.id;
141
- expect(regionId).toBeTruthy();
142
-
143
- const volume = await client.volumes.create({
144
- name: `sdk-int-${Date.now()}`,
145
- sizeGB: MIN_VOLUME_SIZE_GB,
146
- region: regionId as string,
147
- type: VolumeType.Sandbox,
148
- });
149
-
150
- try {
151
- expect(volume.type).toBe(VolumeType.Sandbox);
152
-
153
- const fetchedVolume = await client.volumes.get(volume.id);
154
- expect(fetchedVolume.id).toBe(volume.id);
155
-
156
- let volumeSeen = false;
157
- for await (const iteratedVolume of client.volumes.iterate({ limit: 15 })) {
158
- if (iteratedVolume.id === volume.id) {
159
- volumeSeen = true;
160
- break;
161
- }
162
- }
163
- expect(volumeSeen).toBe(true);
164
- } finally {
165
- await client.volumes.delete(volume.id);
166
- }
167
- });
168
-
169
- test('covers sandbox runtime/snapshots lifecycle end-to-end', { timeout: 420_000 }, async () => {
170
- const client = new SandboxClient({ apiKey, timeoutMs: 60_000 });
171
-
172
- const templates = await client.sandboxes.listTemplates();
173
- const selectedTemplate = chooseTemplate(templates);
174
-
175
- let sandbox: SandboxHandle | null = null;
176
-
177
- try {
178
- sandbox = await createReadySandboxWithRetries(
179
- client,
180
- {
181
- ...(selectedTemplate ? { template: selectedTemplate } : {}),
182
- persistent: true,
183
- persistentDiskGB: MIN_VOLUME_SIZE_GB,
184
- },
185
- 6,
186
- );
187
-
188
- const fetchedSandbox = await client.sandboxes.get(sandbox.id);
189
- expect(fetchedSandbox.id).toBe(sandbox.id);
190
-
191
- const readySandbox = await client.sandboxes.getReady(sandbox.id, {
192
- wait: { timeoutMs: 180_000, pollIntervalMs: 2_000 },
193
- });
194
- expect(readySandbox.id).toBe(sandbox.id);
195
-
196
- let sandboxSeen = false;
197
- for await (const iteratedSandbox of client.sandboxes.iterate({ limit: 15 })) {
198
- if (iteratedSandbox.id === sandbox.id) {
199
- sandboxSeen = true;
200
- break;
201
- }
202
- }
203
- expect(sandboxSeen).toBe(true);
204
-
205
- const execResult = await sandbox.exec({ cmd: 'echo brimble-sdk-test' });
206
- expect(execResult.exit_code).toBe(0);
207
- expect(execResult.stdout).toContain('brimble-sdk-test');
208
-
209
- const scopedExec = await client.sandboxes.use(sandbox.id).exec({ cmd: 'printf scoped-runtime' });
210
- expect(scopedExec.exit_code).toBe(0);
211
- expect(scopedExec.stdout).toContain('scoped-runtime');
212
-
213
- const codeResult = await sandbox.runCode({
214
- language: CodeLanguage.Node,
215
- code: 'console.log("run-code-ok")',
216
- });
217
- expect(codeResult.exit_code).toBe(0);
218
- expect(codeResult.stdout).toContain('run-code-ok');
219
-
220
- const sseStream = await sandbox.exec({
221
- cmd: 'for i in 1 2 3; do echo $i; done',
222
- stream: true,
223
- });
224
- const sseText = await readStreamToText(sseStream);
225
- const sseFrames = parseSseFrames(sseText);
226
- const doneFrame = sseFrames.find((frame) => frame.type === 'done');
227
- expect(doneFrame).toBeTruthy();
228
- expect(doneFrame && doneFrame.type === 'done' ? doneFrame.exit_code : 1).toBe(0);
229
-
230
- await sandbox.putFile('tmp/sdk-integration.txt', Buffer.from('file-roundtrip-ok', 'utf-8'));
231
- const fileStream = await sandbox.getFile('tmp/sdk-integration.txt');
232
- const fileContents = await readStreamToText(fileStream);
233
- expect(fileContents).toContain('file-roundtrip-ok');
234
-
235
- const stats = await sandbox.stats({ hoursAgo: 1 });
236
- expect(stats.replicaCount).toBeGreaterThanOrEqual(0);
237
-
238
- const snapshot = await sandbox.createSnapshot({ name: `sdk-snap-${Date.now()}` });
239
- expect(snapshot.id).toBeTruthy();
240
-
241
- const sandboxSnapshots = await sandbox.listSnapshots({ limit: 15, page: 1 });
242
- expect(sandboxSnapshots.data.some((item) => item.id === snapshot.id)).toBe(true);
243
-
244
- const nestedSnapshots = await sandbox.snapshots.list({ limit: 15, page: 1 });
245
- expect(nestedSnapshots.data.some((item) => item.id === snapshot.id)).toBe(true);
246
-
247
- const allSnapshots = await client.snapshots.listAll({ limit: 15, page: 1 });
248
- expect(allSnapshots.data.some((item) => item.id === snapshot.id)).toBe(true);
249
-
250
- try {
251
- await client.snapshots.delete(snapshot.id);
252
- } catch {
253
- // Snapshot deletion can fail when backend image cleanup is still in-flight.
254
- }
255
-
256
- await sandbox.pause();
257
- await sandbox.resume();
258
- await sandbox.waitUntilReady({ timeoutMs: 180_000, pollIntervalMs: 2_000 });
259
- } finally {
260
- if (sandbox) {
261
- try {
262
- await client.sandboxes.destroy(sandbox.id);
263
- } catch {
264
- // Ignore cleanup race failures in integration context.
265
- }
266
- }
267
- }
268
- });
269
- });
@@ -1,87 +0,0 @@
1
- import { afterEach, describe, expect, test, vi } from 'vitest';
2
-
3
- import { SandboxClient } from '../../src/client';
4
- import { SANDBOX_API_KEY_ENV_NAME } from '../../src/constants';
5
-
6
- const ORIGINAL_API_KEY = process.env[SANDBOX_API_KEY_ENV_NAME];
7
-
8
- function restoreApiKeyEnv(): void {
9
- if (ORIGINAL_API_KEY === undefined) {
10
- delete process.env[SANDBOX_API_KEY_ENV_NAME];
11
- return;
12
- }
13
-
14
- process.env[SANDBOX_API_KEY_ENV_NAME] = ORIGINAL_API_KEY;
15
- }
16
-
17
- afterEach(() => {
18
- restoreApiKeyEnv();
19
- });
20
-
21
- describe('SandboxClient auth resolution', () => {
22
- test('throws when api key is missing in both options and env', () => {
23
- delete process.env[SANDBOX_API_KEY_ENV_NAME];
24
-
25
- expect(() => new SandboxClient()).toThrow(
26
- `Sandbox API key is required. Pass "apiKey" explicitly or set ${SANDBOX_API_KEY_ENV_NAME} in your environment.`,
27
- );
28
- });
29
-
30
- test('uses BRIMBLE_SANDBOX_KEY when constructor apiKey is not provided', async () => {
31
- process.env[SANDBOX_API_KEY_ENV_NAME] = 'env-test-key';
32
-
33
- let seenAuthHeader: string | null = null;
34
-
35
- const fetchImpl = vi.fn(async (_url: URL | RequestInfo, init?: RequestInit) => {
36
- const headers = new Headers(init?.headers);
37
- seenAuthHeader = headers.get('x-brimble-key');
38
-
39
- return new Response(
40
- JSON.stringify({
41
- message: 'ok',
42
- data: { regions: [] },
43
- }),
44
- {
45
- status: 200,
46
- headers: {
47
- 'content-type': 'application/json',
48
- },
49
- },
50
- );
51
- }) as typeof fetch;
52
-
53
- const client = new SandboxClient({ fetchImpl });
54
- await client.sandboxes.listRegions();
55
-
56
- expect(seenAuthHeader).toBe('env-test-key');
57
- });
58
-
59
- test('prefers explicit apiKey over BRIMBLE_SANDBOX_KEY', async () => {
60
- process.env[SANDBOX_API_KEY_ENV_NAME] = 'env-test-key';
61
-
62
- let seenAuthHeader: string | null = null;
63
-
64
- const fetchImpl = vi.fn(async (_url: URL | RequestInfo, init?: RequestInit) => {
65
- const headers = new Headers(init?.headers);
66
- seenAuthHeader = headers.get('x-brimble-key');
67
-
68
- return new Response(
69
- JSON.stringify({
70
- message: 'ok',
71
- data: { regions: [] },
72
- }),
73
- {
74
- status: 200,
75
- headers: {
76
- 'content-type': 'application/json',
77
- },
78
- },
79
- );
80
- }) as typeof fetch;
81
-
82
- const client = new SandboxClient({ apiKey: 'explicit-key', fetchImpl });
83
- await client.sandboxes.listRegions();
84
-
85
- expect(seenAuthHeader).toBe('explicit-key');
86
- });
87
- });
@@ -1,69 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
-
3
- import { SandboxClient } from '../../src/client';
4
-
5
- describe('SandboxesResource templates helpers', () => {
6
- test('listTemplates handles wrapped data.templates payload', async () => {
7
- const fetchImpl = vi.fn(async () =>
8
- new Response(
9
- JSON.stringify({
10
- message: 'Templates fetched',
11
- data: {
12
- templates: [
13
- {
14
- name: 'node-22',
15
- display_name: 'Node.js 22',
16
- description: 'Node runtime',
17
- },
18
- ],
19
- },
20
- }),
21
- {
22
- status: 200,
23
- headers: { 'content-type': 'application/json' },
24
- },
25
- ),
26
- ) as typeof fetch;
27
-
28
- const client = new SandboxClient({
29
- apiKey: 'test-key',
30
- fetchImpl,
31
- });
32
-
33
- const templates = await client.sandboxes.listTemplates();
34
- expect(templates.length).toBe(1);
35
- expect(templates[0]?.name).toBe('node-22');
36
- });
37
-
38
- test('getTemplate finds template by name', async () => {
39
- const fetchImpl = vi.fn(async () =>
40
- new Response(
41
- JSON.stringify({
42
- message: 'Templates fetched',
43
- data: {
44
- templates: [
45
- {
46
- name: 'python-3.12',
47
- display_name: 'Python 3.12',
48
- description: 'Python runtime',
49
- },
50
- ],
51
- },
52
- }),
53
- {
54
- status: 200,
55
- headers: { 'content-type': 'application/json' },
56
- },
57
- ),
58
- ) as typeof fetch;
59
-
60
- const client = new SandboxClient({
61
- apiKey: 'test-key',
62
- fetchImpl,
63
- });
64
-
65
- const template = await client.sandboxes.getTemplate('python-3.12');
66
- expect(template?.name).toBe('python-3.12');
67
- });
68
- });
69
-