From fc4bebedf286dae478229a2764b21ecaf82191d9 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:46:47 +0200 Subject: [PATCH] test: cover the implicit file context, isolate temp fixtures per test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #1689, which moved test fixtures out of the repository tree. Two of that PR's suites kept a beforeEach that deleted and recreated the mkdtemp'd directory at a fixed path, which discarded mkdtemp's 0700 mode and its uniqueness guarantee — the only thing it still provided was the location. They now mkdtemp per test and clean up in afterEach, matching the tempfile.TemporaryDirectory fixture the Python mirrors already use. That PR also switched build.test.ts to an explicit fileContextPath, which left nothing exercising the implicit default (the directory of the file that calls Template()) together with a copy: getCallerDirectory.test.ts covers the helper in isolation, and the one remaining bare Template() never copies. The default has broken before under a change in TypeScript emit, so this adds a test for it in both SDKs, built on a committed read-only fixture — the implicit context is inside the repository by definition, so the fixture cannot be generated into a temp directory. The assertion compares the implicit serialization against an explicit one, with a second test proving a context that lacks the fixture raises, so the first cannot pass vacuously. Both were mutation-checked: stubbing the caller-directory lookup out of each SDK fails the new test. Co-Authored-By: Claude Opus 5 (1M context) --- .../js-sdk/tests/template/fileContext.test.ts | 41 +++++++++++++++++ .../js-sdk/tests/template/fixtures/hello.txt | 3 ++ .../template/utils/getAllFilesInPath.test.ts | 14 +++--- .../template/utils/spoolTarArchive.test.ts | 14 +++--- .../tests/shared/template/fixtures/hello.txt | 3 ++ .../shared/template/test_file_context.py | 44 +++++++++++++++++++ 6 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 packages/js-sdk/tests/template/fileContext.test.ts create mode 100644 packages/js-sdk/tests/template/fixtures/hello.txt create mode 100644 packages/python-sdk/tests/shared/template/fixtures/hello.txt create mode 100644 packages/python-sdk/tests/shared/template/test_file_context.py diff --git a/packages/js-sdk/tests/template/fileContext.test.ts b/packages/js-sdk/tests/template/fileContext.test.ts new file mode 100644 index 0000000000..620cb30102 --- /dev/null +++ b/packages/js-sdk/tests/template/fileContext.test.ts @@ -0,0 +1,41 @@ +import os from 'node:os' +import { describe, expect, test } from 'vitest' +import { Template } from '../../src' + +// Path to a committed, read-only fixture, relative to this file's directory. +// The implicit file context is the directory of the file that calls Template(), +// so it is always inside the repository — which is why the fixture is committed +// and never written to, rather than generated into a temp directory. +const fixturePath = 'fixtures/hello.txt' + +describe('file context', () => { + test('defaults to the directory of the caller of Template()', async () => { + const implicit = Template().fromBaseImage().copy(fixturePath, 'hello.txt') + const explicit = Template({ fileContextPath: __dirname }) + .fromBaseImage() + .copy(fixturePath, 'hello.txt') + + // toJSON hashes each COPY's files, so the two serializations only match if + // the implicit context resolved to this file's directory, the glob found + // the fixture there, and its contents were read. This is the only test that + // exercises the implicit default end to end — every other template test + // passes fileContextPath explicitly, and the unit test for + // getCallerDirectory covers the helper in isolation, not its use here. + expect(await Template.toJSON(implicit)).toBe( + await Template.toJSON(explicit) + ) + }) + + test('fails to resolve a source that is not in the context', async () => { + // Keeps the assertion above from passing vacuously: the hashes match + // because the fixture was found, not because a missing file hashes the + // same either way. + const wrongContext = Template({ fileContextPath: os.tmpdir() }) + .fromBaseImage() + .copy(fixturePath, 'hello.txt') + + await expect(Template.toJSON(wrongContext)).rejects.toThrow( + /No files found/ + ) + }) +}) diff --git a/packages/js-sdk/tests/template/fixtures/hello.txt b/packages/js-sdk/tests/template/fixtures/hello.txt new file mode 100644 index 0000000000..2caac9087c --- /dev/null +++ b/packages/js-sdk/tests/template/fixtures/hello.txt @@ -0,0 +1,3 @@ +This file is the fixture for the implicit file-context test. It is read-only: +the implicit context is the directory of the file that calls Template(), which +is inside the repository, so nothing may be written here at test time. diff --git a/packages/js-sdk/tests/template/utils/getAllFilesInPath.test.ts b/packages/js-sdk/tests/template/utils/getAllFilesInPath.test.ts index 30d888fdef..836e1da837 100644 --- a/packages/js-sdk/tests/template/utils/getAllFilesInPath.test.ts +++ b/packages/js-sdk/tests/template/utils/getAllFilesInPath.test.ts @@ -1,24 +1,20 @@ -import { expect, test, describe, beforeAll, afterAll, beforeEach } from 'vitest' +import { expect, test, describe, afterEach, beforeEach } from 'vitest' import { writeFile, mkdir, mkdtemp, rm } from 'fs/promises' import { tmpdir } from 'os' import { join, basename } from 'path' import { getAllFilesInPath } from '../../../src/template/utils' describe('getAllFilesInPath', () => { - // A temp directory, so a test run never writes into the repository tree. + // A fresh temp directory per test, so a run never writes into the repository + // tree and no fixture leaks from one test into the next. let testDir: string - beforeAll(async () => { + beforeEach(async () => { testDir = await mkdtemp(join(tmpdir(), 'getAllFilesInPath-test-')) }) - afterAll(async () => { - await rm(testDir, { recursive: true, force: true }) - }) - - beforeEach(async () => { + afterEach(async () => { await rm(testDir, { recursive: true, force: true }) - await mkdir(testDir, { recursive: true }) }) test('should return files matching a simple pattern', async () => { diff --git a/packages/js-sdk/tests/template/utils/spoolTarArchive.test.ts b/packages/js-sdk/tests/template/utils/spoolTarArchive.test.ts index 9ad88e7e2f..76da5cbac7 100644 --- a/packages/js-sdk/tests/template/utils/spoolTarArchive.test.ts +++ b/packages/js-sdk/tests/template/utils/spoolTarArchive.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe, beforeAll, afterAll, beforeEach } from 'vitest' +import { expect, test, describe, afterEach, beforeEach } from 'vitest' import { writeFile, mkdir, @@ -15,20 +15,16 @@ import * as tar from 'tar' import { ReadEntry } from 'tar' describe('spoolTarArchive', () => { - // A temp directory, so a test run never writes into the repository tree. + // A fresh temp directory per test, so a run never writes into the repository + // tree and no fixture leaks from one test into the next. let testDir: string - beforeAll(async () => { + beforeEach(async () => { testDir = await mkdtemp(join(tmpdir(), 'spoolTarArchive-test-')) }) - afterAll(async () => { - await rm(testDir, { recursive: true, force: true }) - }) - - beforeEach(async () => { + afterEach(async () => { await rm(testDir, { recursive: true, force: true }) - await mkdir(testDir, { recursive: true }) }) /** diff --git a/packages/python-sdk/tests/shared/template/fixtures/hello.txt b/packages/python-sdk/tests/shared/template/fixtures/hello.txt new file mode 100644 index 0000000000..2caac9087c --- /dev/null +++ b/packages/python-sdk/tests/shared/template/fixtures/hello.txt @@ -0,0 +1,3 @@ +This file is the fixture for the implicit file-context test. It is read-only: +the implicit context is the directory of the file that calls Template(), which +is inside the repository, so nothing may be written here at test time. diff --git a/packages/python-sdk/tests/shared/template/test_file_context.py b/packages/python-sdk/tests/shared/template/test_file_context.py new file mode 100644 index 0000000000..4f68c3ab1d --- /dev/null +++ b/packages/python-sdk/tests/shared/template/test_file_context.py @@ -0,0 +1,44 @@ +import os +import tempfile + +import pytest + +from e2b import Template + +# Path to a committed, read-only fixture, relative to this file's directory. +# The implicit file context is the directory of the file that calls Template(), +# so it is always inside the repository — which is why the fixture is committed +# and never written to, rather than generated into a temp directory. +FIXTURE_PATH = "fixtures/hello.txt" + + +def test_file_context_defaults_to_caller_directory(): + implicit = Template().from_base_image().copy(FIXTURE_PATH, "hello.txt") + explicit = ( + Template(file_context_path=os.path.dirname(__file__)) + .from_base_image() + .copy(FIXTURE_PATH, "hello.txt") + ) + + # to_json hashes each COPY's files, so the two serializations only match if + # the implicit context resolved to this file's directory, the glob found the + # fixture there, and its contents were read. This is the only test that + # exercises the implicit default end to end — every other template test + # passes file_context_path explicitly, and the unit test for + # get_caller_directory covers the helper in isolation, not its use here. + assert Template.to_json(implicit) == Template.to_json(explicit) + + +def test_file_context_without_the_source_fails_to_resolve_it(): + # Keeps the assertion above from passing vacuously: the hashes match because + # the fixture was found, not because a missing file hashes the same either + # way. + with tempfile.TemporaryDirectory() as empty_context: + wrong_context = ( + Template(file_context_path=empty_context) + .from_base_image() + .copy(FIXTURE_PATH, "hello.txt") + ) + + with pytest.raises(ValueError, match="No files found"): + Template.to_json(wrong_context)