tjs-lang 0.2.0
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.
- package/CONTEXT.md +594 -0
- package/LICENSE +190 -0
- package/README.md +220 -0
- package/bin/benchmarks.ts +351 -0
- package/bin/dev.ts +205 -0
- package/bin/docs.js +170 -0
- package/bin/install-cursor.sh +71 -0
- package/bin/install-vscode.sh +71 -0
- package/bin/select-local-models.d.ts +1 -0
- package/bin/select-local-models.js +28 -0
- package/bin/select-local-models.ts +31 -0
- package/demo/autocomplete.test.ts +232 -0
- package/demo/docs.json +186 -0
- package/demo/examples.test.ts +598 -0
- package/demo/index.html +91 -0
- package/demo/src/autocomplete.ts +482 -0
- package/demo/src/capabilities.ts +859 -0
- package/demo/src/demo-nav.ts +2097 -0
- package/demo/src/examples.test.ts +161 -0
- package/demo/src/examples.ts +476 -0
- package/demo/src/imports.test.ts +196 -0
- package/demo/src/imports.ts +421 -0
- package/demo/src/index.ts +639 -0
- package/demo/src/module-store.ts +635 -0
- package/demo/src/module-sw.ts +132 -0
- package/demo/src/playground.ts +949 -0
- package/demo/src/service-host.ts +389 -0
- package/demo/src/settings.ts +440 -0
- package/demo/src/style.ts +280 -0
- package/demo/src/tjs-playground.ts +1605 -0
- package/demo/src/ts-examples.ts +478 -0
- package/demo/src/ts-playground.ts +1092 -0
- package/demo/static/favicon.svg +30 -0
- package/demo/static/photo-1.jpg +0 -0
- package/demo/static/photo-2.jpg +0 -0
- package/demo/static/texts/ai-history.txt +9 -0
- package/demo/static/texts/coffee-origins.txt +9 -0
- package/demo/static/texts/renewable-energy.txt +9 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +37 -0
- package/dist/tjs-batteries.js +4 -0
- package/dist/tjs-batteries.js.map +15 -0
- package/dist/tjs-full.js +256 -0
- package/dist/tjs-full.js.map +37 -0
- package/dist/tjs-transpiler.js +220 -0
- package/dist/tjs-transpiler.js.map +21 -0
- package/dist/tjs-vm.js +4 -0
- package/dist/tjs-vm.js.map +14 -0
- package/docs/CNAME +1 -0
- package/docs/favicon.svg +30 -0
- package/docs/index.html +91 -0
- package/docs/index.js +10468 -0
- package/docs/index.js.map +92 -0
- package/docs/photo-1.jpg +0 -0
- package/docs/photo-1.webp +0 -0
- package/docs/photo-2.jpg +0 -0
- package/docs/photo-2.webp +0 -0
- package/docs/texts/ai-history.txt +9 -0
- package/docs/texts/coffee-origins.txt +9 -0
- package/docs/texts/renewable-energy.txt +9 -0
- package/docs/tjs-lang.svg +31 -0
- package/docs/tosijs-agent.svg +31 -0
- package/editors/README.md +325 -0
- package/editors/ace/ajs-mode.js +328 -0
- package/editors/ace/ajs-mode.ts +269 -0
- package/editors/ajs-syntax.ts +212 -0
- package/editors/build-grammars.ts +510 -0
- package/editors/codemirror/ajs-language.js +287 -0
- package/editors/codemirror/ajs-language.ts +1447 -0
- package/editors/codemirror/autocomplete.test.ts +531 -0
- package/editors/codemirror/component.ts +404 -0
- package/editors/monaco/ajs-monarch.js +243 -0
- package/editors/monaco/ajs-monarch.ts +225 -0
- package/editors/tjs-syntax.ts +115 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +65 -0
- package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
- package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
- package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
- package/package.json +83 -0
- package/src/cli/commands/check.ts +41 -0
- package/src/cli/commands/convert.ts +133 -0
- package/src/cli/commands/emit.ts +260 -0
- package/src/cli/commands/run.ts +68 -0
- package/src/cli/commands/test.ts +194 -0
- package/src/cli/commands/types.ts +20 -0
- package/src/cli/create-app.ts +236 -0
- package/src/cli/playground.ts +250 -0
- package/src/cli/tjs.ts +166 -0
- package/src/cli/tjsx.ts +160 -0
- package/tjs-lang.svg +31 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for playground examples
|
|
3
|
+
*
|
|
4
|
+
* By default, uses LM Studio if available. Set SKIP_LLM_TESTS=1 to use mocks.
|
|
5
|
+
* Vision tests require a vision-capable model.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Provide browser globals (document, window, etc.) for capabilities.ts
|
|
9
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator'
|
|
10
|
+
GlobalRegistrator.register()
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
|
|
13
|
+
|
|
14
|
+
afterAll(() => {
|
|
15
|
+
GlobalRegistrator.unregister()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Poison pill: detect concurrent execution
|
|
19
|
+
let activeTests = 0
|
|
20
|
+
let maxConcurrentTests = 0
|
|
21
|
+
|
|
22
|
+
function trackTestStart() {
|
|
23
|
+
activeTests++
|
|
24
|
+
maxConcurrentTests = Math.max(maxConcurrentTests, activeTests)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function trackTestEnd() {
|
|
28
|
+
activeTests--
|
|
29
|
+
}
|
|
30
|
+
import { examples } from './src/examples'
|
|
31
|
+
import { AgentVM, transpile, coreAtoms, batteryAtoms, tjs } from '../src'
|
|
32
|
+
import { withRetry } from '../src/test-utils'
|
|
33
|
+
|
|
34
|
+
// Helper to detect if example is TJS (uses TJS-specific syntax)
|
|
35
|
+
function isTjsExample(code: string): boolean {
|
|
36
|
+
// TJS uses -> for return types, : for required params with example values
|
|
37
|
+
return /\)\s*->\s*/.test(code) || /mock\s*\{/.test(code)
|
|
38
|
+
}
|
|
39
|
+
import {
|
|
40
|
+
buildLLMCapability,
|
|
41
|
+
buildLLMBattery,
|
|
42
|
+
getLocalModels,
|
|
43
|
+
type LLMSettings,
|
|
44
|
+
} from './src/capabilities'
|
|
45
|
+
|
|
46
|
+
// Use the SAME code path as the playground
|
|
47
|
+
const LM_STUDIO_URL = 'http://localhost:1234/v1'
|
|
48
|
+
|
|
49
|
+
// Test settings that mirror what the playground uses
|
|
50
|
+
const testSettings: LLMSettings = {
|
|
51
|
+
preferredProvider: 'custom',
|
|
52
|
+
customLlmUrl: LM_STUDIO_URL,
|
|
53
|
+
openaiKey: '',
|
|
54
|
+
anthropicKey: '',
|
|
55
|
+
deepseekKey: '',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let llmCapability: ReturnType<typeof buildLLMCapability>
|
|
59
|
+
let llmBattery: ReturnType<typeof buildLLMBattery>
|
|
60
|
+
let hasLLM = false
|
|
61
|
+
let hasVision = false
|
|
62
|
+
|
|
63
|
+
// Check if a model ID indicates vision capability (same logic as capabilities.ts)
|
|
64
|
+
function isVisionModel(id: string): boolean {
|
|
65
|
+
return (
|
|
66
|
+
id.includes('-vl') ||
|
|
67
|
+
id.includes('vl-') ||
|
|
68
|
+
id.includes('vision') ||
|
|
69
|
+
id.includes('llava') ||
|
|
70
|
+
id.includes('gemma-3') ||
|
|
71
|
+
id.includes('gemma3') ||
|
|
72
|
+
id.includes('qwen3-vl') ||
|
|
73
|
+
id.includes('qwen2.5-vl') ||
|
|
74
|
+
id.includes('pixtral')
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Mock fetch for HTTP APIs (weather, iTunes, GitHub) - these we still mock
|
|
79
|
+
// because they're external APIs, not local LLM
|
|
80
|
+
const createHttpFetchCapability = () => {
|
|
81
|
+
// Load real images from disk for tests
|
|
82
|
+
const fs = require('fs')
|
|
83
|
+
const path = require('path')
|
|
84
|
+
const staticDir = path.join(__dirname, 'static')
|
|
85
|
+
const testDataDir = path.join(__dirname, '..', 'test-data')
|
|
86
|
+
|
|
87
|
+
const loadImage = (dir: string, filename: string): Uint8Array => {
|
|
88
|
+
try {
|
|
89
|
+
const buffer = fs.readFileSync(path.join(dir, filename))
|
|
90
|
+
return new Uint8Array(buffer)
|
|
91
|
+
} catch {
|
|
92
|
+
// Fallback to minimal JPEG header if file not found
|
|
93
|
+
return new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10])
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const mockResponses: Record<string, { body: any; contentType: string }> = {
|
|
98
|
+
'/photo-1.jpg': {
|
|
99
|
+
body: loadImage(staticDir, 'photo-1.jpg'),
|
|
100
|
+
contentType: 'image/jpeg',
|
|
101
|
+
},
|
|
102
|
+
'/photo-2.jpg': {
|
|
103
|
+
body: loadImage(staticDir, 'photo-2.jpg'),
|
|
104
|
+
contentType: 'image/jpeg',
|
|
105
|
+
},
|
|
106
|
+
'/test-shapes.jpg': {
|
|
107
|
+
body: loadImage(testDataDir, 'test-shapes.jpg'),
|
|
108
|
+
contentType: 'image/jpeg',
|
|
109
|
+
},
|
|
110
|
+
'/test-text.jpg': {
|
|
111
|
+
body: loadImage(testDataDir, 'test-text.jpg'),
|
|
112
|
+
contentType: 'image/jpeg',
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const jsonResponses: Record<string, any> = {
|
|
117
|
+
'open-meteo.com': {
|
|
118
|
+
current_weather: {
|
|
119
|
+
temperature: 18.5,
|
|
120
|
+
windspeed: 12.3,
|
|
121
|
+
weathercode: 1,
|
|
122
|
+
time: '2024-01-15T12:00',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
'itunes.apple.com': {
|
|
126
|
+
resultCount: 3,
|
|
127
|
+
results: [
|
|
128
|
+
{
|
|
129
|
+
artistName: 'The Beatles',
|
|
130
|
+
trackName: 'Yesterday',
|
|
131
|
+
collectionName: 'Help!',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
artistName: 'The Beatles',
|
|
135
|
+
trackName: 'Yesterday',
|
|
136
|
+
collectionName: '1',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
artistName: 'Frank Sinatra',
|
|
140
|
+
trackName: 'Yesterday',
|
|
141
|
+
collectionName: 'My Way',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
'api.github.com': {
|
|
146
|
+
total_count: 2,
|
|
147
|
+
items: [
|
|
148
|
+
{
|
|
149
|
+
full_name: 'user/tosijs',
|
|
150
|
+
stargazers_count: 100,
|
|
151
|
+
description: 'A great library',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
full_name: 'other/tosijs-demo',
|
|
155
|
+
stargazers_count: 50,
|
|
156
|
+
description: 'Demo project',
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return async (url: string, options?: any) => {
|
|
163
|
+
let response: Response | undefined
|
|
164
|
+
|
|
165
|
+
for (const [path, data] of Object.entries(mockResponses)) {
|
|
166
|
+
if (url.endsWith(path)) {
|
|
167
|
+
response = new Response(data.body, {
|
|
168
|
+
headers: { 'content-type': data.contentType },
|
|
169
|
+
})
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!response) {
|
|
175
|
+
for (const [domain, jsonData] of Object.entries(jsonResponses)) {
|
|
176
|
+
if (url.includes(domain)) {
|
|
177
|
+
response = new Response(JSON.stringify(jsonData), {
|
|
178
|
+
headers: { 'content-type': 'application/json' },
|
|
179
|
+
})
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!response && url.includes('/texts/')) {
|
|
186
|
+
response = new Response(
|
|
187
|
+
'This is sample text content for testing the summarizer example.',
|
|
188
|
+
{ headers: { 'content-type': 'text/plain' } }
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!response) {
|
|
193
|
+
throw new Error(`Unmocked URL: ${url}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Same dataUrl handling as playground.ts
|
|
197
|
+
if (options?.responseType === 'dataUrl') {
|
|
198
|
+
const buffer = await response.arrayBuffer()
|
|
199
|
+
const bytes = new Uint8Array(buffer)
|
|
200
|
+
let binary = ''
|
|
201
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
202
|
+
binary += String.fromCharCode(bytes[i])
|
|
203
|
+
}
|
|
204
|
+
const base64 = btoa(binary)
|
|
205
|
+
const ct =
|
|
206
|
+
response.headers.get('content-type') || 'application/octet-stream'
|
|
207
|
+
return `data:${ct};base64,${base64}`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const contentType = response.headers.get('content-type')
|
|
211
|
+
if (contentType?.includes('application/json')) {
|
|
212
|
+
return response.json()
|
|
213
|
+
}
|
|
214
|
+
return response.text()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const httpFetch = createHttpFetchCapability()
|
|
219
|
+
|
|
220
|
+
// Simple mock LLM for when LM Studio isn't available
|
|
221
|
+
const mockLLM = {
|
|
222
|
+
predict: async (prompt: string) => {
|
|
223
|
+
if (prompt.includes('capital of France')) return 'Paris'
|
|
224
|
+
if (prompt.includes('Summarize'))
|
|
225
|
+
return 'This is a summary of the provided text.'
|
|
226
|
+
if (prompt.includes('Extract person info')) {
|
|
227
|
+
return JSON.stringify({
|
|
228
|
+
name: 'John Smith',
|
|
229
|
+
age: 35,
|
|
230
|
+
occupation: 'software engineer',
|
|
231
|
+
location: 'San Francisco',
|
|
232
|
+
hobbies: ['hiking', 'photography'],
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
if (prompt.includes('cover versions') || prompt.includes('NOT by')) {
|
|
236
|
+
return JSON.stringify({
|
|
237
|
+
covers: [
|
|
238
|
+
{ track: 'Yesterday', artist: 'Frank Sinatra', album: 'My Way' },
|
|
239
|
+
],
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
if (prompt.includes('Extract the math expression')) return '23 * 47 + 156'
|
|
243
|
+
if (prompt.includes('Calculate:')) return '1237'
|
|
244
|
+
if (prompt.includes('friendly response')) return 'The answer is 1,237!'
|
|
245
|
+
if (prompt.includes('research agent'))
|
|
246
|
+
return '1. Point one\n2. Point two\n3. Point three'
|
|
247
|
+
if (prompt.includes('writer agent'))
|
|
248
|
+
return 'This is a well-written paragraph.'
|
|
249
|
+
if (prompt.includes('editor agent'))
|
|
250
|
+
return 'Suggestion: Add more detail.\n\nImproved: Better paragraph.'
|
|
251
|
+
// LLM Code Solver - generate valid AsyncJS code (Fibonacci)
|
|
252
|
+
if (prompt.includes('function called "solve"')) {
|
|
253
|
+
return `function solve() {
|
|
254
|
+
let a = 0
|
|
255
|
+
let b = 1
|
|
256
|
+
let i = 0
|
|
257
|
+
while (i < 10) {
|
|
258
|
+
let temp = a + b
|
|
259
|
+
a = b
|
|
260
|
+
b = temp
|
|
261
|
+
i = i + 1
|
|
262
|
+
}
|
|
263
|
+
return { result: a }
|
|
264
|
+
}`
|
|
265
|
+
}
|
|
266
|
+
// LLM Code Generator - return code without execution
|
|
267
|
+
if (
|
|
268
|
+
prompt.includes('Write an AsyncJS function') &&
|
|
269
|
+
prompt.includes('factorial')
|
|
270
|
+
) {
|
|
271
|
+
return JSON.stringify({
|
|
272
|
+
code: `function factorial(n: 5) {
|
|
273
|
+
let result = 1
|
|
274
|
+
let i = n
|
|
275
|
+
while (i > 1) {
|
|
276
|
+
result = result * i
|
|
277
|
+
i = i - 1
|
|
278
|
+
}
|
|
279
|
+
return { result }
|
|
280
|
+
}`,
|
|
281
|
+
description: 'Calculates the factorial of n using iteration.',
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
return 'Mock LLM response'
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Mock LLM battery wrapper (for when LM Studio isn't available)
|
|
289
|
+
const mockLLMBattery = {
|
|
290
|
+
predict: async (
|
|
291
|
+
system: string,
|
|
292
|
+
user: any,
|
|
293
|
+
tools?: any[],
|
|
294
|
+
responseFormat?: any
|
|
295
|
+
) => {
|
|
296
|
+
const prompt = typeof user === 'string' ? user : user.text
|
|
297
|
+
const content = await mockLLM.predict(prompt)
|
|
298
|
+
return { content }
|
|
299
|
+
},
|
|
300
|
+
embed: async () => {
|
|
301
|
+
throw new Error('Embedding not available in mock')
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
beforeAll(async () => {
|
|
306
|
+
// Skip LLM if SKIP_LLM_TESTS is set
|
|
307
|
+
if (process.env.SKIP_LLM_TESTS) {
|
|
308
|
+
console.log('SKIP_LLM_TESTS set, using mocks')
|
|
309
|
+
hasLLM = false
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Use the SAME builders as the playground
|
|
314
|
+
llmCapability = buildLLMCapability(testSettings)
|
|
315
|
+
llmBattery = buildLLMBattery(testSettings)
|
|
316
|
+
hasLLM = llmCapability !== null
|
|
317
|
+
|
|
318
|
+
if (hasLLM) {
|
|
319
|
+
// Check for vision models using the same getLocalModels function
|
|
320
|
+
try {
|
|
321
|
+
const models = await getLocalModels(LM_STUDIO_URL)
|
|
322
|
+
const visionModels = models.filter(isVisionModel)
|
|
323
|
+
console.log(
|
|
324
|
+
`LM Studio: ${models.length} models, ${visionModels.length} vision-capable`
|
|
325
|
+
)
|
|
326
|
+
if (visionModels.length > 0) {
|
|
327
|
+
console.log(`Vision models: ${visionModels.join(', ')}`)
|
|
328
|
+
// Actually test if vision works by sending a minimal request
|
|
329
|
+
for (const model of visionModels) {
|
|
330
|
+
console.log(`🔍 Testing vision capability: ${model}`)
|
|
331
|
+
try {
|
|
332
|
+
const testResponse = await fetch(
|
|
333
|
+
`${LM_STUDIO_URL}/chat/completions`,
|
|
334
|
+
{
|
|
335
|
+
method: 'POST',
|
|
336
|
+
headers: { 'Content-Type': 'application/json' },
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
model,
|
|
339
|
+
messages: [
|
|
340
|
+
{
|
|
341
|
+
role: 'user',
|
|
342
|
+
content: [
|
|
343
|
+
{
|
|
344
|
+
type: 'text',
|
|
345
|
+
text: 'test',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
type: 'image_url',
|
|
349
|
+
image_url: {
|
|
350
|
+
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
max_tokens: 1,
|
|
357
|
+
}),
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
if (testResponse.ok) {
|
|
361
|
+
hasVision = true
|
|
362
|
+
console.log(`✅ Vision test for ${model}: works`)
|
|
363
|
+
break
|
|
364
|
+
} else {
|
|
365
|
+
const errorData = await testResponse.json().catch(() => ({}))
|
|
366
|
+
console.log(
|
|
367
|
+
`🧪 Vision test for ${model}: HTTP ${
|
|
368
|
+
testResponse.status
|
|
369
|
+
} - ${JSON.stringify(errorData)}`
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
} catch (e: any) {
|
|
373
|
+
console.log(`🧪 Vision test for ${model}: ${e.message}`)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
console.log('Could not fetch models:', e)
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
console.log('No LLM configured, using mocks')
|
|
382
|
+
}
|
|
383
|
+
}, 30000)
|
|
384
|
+
|
|
385
|
+
describe('Playground Examples', () => {
|
|
386
|
+
const vm = new AgentVM({ ...coreAtoms, ...batteryAtoms })
|
|
387
|
+
|
|
388
|
+
for (const example of examples) {
|
|
389
|
+
const isVision = example.name.startsWith('Vision:')
|
|
390
|
+
const shouldFail =
|
|
391
|
+
example.name === 'Fuel Exhaustion' || example.name === 'Fuel Limits'
|
|
392
|
+
// Examples that generate and run code need retry due to LLM variability
|
|
393
|
+
const needsRetry = example.code.includes('runCode(')
|
|
394
|
+
// TJS examples use the TJS transpiler, not AgentJS
|
|
395
|
+
const isTjs = isTjsExample(example.code)
|
|
396
|
+
|
|
397
|
+
it(`${example.name} - transpiles correctly`, () => {
|
|
398
|
+
if (isTjs) {
|
|
399
|
+
// TJS examples use the TJS transpiler
|
|
400
|
+
const result = tjs(example.code)
|
|
401
|
+
expect(result.code).toBeDefined()
|
|
402
|
+
expect(result.metadata).toBeDefined()
|
|
403
|
+
} else {
|
|
404
|
+
const result = transpile(example.code)
|
|
405
|
+
expect(result.ast).toBeDefined()
|
|
406
|
+
expect(result.error).toBeUndefined()
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
if (shouldFail) {
|
|
411
|
+
it(`${example.name} - runs out of fuel as expected`, async () => {
|
|
412
|
+
const result = transpile(example.code)
|
|
413
|
+
const runResult = await vm.run(result.ast, {}, { fuel: 1000 })
|
|
414
|
+
expect(runResult.error).toBeDefined()
|
|
415
|
+
const errorMsg =
|
|
416
|
+
typeof runResult.error === 'string'
|
|
417
|
+
? runResult.error
|
|
418
|
+
: runResult.error?.message || JSON.stringify(runResult.error)
|
|
419
|
+
expect(errorMsg.toLowerCase()).toContain('fuel')
|
|
420
|
+
})
|
|
421
|
+
} else if (isVision) {
|
|
422
|
+
// Vision tests - check hasVision at runtime, not registration time
|
|
423
|
+
it(`${example.name} - runs successfully`, async () => {
|
|
424
|
+
if (!hasVision) {
|
|
425
|
+
console.log(`Skipping ${example.name}: no vision model available`)
|
|
426
|
+
return // Skip gracefully at runtime
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
trackTestStart()
|
|
430
|
+
try {
|
|
431
|
+
const result = transpile(example.code)
|
|
432
|
+
|
|
433
|
+
const args: Record<string, any> = {}
|
|
434
|
+
if (result.signature?.parameters) {
|
|
435
|
+
for (const [key, param] of Object.entries(
|
|
436
|
+
result.signature.parameters
|
|
437
|
+
)) {
|
|
438
|
+
if ('default' in param) {
|
|
439
|
+
args[key] = param.default
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Override with small test images for faster tests
|
|
445
|
+
if (example.name === 'Vision: OCR') {
|
|
446
|
+
args.imageUrl = '/test-text.jpg'
|
|
447
|
+
} else if (example.name === 'Vision: Classification') {
|
|
448
|
+
args.imageUrl = '/test-shapes.jpg'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Use the SAME capabilities as the playground
|
|
452
|
+
const runResult = await vm.run(result.ast, args, {
|
|
453
|
+
fuel: 100000,
|
|
454
|
+
capabilities: {
|
|
455
|
+
fetch: httpFetch,
|
|
456
|
+
llm: llmCapability || mockLLM,
|
|
457
|
+
llmBattery: llmBattery || mockLLMBattery,
|
|
458
|
+
code: {
|
|
459
|
+
transpile: (source: string) => transpile(source).ast,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
expect(runResult.error).toBeUndefined()
|
|
465
|
+
expect(runResult.result).toBeDefined()
|
|
466
|
+
} finally {
|
|
467
|
+
trackTestEnd()
|
|
468
|
+
}
|
|
469
|
+
}, 120000)
|
|
470
|
+
} else if (needsRetry) {
|
|
471
|
+
// Examples that use runCode need retry due to LLM variability
|
|
472
|
+
it(`${example.name} - runs successfully`, async () => {
|
|
473
|
+
await withRetry(async () => {
|
|
474
|
+
trackTestStart()
|
|
475
|
+
try {
|
|
476
|
+
const result = transpile(example.code)
|
|
477
|
+
|
|
478
|
+
const args: Record<string, any> = {}
|
|
479
|
+
if (result.signature?.parameters) {
|
|
480
|
+
for (const [key, param] of Object.entries(
|
|
481
|
+
result.signature.parameters
|
|
482
|
+
)) {
|
|
483
|
+
if ('default' in param) {
|
|
484
|
+
args[key] = param.default
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const runResult = await vm.run(result.ast, args, {
|
|
490
|
+
fuel: 100000,
|
|
491
|
+
capabilities: {
|
|
492
|
+
fetch: httpFetch,
|
|
493
|
+
llm: llmCapability || mockLLM,
|
|
494
|
+
llmBattery: llmBattery || mockLLMBattery,
|
|
495
|
+
code: {
|
|
496
|
+
transpile: (source: string) => transpile(source).ast,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
if (runResult.error) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
runResult.error.message || String(runResult.error)
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
expect(runResult.result).toBeDefined()
|
|
507
|
+
} finally {
|
|
508
|
+
trackTestEnd()
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
}, 360000) // 3 attempts * 120s each
|
|
512
|
+
} else if (isTjs) {
|
|
513
|
+
// TJS examples run via direct JS execution
|
|
514
|
+
it(`${example.name} - runs successfully`, async () => {
|
|
515
|
+
trackTestStart()
|
|
516
|
+
try {
|
|
517
|
+
const result = tjs(example.code)
|
|
518
|
+
// Execute the transpiled JS code
|
|
519
|
+
const fn = new Function(
|
|
520
|
+
result.code + '\nreturn typeof greet === "function" ? greet : null'
|
|
521
|
+
)
|
|
522
|
+
const greetFn = fn()
|
|
523
|
+
if (greetFn) {
|
|
524
|
+
const output = greetFn('Test', 1)
|
|
525
|
+
expect(output).toContain('Hello')
|
|
526
|
+
}
|
|
527
|
+
} finally {
|
|
528
|
+
trackTestEnd()
|
|
529
|
+
}
|
|
530
|
+
}, 10000)
|
|
531
|
+
} else {
|
|
532
|
+
it(`${example.name} - runs successfully`, async () => {
|
|
533
|
+
trackTestStart()
|
|
534
|
+
try {
|
|
535
|
+
const result = transpile(example.code)
|
|
536
|
+
|
|
537
|
+
const args: Record<string, any> = {}
|
|
538
|
+
if (result.signature?.parameters) {
|
|
539
|
+
for (const [key, param] of Object.entries(
|
|
540
|
+
result.signature.parameters
|
|
541
|
+
)) {
|
|
542
|
+
if ('default' in param) {
|
|
543
|
+
args[key] = param.default
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Use the SAME capabilities as the playground
|
|
549
|
+
const runResult = await vm.run(result.ast, args, {
|
|
550
|
+
fuel: 100000, // High fuel for real LLM calls
|
|
551
|
+
capabilities: {
|
|
552
|
+
fetch: httpFetch,
|
|
553
|
+
llm: llmCapability || mockLLM,
|
|
554
|
+
llmBattery: llmBattery || mockLLMBattery,
|
|
555
|
+
code: {
|
|
556
|
+
transpile: (source: string) => transpile(source).ast,
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
expect(runResult.error).toBeUndefined()
|
|
562
|
+
expect(runResult.result).toBeDefined()
|
|
563
|
+
} finally {
|
|
564
|
+
trackTestEnd()
|
|
565
|
+
}
|
|
566
|
+
}, 120000) // Long timeout for LM Studio
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Poison pill: fail if tests ran concurrently
|
|
571
|
+
it('tests must run sequentially (use --max-concurrency 1)', () => {
|
|
572
|
+
expect(maxConcurrentTests).toBeLessThanOrEqual(1)
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
describe('Example Code Quality', () => {
|
|
577
|
+
it('all examples have unique names', () => {
|
|
578
|
+
const names = examples.map((e) => e.name)
|
|
579
|
+
expect(new Set(names).size).toBe(names.length)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('all examples have descriptions', () => {
|
|
583
|
+
for (const example of examples) {
|
|
584
|
+
expect(example.description.length).toBeGreaterThan(5)
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('LLM examples are marked with requiresApi', () => {
|
|
589
|
+
for (const example of examples) {
|
|
590
|
+
if (
|
|
591
|
+
example.code.includes('llmPredict') ||
|
|
592
|
+
example.code.includes('llmVision')
|
|
593
|
+
) {
|
|
594
|
+
expect(example.requiresApi).toBe(true)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
})
|
|
598
|
+
})
|
package/demo/index.html
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>tjs-lang | Secure Agent Runtime</title>
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="A type-safe virtual machine for executing untrusted agent code safely. Build, run, and deploy AI agents with confidence."
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<!-- Favicon -->
|
|
13
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
|
+
|
|
15
|
+
<!-- Preload critical resources -->
|
|
16
|
+
<link rel="modulepreload" href="/index.js" />
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
/* Critical CSS - prevents flash of unstyled content */
|
|
20
|
+
:root {
|
|
21
|
+
--brand-color: #3d4a6b;
|
|
22
|
+
--brand-text-color: white;
|
|
23
|
+
--background: #ffffff;
|
|
24
|
+
--text-color: #1f2937;
|
|
25
|
+
--spacing: 10px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
* {
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
html,
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: 0;
|
|
36
|
+
height: 100%;
|
|
37
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
38
|
+
Roboto, sans-serif;
|
|
39
|
+
background: var(--background);
|
|
40
|
+
color: var(--text-color);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
body {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main {
|
|
49
|
+
flex: 1;
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Loading state */
|
|
56
|
+
.loading {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
height: 100%;
|
|
61
|
+
font-size: 1.2em;
|
|
62
|
+
color: var(--text-color);
|
|
63
|
+
opacity: 0.6;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.loading::after {
|
|
67
|
+
content: '';
|
|
68
|
+
width: 20px;
|
|
69
|
+
height: 20px;
|
|
70
|
+
margin-left: 10px;
|
|
71
|
+
border: 2px solid var(--brand-color);
|
|
72
|
+
border-top-color: transparent;
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
animation: spin 1s linear infinite;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@keyframes spin {
|
|
78
|
+
to {
|
|
79
|
+
transform: rotate(360deg);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
83
|
+
</head>
|
|
84
|
+
<body>
|
|
85
|
+
<main>
|
|
86
|
+
<div class="loading">Loading tjs-lang</div>
|
|
87
|
+
</main>
|
|
88
|
+
|
|
89
|
+
<script type="module" src="/index.js"></script>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|