diff --git a/packages/js-sdk/tests/template/backgroundBuild.test.ts b/packages/js-sdk/tests/template/backgroundBuild.test.ts index 0e798b47cd..f8ad08f2d7 100644 --- a/packages/js-sdk/tests/template/backgroundBuild.test.ts +++ b/packages/js-sdk/tests/template/backgroundBuild.test.ts @@ -1,25 +1,30 @@ import { randomUUID } from 'node:crypto' -import { expect, test } from 'vitest' +import { expect } from 'vitest' import { Template, waitForTimeout } from '../../src' +import { e2eTest } from '../setup' -test('build template in background', async () => { - const template = Template() - .fromImage('ubuntu:22.04') - .skipCache() - .runCmd('sleep 5') // Add a delay to ensure build takes time - .setStartCmd('echo "Hello"', waitForTimeout(10_000)) +e2eTest( + 'build template in background', + async () => { + const template = Template() + .fromImage('ubuntu:22.04') + .skipCache() + .runCmd('sleep 5') // Add a delay to ensure build takes time + .setStartCmd('echo "Hello"', waitForTimeout(10_000)) - const name = `e2b-test:v1-${randomUUID()}` + const name = `e2b-test:v1-${randomUUID()}` - const buildInfo = await Template.buildInBackground(template, name, { - cpuCount: 1, - memoryMB: 1024, - }) + const buildInfo = await Template.buildInBackground(template, name, { + cpuCount: 1, + memoryMB: 1024, + }) - // Should return quickly (within a few seconds), not wait for the full build - expect(buildInfo).toBeDefined() + // Should return quickly (within a few seconds), not wait for the full build + expect(buildInfo).toBeDefined() - // Verify the build is actually running - const status = await Template.getBuildStatus(buildInfo) - expect(status.status).toEqual('building') -}, 10_000) + // Verify the build is actually running + const status = await Template.getBuildStatus(buildInfo) + expect(status.status).toEqual('building') + }, + 10_000 +) diff --git a/packages/js-sdk/tests/template/build.test.ts b/packages/js-sdk/tests/template/build.test.ts index 3e06263cee..0bdd42ead1 100644 --- a/packages/js-sdk/tests/template/build.test.ts +++ b/packages/js-sdk/tests/template/build.test.ts @@ -3,7 +3,7 @@ import os from 'node:os' import path from 'node:path' import { afterAll, beforeAll } from 'vitest' import { defaultBuildLogger, Template, waitForTimeout } from '../../src' -import { buildTemplateTest } from '../setup' +import { e2eBuildTemplateTest } from '../setup' // The file context lives in a temp directory so a test run never writes into // the repository tree. It is created in beforeAll rather than at module load so @@ -35,7 +35,7 @@ afterAll(() => { } }) -buildTemplateTest('build template', async ({ buildTemplate }) => { +e2eBuildTemplateTest('build template', async ({ buildTemplate }) => { const template = Template({ fileContextPath: contextPath }) // using base image to avoid re-building ubuntu:22.04 image .fromBaseImage() @@ -47,7 +47,7 @@ buildTemplateTest('build template', async ({ buildTemplate }) => { await buildTemplate(template, { skipCache: true }, defaultBuildLogger()) }) -buildTemplateTest( +e2eBuildTemplateTest( 'build template from base template', async ({ buildTemplate }) => { const template = Template().fromTemplate('base') @@ -55,17 +55,20 @@ buildTemplateTest( } ) -buildTemplateTest('build template with symlinks', async ({ buildTemplate }) => { - const template = Template({ fileContextPath: contextPath }) - .fromImage('ubuntu:22.04') - .skipCache() - .copy('folder/*', 'folder', { forceUpload: true }) - .runCmd('cat folder/symlink.txt') +e2eBuildTemplateTest( + 'build template with symlinks', + async ({ buildTemplate }) => { + const template = Template({ fileContextPath: contextPath }) + .fromImage('ubuntu:22.04') + .skipCache() + .copy('folder/*', 'folder', { forceUpload: true }) + .runCmd('cat folder/symlink.txt') - await buildTemplate(template) -}) + await buildTemplate(template) + } +) -buildTemplateTest( +e2eBuildTemplateTest( 'build template with resolveSymlinks', async ({ buildTemplate }) => { const template = Template({ fileContextPath: contextPath }) diff --git a/packages/js-sdk/tests/template/exists.test.ts b/packages/js-sdk/tests/template/exists.test.ts index aa62857c72..50112da0a7 100644 --- a/packages/js-sdk/tests/template/exists.test.ts +++ b/packages/js-sdk/tests/template/exists.test.ts @@ -1,13 +1,14 @@ import { randomUUID } from 'node:crypto' -import { expect, test } from 'vitest' +import { expect } from 'vitest' import { Template } from '../../src' +import { e2eTest } from '../setup' -test('check if base template name exists', async () => { +e2eTest('check if base template name exists', async () => { const exists = await Template.exists('base') expect(exists).toBe(true) }) -test('check non existing name', async () => { +e2eTest('check non existing name', async () => { const nonExistingName = `nonexistent-${randomUUID()}` const exists = await Template.exists(nonExistingName) expect(exists).toBe(false) diff --git a/packages/js-sdk/tests/template/methods/makeSymlink.test.ts b/packages/js-sdk/tests/template/methods/makeSymlink.test.ts index 67a1c816a7..27f7ae55a6 100644 --- a/packages/js-sdk/tests/template/methods/makeSymlink.test.ts +++ b/packages/js-sdk/tests/template/methods/makeSymlink.test.ts @@ -1,7 +1,7 @@ import { Template } from '../../../src' -import { buildTemplateTest } from '../../setup' +import { e2eBuildTemplateTest } from '../../setup' -buildTemplateTest('make symlink', async ({ buildTemplate }) => { +e2eBuildTemplateTest('make symlink', async ({ buildTemplate }) => { const template = Template() .fromImage('ubuntu:22.04') .skipCache() @@ -11,7 +11,7 @@ buildTemplateTest('make symlink', async ({ buildTemplate }) => { await buildTemplate(template) }) -buildTemplateTest('make symlink (force)', async ({ buildTemplate }) => { +e2eBuildTemplateTest('make symlink (force)', async ({ buildTemplate }) => { const template = Template() .fromImage('ubuntu:22.04') .makeSymlink('.bashrc', '.bashrc.local') diff --git a/packages/js-sdk/tests/template/methods/runCmd.test.ts b/packages/js-sdk/tests/template/methods/runCmd.test.ts index 340a226a74..6d3e26c1ab 100644 --- a/packages/js-sdk/tests/template/methods/runCmd.test.ts +++ b/packages/js-sdk/tests/template/methods/runCmd.test.ts @@ -1,8 +1,8 @@ import { expect } from 'vitest' import { Template } from '../../../src' -import { buildTemplateTest } from '../../setup' +import { e2eBuildTemplateTest } from '../../setup' -buildTemplateTest('run command', async ({ buildTemplate }) => { +e2eBuildTemplateTest('run command', async ({ buildTemplate }) => { const template = Template() .fromImage('ubuntu:22.04') .skipCache() @@ -11,7 +11,7 @@ buildTemplateTest('run command', async ({ buildTemplate }) => { await buildTemplate(template) }) -buildTemplateTest( +e2eBuildTemplateTest( 'run command as a different user', async ({ buildTemplate }) => { const template = Template() @@ -23,7 +23,7 @@ buildTemplateTest( } ) -buildTemplateTest( +e2eBuildTemplateTest( 'run command as user that does not exist', async ({ buildTemplate }) => { const template = Template() diff --git a/packages/js-sdk/tests/template/serialization.test.ts b/packages/js-sdk/tests/template/serialization.test.ts new file mode 100644 index 0000000000..f53e232654 --- /dev/null +++ b/packages/js-sdk/tests/template/serialization.test.ts @@ -0,0 +1,112 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, assert, beforeAll, expect, test } from 'vitest' + +import { Template } from '../../src' +import { InstructionType } from '../../src/template/types' +import { calculateFilesHash } from '../../src/template/utils' + +let contextPath: string + +beforeAll(async () => { + contextPath = await mkdtemp(join(tmpdir(), 'template-serialization-')) + await writeFile(join(contextPath, 'app.txt'), 'hello') + await writeFile(join(contextPath, 'other.txt'), 'hello') +}) + +afterAll(async () => { + await rm(contextPath, { recursive: true, force: true }) +}) + +const filesHash = (src: string, dest: string) => + calculateFilesHash(src, dest, contextPath, [], false, undefined) + +test('hash is stable and content-dependent', async () => { + const before = await filesHash('app.txt', '/app/') + assert.equal(await filesHash('app.txt', '/app/'), before) + + await writeFile(join(contextPath, 'app.txt'), 'hello again') + const after = await filesHash('app.txt', '/app/') + + assert.notEqual(after, before) + assert.match(after, /^[0-9a-f]{64}$/) +}) + +test('hash covers the source and destination paths', async () => { + // Identical content, different instruction — the hash seeds on `COPY src dest`. + assert.notEqual( + await filesHash('app.txt', '/app/'), + await filesHash('other.txt', '/app/') + ) + assert.notEqual( + await filesHash('app.txt', '/app/'), + await filesHash('app.txt', '/srv/') + ) +}) + +test('hashing a source that matches no file fails', async () => { + // TODO: should reject with TemplateError once calculateFilesHash stops + // throwing a bare Error. + await expect(filesHash('nope.txt', '/app/')).rejects.toThrow() +}) + +test('serializes a build payload from the builder', async () => { + const template = Template({ fileContextPath: contextPath }) + .fromImage('ubuntu:22.04') + .runCmd('echo hello') + .setWorkdir('/app') + .setStartCmd('python main.py', 'curl -f http://localhost:8000') + + const payload = JSON.parse(await Template.toJSON(template, false)) + + assert.equal(payload.fromImage, 'ubuntu:22.04') + assert.equal(payload.startCmd, 'python main.py') + assert.equal(payload.readyCmd, 'curl -f http://localhost:8000') + assert.isUndefined(payload.fromTemplate) + assert.deepEqual( + payload.steps.map((step: { type: string }) => step.type), + [InstructionType.RUN, InstructionType.WORKDIR] + ) +}) + +test('serializes fromTemplate instead of fromImage', async () => { + const payload = JSON.parse( + await Template.toJSON(Template().fromTemplate('base')) + ) + + assert.equal(payload.fromTemplate, 'base') + assert.isUndefined(payload.fromImage) +}) + +test('serializes a registry config next to the image', async () => { + const template = Template().fromImage('registry.example.com/app:latest', { + username: 'user', + password: 'pass', + }) + + const payload = JSON.parse(await Template.toJSON(template)) + + assert.equal(payload.fromImage, 'registry.example.com/app:latest') + assert.equal(payload.fromImageRegistry.type, 'registry') + assert.equal(payload.fromImageRegistry.username, 'user') +}) + +test('computeHashes adds the copy hash to the payload', async () => { + const template = Template({ fileContextPath: contextPath }) + .fromImage('ubuntu:22.04') + .copy('app.txt', '/app/') + + const withoutHashes = JSON.parse(await Template.toJSON(template, false)) + const withHashes = JSON.parse(await Template.toJSON(template, true)) + + const copyStep = (payload: { + steps: { type: string; filesHash?: string }[] + }) => payload.steps.find((step) => step.type === InstructionType.COPY) + + assert.isUndefined(copyStep(withoutHashes)?.filesHash) + assert.equal( + copyStep(withHashes)?.filesHash, + await filesHash('app.txt', '/app/') + ) +}) diff --git a/packages/js-sdk/tests/template/tags.test.ts b/packages/js-sdk/tests/template/tags.test.ts index 21e2166709..bab1da320b 100644 --- a/packages/js-sdk/tests/template/tags.test.ts +++ b/packages/js-sdk/tests/template/tags.test.ts @@ -1,11 +1,10 @@ -import { randomUUID } from 'node:crypto' import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { Template } from '../../src' -import { apiUrl, buildTemplateTest, isDebug } from '../setup' +import { apiUrl } from '../setup' // Mock handlers for tag API endpoints const mockHandlers = [ @@ -121,82 +120,3 @@ describe('Template tags unit tests', () => { }) }) }) - -// Integration tests -buildTemplateTest.skipIf(isDebug)( - 'build template with tags, assign and delete', - { timeout: 300_000 }, - async ({ buildTemplate }) => { - const templateName = 'e2b-tags-test' - const initialTag = `${templateName}:v1-${randomUUID()}` - - // Build a template with initial tag - const template = Template().fromBaseImage() - const buildInfo = await buildTemplate(template, { name: initialTag }) - - expect(buildInfo.buildId).toBeTruthy() - expect(buildInfo.templateId).toBeTruthy() - - // Assign additional tags (just tag names, not full alias:tag format) - const tagInfo = await Template.assignTags(initialTag, [ - 'production', - 'latest', - ]) - - expect(tagInfo.buildId).toBeTruthy() - expect(tagInfo.tags).toContain('production') - expect(tagInfo.tags).toContain('latest') - } -) - -buildTemplateTest.skipIf(isDebug)( - 'assign single tag to existing template', - { timeout: 300_000 }, - async ({ buildTemplate }) => { - const templateName = 'e2b-tags-test' - const initialTag = `${templateName}:v1-${randomUUID()}` - - const template = Template().fromBaseImage() - await buildTemplate(template, { name: initialTag }) - - // Assign single tag (just tag name, not full alias:tag format) - const tagInfo = await Template.assignTags(initialTag, 'stable') - - expect(tagInfo.buildId).toBeTruthy() - expect(tagInfo.tags).toContain('stable') - } -) - -buildTemplateTest.skipIf(isDebug)( - 'rejects invalid tag format - missing alias', - { timeout: 300_000 }, - async ({ buildTemplate }) => { - const templateName = 'e2b-tags-test' - const initialTag = `${templateName}:v1-${randomUUID()}` - - const template = Template().fromBaseImage() - await buildTemplate(template, { name: initialTag }) - - // Tag without alias (starts with colon) should be rejected - await expect( - Template.assignTags(initialTag, ':invalid-tag') - ).rejects.toThrow() - } -) - -buildTemplateTest.skipIf(isDebug)( - 'rejects invalid tag format - missing tag', - { timeout: 300_000 }, - async ({ buildTemplate }) => { - const templateName = 'e2b-tags-test' - const initialTag = `${templateName}:v1-${randomUUID()}` - - const template = Template().fromBaseImage() - await buildTemplate(template, { name: initialTag }) - - // Tag without tag portion (ends with colon) should be rejected - await expect( - Template.assignTags(initialTag, `${templateName}:`) - ).rejects.toThrow() - } -) diff --git a/packages/js-sdk/tests/template/tagsBuild.test.ts b/packages/js-sdk/tests/template/tagsBuild.test.ts new file mode 100644 index 0000000000..c2a79a2b34 --- /dev/null +++ b/packages/js-sdk/tests/template/tagsBuild.test.ts @@ -0,0 +1,83 @@ +import { randomUUID } from 'node:crypto' +import { expect } from 'vitest' + +import { Template } from '../../src' +import { e2eBuildTemplateTest } from '../setup' + +e2eBuildTemplateTest( + 'build template with tags, assign and delete', + { timeout: 300_000 }, + async ({ buildTemplate }) => { + const templateName = 'e2b-tags-test' + const initialTag = `${templateName}:v1-${randomUUID()}` + + // Build a template with initial tag + const template = Template().fromBaseImage() + const buildInfo = await buildTemplate(template, { name: initialTag }) + + expect(buildInfo.buildId).toBeTruthy() + expect(buildInfo.templateId).toBeTruthy() + + // Assign additional tags (just tag names, not full alias:tag format) + const tagInfo = await Template.assignTags(initialTag, [ + 'production', + 'latest', + ]) + + expect(tagInfo.buildId).toBeTruthy() + expect(tagInfo.tags).toContain('production') + expect(tagInfo.tags).toContain('latest') + } +) + +e2eBuildTemplateTest( + 'assign single tag to existing template', + { timeout: 300_000 }, + async ({ buildTemplate }) => { + const templateName = 'e2b-tags-test' + const initialTag = `${templateName}:v1-${randomUUID()}` + + const template = Template().fromBaseImage() + await buildTemplate(template, { name: initialTag }) + + // Assign single tag (just tag name, not full alias:tag format) + const tagInfo = await Template.assignTags(initialTag, 'stable') + + expect(tagInfo.buildId).toBeTruthy() + expect(tagInfo.tags).toContain('stable') + } +) + +e2eBuildTemplateTest( + 'rejects invalid tag format - missing alias', + { timeout: 300_000 }, + async ({ buildTemplate }) => { + const templateName = 'e2b-tags-test' + const initialTag = `${templateName}:v1-${randomUUID()}` + + const template = Template().fromBaseImage() + await buildTemplate(template, { name: initialTag }) + + // Tag without alias (starts with colon) should be rejected + await expect( + Template.assignTags(initialTag, ':invalid-tag') + ).rejects.toThrow() + } +) + +e2eBuildTemplateTest( + 'rejects invalid tag format - missing tag', + { timeout: 300_000 }, + async ({ buildTemplate }) => { + const templateName = 'e2b-tags-test' + const initialTag = `${templateName}:v1-${randomUUID()}` + + const template = Template().fromBaseImage() + await buildTemplate(template, { name: initialTag }) + + // Tag without tag portion (ends with colon) should be rejected + await expect( + Template.assignTags(initialTag, `${templateName}:`) + ).rejects.toThrow() + } +) diff --git a/packages/python-sdk/tests/async/template_async/test_background_build.py b/packages/python-sdk/tests/async/template_async/test_background_build.py index 16690c7232..e7f130bdfe 100644 --- a/packages/python-sdk/tests/async/template_async/test_background_build.py +++ b/packages/python-sdk/tests/async/template_async/test_background_build.py @@ -5,6 +5,7 @@ from e2b import AsyncTemplate, wait_for_timeout +@pytest.mark.e2e @pytest.mark.skip_debug() @pytest.mark.timeout(10) async def test_build_in_background_should_start_build_and_return_info(): diff --git a/packages/python-sdk/tests/async/template_async/test_exists.py b/packages/python-sdk/tests/async/template_async/test_exists.py index 6da5609470..88338d5473 100644 --- a/packages/python-sdk/tests/async/template_async/test_exists.py +++ b/packages/python-sdk/tests/async/template_async/test_exists.py @@ -5,6 +5,7 @@ from e2b import AsyncTemplate +@pytest.mark.e2e @pytest.mark.skip_debug() async def test_check_base_template_name_exists(): """Test that the base template name exists.""" @@ -12,6 +13,7 @@ async def test_check_base_template_name_exists(): assert exists is True +@pytest.mark.e2e @pytest.mark.skip_debug() async def test_check_non_existing_name(): """Test that a non-existing name returns False.""" diff --git a/packages/python-sdk/tests/async/template_async/test_stacktrace.py b/packages/python-sdk/tests/async/template_async/test_stacktrace.py index 63c3e5e7ac..95812b3069 100644 --- a/packages/python-sdk/tests/async/template_async/test_stacktrace.py +++ b/packages/python-sdk/tests/async/template_async/test_stacktrace.py @@ -48,6 +48,11 @@ } +# Every build API call is mocked below, so these stay in the default unit tier +# despite requesting the `build` fixture. +pytestmark = pytest.mark.mocked + + @pytest.fixture(autouse=True) def mock_template_build(monkeypatch): async def mock_request_build( diff --git a/packages/python-sdk/tests/sync/template_sync/test_background_build.py b/packages/python-sdk/tests/sync/template_sync/test_background_build.py index f5d41db4e4..30f3b3faf8 100644 --- a/packages/python-sdk/tests/sync/template_sync/test_background_build.py +++ b/packages/python-sdk/tests/sync/template_sync/test_background_build.py @@ -5,6 +5,7 @@ from e2b import Template, wait_for_timeout +@pytest.mark.e2e @pytest.mark.skip_debug() @pytest.mark.timeout(10) def test_build_in_background_should_start_build_and_return_info(): diff --git a/packages/python-sdk/tests/sync/template_sync/test_exists.py b/packages/python-sdk/tests/sync/template_sync/test_exists.py index 641b58ba08..fd0123fa19 100644 --- a/packages/python-sdk/tests/sync/template_sync/test_exists.py +++ b/packages/python-sdk/tests/sync/template_sync/test_exists.py @@ -5,6 +5,7 @@ from e2b import Template +@pytest.mark.e2e @pytest.mark.skip_debug() def test_check_base_template_name_exists(): """Test that the base template name exists.""" @@ -12,6 +13,7 @@ def test_check_base_template_name_exists(): assert exists is True +@pytest.mark.e2e @pytest.mark.skip_debug() def test_check_non_existing_name(): """Test that a non-existing name returns False.""" diff --git a/packages/python-sdk/tests/sync/template_sync/test_serialization.py b/packages/python-sdk/tests/sync/template_sync/test_serialization.py new file mode 100644 index 0000000000..c0a7336ea3 --- /dev/null +++ b/packages/python-sdk/tests/sync/template_sync/test_serialization.py @@ -0,0 +1,112 @@ +"""Template payload serialization and copy-file hashing — pure client logic. + +Mirrors `tests/template/serialization.test.ts` in the JS SDK: no build is +started, only the JSON the SDK would send and the hash it derives from the +local file context. +""" + +import json +from pathlib import Path + +import pytest + +from e2b import Template +from e2b.template.types import InstructionType +from e2b.template.utils import calculate_files_hash + + +@pytest.fixture() +def context_path(tmp_path: Path) -> Path: + (tmp_path / "app.txt").write_text("hello") + (tmp_path / "other.txt").write_text("hello") + return tmp_path + + +def _files_hash(context_path: Path, src: str, dest: str) -> str: + return calculate_files_hash(src, dest, str(context_path), [], False, None) + + +def test_hash_is_stable_and_content_dependent(context_path: Path): + before = _files_hash(context_path, "app.txt", "/app/") + assert _files_hash(context_path, "app.txt", "/app/") == before + + (context_path / "app.txt").write_text("hello again") + after = _files_hash(context_path, "app.txt", "/app/") + + assert after != before + assert len(after) == 64 + assert set(after) <= set("0123456789abcdef") + + +def test_hash_covers_the_source_and_destination_paths(context_path: Path): + # Identical content, different instruction — the hash seeds on `COPY src dest`. + assert _files_hash(context_path, "app.txt", "/app/") != _files_hash( + context_path, "other.txt", "/app/" + ) + assert _files_hash(context_path, "app.txt", "/app/") != _files_hash( + context_path, "app.txt", "/srv/" + ) + + +def test_hashing_a_source_that_matches_no_file_fails(context_path: Path): + # TODO: should raise TemplateException once calculate_files_hash stops + # raising a bare ValueError. + with pytest.raises(ValueError): + _files_hash(context_path, "nope.txt", "/app/") + + +def test_serializes_a_build_payload_from_the_builder(context_path: Path): + template = ( + Template(file_context_path=context_path) + .from_image("ubuntu:22.04") + .run_cmd("echo hello") + .set_workdir("/app") + .set_start_cmd("python main.py", "curl -f http://localhost:8000") + ) + + payload = json.loads(Template.to_json(template)) + + assert payload["fromImage"] == "ubuntu:22.04" + assert payload["startCmd"] == "python main.py" + assert payload["readyCmd"] == "curl -f http://localhost:8000" + assert payload.get("fromTemplate") is None + assert [step["type"] for step in payload["steps"]] == [ + InstructionType.RUN, + InstructionType.WORKDIR, + ] + + +def test_serializes_from_template_instead_of_from_image(): + payload = json.loads(Template.to_json(Template().from_template("base"))) + + assert payload["fromTemplate"] == "base" + assert payload.get("fromImage") is None + + +def test_serializes_a_registry_config_next_to_the_image(): + template = Template().from_image( + "registry.example.com/app:latest", + username="user", + password="pass", + ) + + payload = json.loads(Template.to_json(template)) + + assert payload["fromImage"] == "registry.example.com/app:latest" + assert payload["fromImageRegistry"]["type"] == "registry" + assert payload["fromImageRegistry"]["username"] == "user" + + +def test_copy_step_carries_the_files_hash(context_path: Path): + template = ( + Template(file_context_path=context_path) + .from_image("ubuntu:22.04") + .copy("app.txt", "/app/") + ) + + payload = json.loads(Template.to_json(template)) + copy_step = next( + step for step in payload["steps"] if step["type"] == InstructionType.COPY + ) + + assert copy_step["filesHash"] == _files_hash(context_path, "app.txt", "/app/") diff --git a/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py b/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py index f416c84ee4..b4dd35db77 100644 --- a/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py +++ b/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py @@ -48,6 +48,11 @@ } +# Every build API call is mocked below, so these stay in the default unit tier +# despite requesting the `build` fixture. +pytestmark = pytest.mark.mocked + + @pytest.fixture(autouse=True) def mock_template_build(monkeypatch): def mock_request_build(