Skip to content

Commit 7081663

Browse files
committed
test: add SDK and contract package tests (89 tests)
1 parent 5bb9d3d commit 7081663

8 files changed

Lines changed: 850 additions & 1 deletion

File tree

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/contract/src/id.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ describe('generateUUIDv7', () => {
5050
expect(timestamp).toBeGreaterThanOrEqual(before)
5151
expect(timestamp).toBeLessThanOrEqual(after)
5252
})
53+
54+
test('generates unique values across 100 rapid calls', () => {
55+
const set = new Set<string>()
56+
for (let i = 0; i < 100; i++) {
57+
set.add(base62Encode(generateUUIDv7()))
58+
}
59+
expect(set.size).toBe(100)
60+
})
61+
62+
test('version and variant are consistent across many generations', () => {
63+
for (let i = 0; i < 50; i++) {
64+
const bytes = generateUUIDv7()
65+
expect((bytes[6] >> 4) & 0x0f).toBe(7)
66+
expect((bytes[8] >> 6) & 0x03).toBe(2)
67+
}
68+
})
5369
})
5470

5571
describe('base62', () => {
@@ -87,6 +103,19 @@ describe('base62', () => {
87103
test('throws on invalid character', () => {
88104
expect(() => base62Decode('!'.repeat(22))).toThrow('Invalid base62 character')
89105
})
106+
107+
test('encoding is deterministic', () => {
108+
const bytes = generateUUIDv7()
109+
const a = base62Encode(bytes)
110+
const b = base62Encode(bytes)
111+
expect(a).toBe(b)
112+
})
113+
114+
test('different bytes produce different encodings', () => {
115+
const a = base62Encode(generateUUIDv7())
116+
const b = base62Encode(generateUUIDv7())
117+
expect(a).not.toBe(b)
118+
})
90119
})
91120

92121
describe('generateId', () => {
@@ -110,6 +139,19 @@ describe('generateId', () => {
110139
const b = generateId('ex_')
111140
expect(a < b).toBe(true)
112141
})
142+
143+
test('ID starts with the given prefix', () => {
144+
const id = generateId('sb_')
145+
expect(id.startsWith('sb_')).toBe(true)
146+
})
147+
148+
test('ID length is prefix + 22 base62 chars', () => {
149+
const prefixes = ['sb_', 'ex_', 'sess_', 'art_', 'img_', 'prof_', 'node_', 'proj_']
150+
for (const prefix of prefixes) {
151+
const id = generateId(prefix)
152+
expect(id.length).toBe(prefix.length + 22)
153+
}
154+
})
113155
})
114156

115157
describe('parseId', () => {
@@ -153,4 +195,46 @@ describe('idToBytes / bytesToId', () => {
153195
const reconstructed = bytesToId('art_', bytes)
154196
expect(reconstructed).toBe(id)
155197
})
198+
199+
test('idToBytes returns 16 bytes', () => {
200+
const id = generateId('sb_')
201+
const bytes = idToBytes(id)
202+
expect(bytes.length).toBe(16)
203+
})
204+
205+
test('same ID always produces same bytes', () => {
206+
const id = generateId('sb_')
207+
const a = idToBytes(id)
208+
const b = idToBytes(id)
209+
expect(a).toEqual(b)
210+
})
211+
})
212+
213+
describe('prefix constants', () => {
214+
test('all prefixes end with underscore', () => {
215+
const prefixes = [
216+
SANDBOX_PREFIX,
217+
EXEC_PREFIX,
218+
SESSION_PREFIX,
219+
ARTIFACT_PREFIX,
220+
IMAGE_PREFIX,
221+
PROFILE_PREFIX,
222+
NODE_PREFIX,
223+
PROJECT_PREFIX,
224+
]
225+
for (const prefix of prefixes) {
226+
expect(prefix.endsWith('_')).toBe(true)
227+
}
228+
})
229+
230+
test('prefixes have expected values', () => {
231+
expect(SANDBOX_PREFIX).toBe('sb_')
232+
expect(EXEC_PREFIX).toBe('ex_')
233+
expect(SESSION_PREFIX).toBe('sess_')
234+
expect(ARTIFACT_PREFIX).toBe('art_')
235+
expect(IMAGE_PREFIX).toBe('img_')
236+
expect(PROFILE_PREFIX).toBe('prof_')
237+
expect(NODE_PREFIX).toBe('node_')
238+
expect(PROJECT_PREFIX).toBe('proj_')
239+
})
156240
})

packages/sdk-ts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
"build": "tsc",
1515
"typecheck": "tsc --noEmit",
1616
"lint": "eslint src/",
17-
"test": "echo 'no tests yet'"
17+
"test": "bun test"
1818
},
1919
"dependencies": {
2020
"@sandchest/contract": "workspace:*"
2121
},
2222
"devDependencies": {
23+
"@types/bun": "^1.3.9",
2324
"@types/node": "^25.3.0"
2425
}
2526
}

packages/sdk-ts/src/client.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2+
import { Sandchest } from './client.js'
3+
4+
describe('Sandchest', () => {
5+
const originalEnv = process.env['SANDCHEST_API_KEY']
6+
7+
beforeEach(() => {
8+
delete process.env['SANDCHEST_API_KEY']
9+
})
10+
11+
afterEach(() => {
12+
if (originalEnv !== undefined) {
13+
process.env['SANDCHEST_API_KEY'] = originalEnv
14+
} else {
15+
delete process.env['SANDCHEST_API_KEY']
16+
}
17+
})
18+
19+
test('throws when no API key is provided and env is unset', () => {
20+
expect(() => new Sandchest()).toThrow('Sandchest API key is required')
21+
})
22+
23+
test('accepts API key via options', () => {
24+
const client = new Sandchest({ apiKey: 'sk_test_123' })
25+
expect(client).toBeInstanceOf(Sandchest)
26+
})
27+
28+
test('reads API key from SANDCHEST_API_KEY env var', () => {
29+
process.env['SANDCHEST_API_KEY'] = 'sk_from_env'
30+
const client = new Sandchest()
31+
expect(client).toBeInstanceOf(Sandchest)
32+
})
33+
34+
test('options apiKey takes precedence over env var', () => {
35+
process.env['SANDCHEST_API_KEY'] = 'sk_from_env'
36+
const client = new Sandchest({ apiKey: 'sk_from_opts' })
37+
// The HttpClient is created — we just verify no error thrown
38+
expect(client._http).toBeDefined()
39+
})
40+
41+
test('uses default base URL when not specified', () => {
42+
const client = new Sandchest({ apiKey: 'sk_test' })
43+
expect(client._http).toBeDefined()
44+
})
45+
46+
test('accepts custom baseUrl, timeout, and retries', () => {
47+
const client = new Sandchest({
48+
apiKey: 'sk_test',
49+
baseUrl: 'https://custom.api.com',
50+
timeout: 5000,
51+
retries: 1,
52+
})
53+
expect(client._http).toBeDefined()
54+
})
55+
56+
test('create throws not implemented', async () => {
57+
const client = new Sandchest({ apiKey: 'sk_test' })
58+
await expect(client.create()).rejects.toThrow('Not implemented')
59+
})
60+
61+
test('get throws not implemented', async () => {
62+
const client = new Sandchest({ apiKey: 'sk_test' })
63+
await expect(client.get('sb_abc')).rejects.toThrow('Not implemented')
64+
})
65+
66+
test('list throws not implemented', async () => {
67+
const client = new Sandchest({ apiKey: 'sk_test' })
68+
await expect(client.list()).rejects.toThrow('Not implemented')
69+
})
70+
})

packages/sdk-ts/src/errors.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { describe, test, expect } from 'bun:test'
2+
import {
3+
SandchestError,
4+
NotFoundError,
5+
RateLimitError,
6+
SandboxNotRunningError,
7+
ValidationError,
8+
AuthenticationError,
9+
} from './errors.js'
10+
11+
describe('SandchestError', () => {
12+
test('stores code, status, requestId, and message', () => {
13+
const err = new SandchestError({
14+
code: 'internal_error',
15+
message: 'Something went wrong',
16+
status: 500,
17+
requestId: 'req_abc123',
18+
})
19+
20+
expect(err.code).toBe('internal_error')
21+
expect(err.status).toBe(500)
22+
expect(err.requestId).toBe('req_abc123')
23+
expect(err.message).toBe('Something went wrong')
24+
expect(err.name).toBe('SandchestError')
25+
})
26+
27+
test('extends Error', () => {
28+
const err = new SandchestError({
29+
code: 'internal_error',
30+
message: 'fail',
31+
status: 500,
32+
requestId: 'req_1',
33+
})
34+
expect(err).toBeInstanceOf(Error)
35+
})
36+
})
37+
38+
describe('NotFoundError', () => {
39+
test('sets status 404 and code not_found', () => {
40+
const err = new NotFoundError({ message: 'Sandbox not found', requestId: 'req_2' })
41+
expect(err.status).toBe(404)
42+
expect(err.code).toBe('not_found')
43+
expect(err.name).toBe('NotFoundError')
44+
expect(err.message).toBe('Sandbox not found')
45+
})
46+
47+
test('is instanceof SandchestError', () => {
48+
const err = new NotFoundError({ message: 'gone', requestId: 'req_3' })
49+
expect(err).toBeInstanceOf(SandchestError)
50+
expect(err).toBeInstanceOf(Error)
51+
})
52+
})
53+
54+
describe('RateLimitError', () => {
55+
test('sets status 429, code rate_limited, and retryAfter', () => {
56+
const err = new RateLimitError({
57+
message: 'Too many requests',
58+
requestId: 'req_4',
59+
retryAfter: 30,
60+
})
61+
expect(err.status).toBe(429)
62+
expect(err.code).toBe('rate_limited')
63+
expect(err.name).toBe('RateLimitError')
64+
expect(err.retryAfter).toBe(30)
65+
})
66+
67+
test('is instanceof SandchestError', () => {
68+
const err = new RateLimitError({ message: 'slow down', requestId: 'req_5', retryAfter: 1 })
69+
expect(err).toBeInstanceOf(SandchestError)
70+
})
71+
})
72+
73+
describe('SandboxNotRunningError', () => {
74+
test('sets status 409 and code sandbox_not_running', () => {
75+
const err = new SandboxNotRunningError({
76+
message: 'Sandbox is stopped',
77+
requestId: 'req_6',
78+
})
79+
expect(err.status).toBe(409)
80+
expect(err.code).toBe('sandbox_not_running')
81+
expect(err.name).toBe('SandboxNotRunningError')
82+
})
83+
84+
test('is instanceof SandchestError', () => {
85+
const err = new SandboxNotRunningError({ message: 'not running', requestId: 'req_7' })
86+
expect(err).toBeInstanceOf(SandchestError)
87+
})
88+
})
89+
90+
describe('ValidationError', () => {
91+
test('sets status 400 and code validation_error', () => {
92+
const err = new ValidationError({ message: 'Invalid body', requestId: 'req_8' })
93+
expect(err.status).toBe(400)
94+
expect(err.code).toBe('validation_error')
95+
expect(err.name).toBe('ValidationError')
96+
})
97+
98+
test('is instanceof SandchestError', () => {
99+
const err = new ValidationError({ message: 'bad', requestId: 'req_9' })
100+
expect(err).toBeInstanceOf(SandchestError)
101+
})
102+
})
103+
104+
describe('AuthenticationError', () => {
105+
test('sets status 401 and code unauthorized', () => {
106+
const err = new AuthenticationError({ message: 'Invalid API key', requestId: 'req_10' })
107+
expect(err.status).toBe(401)
108+
expect(err.code).toBe('unauthorized')
109+
expect(err.name).toBe('AuthenticationError')
110+
})
111+
112+
test('is instanceof SandchestError', () => {
113+
const err = new AuthenticationError({ message: 'unauthed', requestId: 'req_11' })
114+
expect(err).toBeInstanceOf(SandchestError)
115+
})
116+
})

0 commit comments

Comments
 (0)