Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions packages/js-sdk/tests/api/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,6 @@ import { assert } from 'vitest'
import { sandboxTest, isDebug } from '../setup.js'
import { Sandbox } from '../../src'

sandboxTest.skipIf(isDebug)(
'pause sandbox',
async ({ sandbox }) => {
await Sandbox.pause(sandbox.sandboxId)
assert.isFalse(
await sandbox.isRunning(),
'Sandbox should not be running after pause'
)
},
120_000
)

sandboxTest.skipIf(isDebug)(
'resume sandbox',
async ({ sandbox }) => {
Expand Down
10 changes: 1 addition & 9 deletions packages/js-sdk/tests/paginator.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert, expect, test } from 'vitest'
import { assert, test } from 'vitest'

import { Paginator } from '../src/paginator'

Expand Down Expand Up @@ -52,11 +52,3 @@ test('paginator exposes pagination state and advances across pages', async () =>
assert.isFalse(paginator.hasNext)
assert.isUndefined(paginator.nextToken)
})

test('paginator throws once exhausted', async () => {
const paginator = new FakePaginator([{ items: [], nextToken: undefined }])
await paginator.nextItems()

assert.isFalse(paginator.hasNext)
await expect(paginator.nextItems()).rejects.toThrow('No more items to fetch')
})
3 changes: 1 addition & 2 deletions packages/js-sdk/tests/sandbox/commands/connect.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert, expect, expectTypeOf } from 'vitest'
import { assert, expect } from 'vitest'
import { sandboxTest } from '../../setup.js'

sandboxTest('connect to process', async ({ sandbox }) => {
Expand Down Expand Up @@ -30,7 +30,6 @@ sandboxTest('connect without retaining output', async ({ sandbox }) => {
},
})

expectTypeOf(handle.stdout).toEqualTypeOf<undefined>()
const result = await handle.wait()

assert.isUndefined(result.stdout)
Expand Down
41 changes: 41 additions & 0 deletions packages/js-sdk/tests/sandbox/commands/retainOutput.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expectTypeOf, test } from 'vitest'

import type { CommandStartOpts, Sandbox } from '../../../src/index.js'

declare const sandbox: Sandbox
declare const opts: CommandStartOpts

test('retained output types stdout as a string', async () => {
expectTypeOf(
(await sandbox.commands.run('echo')).stdout
).toEqualTypeOf<string>()
expectTypeOf(
(await sandbox.commands.run('echo', {})).stdout
).toEqualTypeOf<string>()
expectTypeOf(
(await sandbox.commands.run('echo', { retainOutput: true })).stdout
).toEqualTypeOf<string>()
})

test('retainOutput: false types stdout as undefined', async () => {
expectTypeOf(
(await sandbox.commands.run('echo', { retainOutput: false })).stdout
).toEqualTypeOf<undefined>()
expectTypeOf(
(
await sandbox.commands.run('echo', {
background: true,
retainOutput: false,
})
).stdout
).toEqualTypeOf<undefined>()
expectTypeOf(
(await sandbox.commands.connect(1, { retainOutput: false })).stdout
).toEqualTypeOf<undefined>()
})

test('non-literal retainOutput types stdout as possibly undefined', async () => {
expectTypeOf((await sandbox.commands.run('echo', opts)).stdout).toEqualTypeOf<
string | undefined
>()
})
39 changes: 2 additions & 37 deletions packages/js-sdk/tests/sandbox/commands/run.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { expect, assert, expectTypeOf } from 'vitest'
import { expect, assert } from 'vitest'

import { sandboxTest } from '../../setup.js'
import {
CommandStartOpts,
InvalidArgumentError,
TimeoutError,
} from '../../../src/index.js'
import { InvalidArgumentError, TimeoutError } from '../../../src/index.js'

sandboxTest('run', async ({ sandbox }) => {
const text = 'Hello, World!'
Expand Down Expand Up @@ -71,7 +67,6 @@ sandboxTest('run without retaining output', async ({ sandbox }) => {
},
})

expectTypeOf(cmd.stdout).toEqualTypeOf<undefined>()
assert.equal(cmd.exitCode, 0)
assert.isUndefined(cmd.stdout)
assert.isUndefined(cmd.stderr)
Expand All @@ -87,7 +82,6 @@ sandboxTest(
retainOutput: false,
})

expectTypeOf(handle.stdout).toEqualTypeOf<undefined>()
const result = await handle.wait()

assert.equal(result.exitCode, 0)
Expand All @@ -97,27 +91,6 @@ sandboxTest(
}
)

sandboxTest('run retains output by default', async ({ sandbox }) => {
const cmd = await sandbox.commands.run('echo out', { retainOutput: true })

expectTypeOf(cmd.stdout).toEqualTypeOf<string>()
assert.equal(cmd.stdout, 'out\n')
})

sandboxTest(
'run types non-literal retainOutput as possibly undefined',
async ({ sandbox }) => {
const opts: CommandStartOpts = { retainOutput: false }
const cmd = await sandbox.commands.run('echo out', opts)

expectTypeOf(cmd.stdout).toEqualTypeOf<string | undefined>()
assert.isUndefined(cmd.stdout)

const defaults = await sandbox.commands.run('echo out', {})
expectTypeOf(defaults.stdout).toEqualTypeOf<string>()
}
)

sandboxTest('run with custom shell', async ({ sandbox }) => {
const cmd = await sandbox.commands.run('print(6 * 7)', {
shell: ['python3', '-c'],
Expand Down Expand Up @@ -147,11 +120,3 @@ sandboxTest('run with string shell', async ({ sandbox }) => {
})
).rejects.toThrowError(InvalidArgumentError)
})

sandboxTest('run with non-array shell', async ({ sandbox }) => {
await expect(
sandbox.commands.run('echo hello', {
shell: new Set(['/bin/sh', '-c']) as unknown as string[],
})
).rejects.toThrowError(InvalidArgumentError)
})
21 changes: 0 additions & 21 deletions packages/js-sdk/tests/sandbox/commands/sendStdin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,6 @@ sandboxTest('send Uint8Array stdin to process', async ({ sandbox }) => {
assert.equal(cmd.stdout, text)
})

sandboxTest('send stdin via command handle', async ({ sandbox }) => {
const text = 'Hello, World!'
const cmd = await sandbox.commands.run('cat', {
background: true,
stdin: true,
})

await cmd.sendStdin(text)

for (let i = 0; i < 5; i++) {
if (cmd.stdout === text) {
break
}
await new Promise((r) => setTimeout(r, 500))
}

await cmd.kill()

assert.equal(cmd.stdout, text)
})

sandboxTest('close stdin via command handle', async ({ sandbox }) => {
const text = 'Hello, World!'
const cmd = await sandbox.commands.run('cat', {
Expand Down
42 changes: 0 additions & 42 deletions packages/js-sdk/tests/sandbox/snapshot-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,48 +205,6 @@ sandboxTest.skipIf(isDebug)('delete snapshot', async ({ sandbox }) => {
assert.isFalse(deletedAgain)
})

sandboxTest.skipIf(isDebug)(
'snapshot preserves file system state',
async ({ sandbox, sandboxTestId }) => {
const appDir = '/home/user/app'
const configPath = `${appDir}/config.json`
const configContent = '{"env": "test"}'
const dataPath = `${appDir}/data.txt`
const dataContent = 'important data'

await sandbox.files.makeDir(appDir)
await sandbox.files.write(configPath, configContent)
await sandbox.files.write(dataPath, dataContent)

const snapshot = await sandbox.createSnapshot()

try {
const newSandbox = await Sandbox.create(snapshot.snapshotId, {
metadata: { sandboxTestId: `${sandboxTestId}-fs-test` },
requestTimeoutMs: SNAPSHOT_CREATE_REQUEST_TIMEOUT_MS,
})

try {
// Verify directory exists
const dirExists = await newSandbox.files.exists(appDir)
assert.isTrue(dirExists)

// Verify files exist with correct content
const config = await newSandbox.files.read(configPath)
const data = await newSandbox.files.read(dataPath)

assert.equal(config, configContent)
assert.equal(data, dataContent)
} finally {
await newSandbox.kill()
}
} finally {
await Sandbox.deleteSnapshot(snapshot.snapshotId)
}
},
180_000
)

sandboxTest.skipIf(isDebug)(
'create a filesystem-only snapshot',
async ({ sandbox, sandboxTestId, skip }) => {
Expand Down
17 changes: 0 additions & 17 deletions packages/js-sdk/tests/template/apiDefaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,3 @@ test('template build request sends explicit cpuCount and memoryMB', async () =>
expect(lastBuildBody?.cpuCount).toBe(1)
expect(lastBuildBody?.memoryMB).toBe(512)
})

test('template build request omits maxCpuCount when unset', async () => {
await requestBuild(client(), { name: 'test-template', cpuCount: 2 })

expect(lastBuildBody).not.toHaveProperty('maxCpuCount')
})

test('template build request sends explicit maxCpuCount', async () => {
await requestBuild(client(), {
name: 'test-template',
cpuCount: 2,
maxCpuCount: 4,
})

expect(lastBuildBody?.cpuCount).toBe(2)
expect(lastBuildBody?.maxCpuCount).toBe(4)
})
2 changes: 1 addition & 1 deletion packages/js-sdk/tests/template/boundOpts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ test.each([
})

test.each([
[{}, {}],
[{ cpuCount: 2 }, { cpuCount: 2 }],
[
{ cpuCount: 2, maxCpuCount: 4 },
{ cpuCount: 2, maxCpuCount: 4 },
Expand Down
34 changes: 13 additions & 21 deletions packages/js-sdk/tests/template/utils/getAllFilesInPath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import {
afterEach,
vi,
} from 'vitest'
import { createHash, randomBytes } from 'crypto'
import fs, { statSync } from 'fs'
import { randomBytes } from 'crypto'
import { appendFile, writeFile, mkdir, mkdtemp, rm, symlink } from 'fs/promises'
import { tmpdir } from 'os'
import { join, basename, relative } from 'path'
Expand Down Expand Up @@ -585,28 +584,21 @@ describe('calculateFilesHash', () => {
test('should hash file content spanning multiple read chunks', async () => {
const content = randomBytes(3 * 1024 * 1024 + 123)
const filePath = join(testDir, 'big.bin')
const hash = () =>
calculateFilesHash('big.bin', '/app', testDir, [], false, undefined)

await writeFile(filePath, content)
const stats = statSync(filePath)

const expected = createHash('sha256')
.update('COPY big.bin /app')
.update('big.bin')
.update(stats.mode.toString())
.update(stats.size.toString())
.update(content)
.digest('hex')

expect(
await calculateFilesHash('big.bin', '/app', testDir, [], false, undefined)
).toBe(expected)
})
const original = await hash()

test('should not read whole files into memory', async () => {
await writeFile(join(testDir, 'big.bin'), randomBytes(2 * 1024 * 1024))
const readFileSync = vi.spyOn(fs, 'readFileSync')
// Flip one byte in the final, partial chunk; same size and mode.
content[content.length - 1] ^= 0xff
await writeFile(filePath, content)
const changed = await hash()

await calculateFilesHash('big.bin', '/app', testDir, [], false, undefined)
content[content.length - 1] ^= 0xff
await writeFile(filePath, content)

expect(readFileSync).not.toHaveBeenCalled()
expect(changed).not.toBe(original)
expect(await hash()).toBe(original)
Comment on lines 584 to +602

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) After this change, no test in either SDK pins the template files-hash value. A recipe change that silently invalidates every user's build cache would now pass CI. The new multi-chunk test only checks that flipping the last byte changes the hash. It still passes if earlier chunks are skipped or if hashFileContent hashes the whole buffer instead of subarray(0, bytesRead). Fix: give both SDKs deterministic content spanning more than one 1 MiB chunk and assert one hard-coded hex digest, shared by JS and Python. The Python twin is at test_get_all_files_in_path.py:576-599.

Why this was flagged

The removed tests recomputed the sha256 recipe (COPY line, relative path, mode, size, content) in both getAllFilesInPath.test.ts and test_get_all_files_in_path.py. They pinned the cache key and kept the two SDKs computing the same digest. The replacement test (getAllFilesInPath.test.ts:584-602, test_get_all_files_in_path.py:576-599) uses random content and only compares hashes against each other. Three regressions now pass in both languages: a loop that hashes only the final chunk, a change to field order or encoding, and in JS hash.update(buffer) without subarray(0, bytesRead) at src/template/utils.ts:297. That last one leaks stale bytes from the reused Buffer.allocUnsafe(HASH_CHUNK_SIZE), so small-file hashes become nondeterministic. Grep finds no 64-hex golden digest under either SDK's tests/template, so nothing else catches these. The base branch failed on all of them. A literal golden digest comes from an independent source, so it does not have the tautology problem that the PR set out to remove.

Verification: The diff deletes the only assertions in either SDK that compared the files hash against an independently computed sha256 of the full content. The new test (getAllFilesInPath.test.ts:584-602; test_get_all_files_in_path.py:576-599) uses random content. It flips only content[content.length - 1]. No hex digest or sha256 recomputation remains anywhere.

})
})
7 changes: 7 additions & 0 deletions packages/js-sdk/tsconfig.typecheck.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src", "tests/**/*.test-d.ts"]
}
12 changes: 12 additions & 0 deletions packages/js-sdk/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ export default defineConfig({
setupFiles: ['tests/globalFetchFallback.setup.ts'],
},
},
{
test: {
name: 'types',
include: [],
typecheck: {
enabled: true,
only: true,
include: ['tests/**/*.test-d.ts'],
tsconfig: './tsconfig.typecheck.json',
},
},
},
{
test: {
name: 'connectionConfig',
Expand Down
Loading
Loading