Skip to content

Commit a08df96

Browse files
committed
feat: add timeout metadata support in script parsing and enhance related tests
- Introduced support for a new `timeout` property in the metadata interface, allowing scripts to specify execution time limits. - Updated the `parseScript` function to handle timeout metadata from comments and exports. - Enhanced unit tests to validate timeout metadata parsing from various script formats, ensuring robust functionality. - Improved type definitions to include the new timeout property, ensuring type safety across the application.
1 parent 309715b commit a08df96

File tree

6 files changed

+281
-50
lines changed

6 files changed

+281
-50
lines changed

src/core/parser.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ export let postprocessMetadata = (
5353
if (metadata.background) {
5454
if (metadata.background !== "auto") {
5555
// @ts-ignore
56-
result.background = typeof metadata.background === 'boolean'
57-
? metadata.background
56+
result.background = typeof metadata.background === 'boolean'
57+
? metadata.background
5858
: metadata.background === "true"
5959
}
6060
}
@@ -175,19 +175,18 @@ export let parseScript = async (filePath: string): Promise<Script> => {
175175
})
176176
}
177177
return cachedEntry.script
178-
} else {
179-
if (parentPort) {
180-
if (!cachedEntry) {
181-
parentPort.postMessage({
182-
channel: Channel.LOG_TO_PARENT,
183-
value: `[parseScript] Cache miss (file not in cache): ${filePath}`
184-
})
185-
} else {
186-
parentPort.postMessage({
187-
channel: Channel.LOG_TO_PARENT,
188-
value: `[parseScript] Cache miss (mtime mismatch on ${filePath} - Cached: ${cachedEntry.mtimeMs}, Current: ${currentMtimeMs})`
189-
})
190-
}
178+
}
179+
if (parentPort) {
180+
if (!cachedEntry) {
181+
parentPort.postMessage({
182+
channel: Channel.LOG_TO_PARENT,
183+
value: `[parseScript] Cache miss (file not in cache): ${filePath}`
184+
})
185+
} else {
186+
parentPort.postMessage({
187+
channel: Channel.LOG_TO_PARENT,
188+
value: `[parseScript] Cache miss (mtime mismatch on ${filePath} - Cached: ${cachedEntry.mtimeMs}, Current: ${currentMtimeMs})`
189+
})
191190
}
192191
}
193192

src/core/utils.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ ava('parseScript comment full metadata', async (t) => {
5656
let schedule = '0 0 * * *'
5757
let shortcut = `${cmd}+9`
5858
let normalizedShortcut = shortcutNormalizer(shortcut)
59+
let timeout = 15000
5960
let fileName = slugify(name, { lower: true })
6061
let scriptContent = `
6162
import "@johnlindquist/kit"
@@ -64,6 +65,7 @@ import "@johnlindquist/kit"
6465
// Description: ${description}
6566
// Schedule: ${schedule}
6667
// Shortcut: ${shortcut}
68+
// Timeout: ${timeout}
6769
`.trim()
6870

6971
let scriptPath = await outputTmpFile(`${fileName}.ts`, scriptContent)
@@ -74,6 +76,7 @@ import "@johnlindquist/kit"
7476
t.is(script.schedule, schedule as CronExpression)
7577
t.is(script.filePath, scriptPath)
7678
t.is(script.shortcut, normalizedShortcut)
79+
t.is(script.timeout, timeout)
7780
})
7881

7982
ava('parseScript multiline description in global metadata', async (t) => {
@@ -138,6 +141,67 @@ export const metadata = {
138141
t.is(script.filePath, scriptPath)
139142
})
140143

144+
ava('parseScript timeout metadata from comments', async (t) => {
145+
let name = 'Testing Timeout Metadata'
146+
let timeout = 5000
147+
let fileName = slugify(name, { lower: true })
148+
let scriptContent = `
149+
import "@johnlindquist/kit"
150+
151+
// Name: ${name}
152+
// Timeout: ${timeout}
153+
`.trim()
154+
155+
let scriptPath = await outputTmpFile(`${fileName}.ts`, scriptContent)
156+
157+
let script = await parseScript(scriptPath)
158+
t.is(script.name, name)
159+
t.is(script.timeout, timeout)
160+
t.is(script.filePath, scriptPath)
161+
})
162+
163+
ava('parseScript timeout metadata from export', async (t) => {
164+
let name = 'Testing Timeout Export Metadata'
165+
let timeout = 10000
166+
let fileName = slugify(name, { lower: true })
167+
let scriptContent = `
168+
import "@johnlindquist/kit"
169+
170+
export const metadata = {
171+
name: "${name}",
172+
timeout: ${timeout}
173+
}
174+
`.trim()
175+
176+
let scriptPath = await outputTmpFile(`${fileName}.ts`, scriptContent)
177+
178+
let script = await parseScript(scriptPath)
179+
t.is(script.name, name)
180+
t.is(script.timeout, timeout)
181+
t.is(script.filePath, scriptPath)
182+
})
183+
184+
ava('parseScript timeout metadata from global', async (t) => {
185+
let name = 'Testing Timeout Global Metadata'
186+
let timeout = 30000
187+
let fileName = slugify(name, { lower: true })
188+
let scriptContent = `
189+
import "@johnlindquist/kit"
190+
191+
metadata = {
192+
name: "${name}",
193+
timeout: ${timeout}
194+
}
195+
`.trim()
196+
197+
let scriptPath = await outputTmpFile(`${fileName}.ts`, scriptContent)
198+
199+
let script = await parseScript(scriptPath)
200+
t.is(script.name, name)
201+
t.is(script.timeout, timeout)
202+
t.is(script.filePath, scriptPath)
203+
})
204+
141205
ava('parseScript global convention metadata name', async (t) => {
142206
let name = 'Testing Parse Script Convention Global'
143207
let fileName = slugify(name, { lower: true })

src/core/utils.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,11 @@ ${contents}`.trim()
255255
return contents
256256
}
257257

258-
// Create a Set directly, type-checked against the Metadata interface keys.
259-
const VALID_METADATA_KEYS_SET = new Set<keyof Metadata>([
258+
// Exhaustive, compile-time-checked list of metadata keys.
259+
// `satisfies` ensures every entry is a valid `keyof Metadata` **and**
260+
// warns if we add an invalid key. Missing keys will surface when hovering the
261+
// `_MissingKeys` helper type during development.
262+
const META_KEYS = [
260263
"author",
261264
"name",
262265
"description",
@@ -284,7 +287,13 @@ const VALID_METADATA_KEYS_SET = new Set<keyof Metadata>([
284287
"tag",
285288
"longRunning",
286289
"mcp",
287-
]);
290+
'timeout'
291+
] as const satisfies readonly (keyof Metadata)[];
292+
293+
// Optional development-time check for forgotten keys.
294+
type _MissingKeys = Exclude<keyof Metadata, typeof META_KEYS[number]>; // should be never
295+
296+
export const VALID_METADATA_KEYS_SET: ReadonlySet<keyof Metadata> = new Set(META_KEYS);
288297

289298
const getMetadataFromComments = (contents: string): Record<string, any> => {
290299
const lines = contents.split('\n')

src/lib/ai-env-integration.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import ava from 'ava'
2+
import { resolveModel } from './ai'
3+
import '../api/global.js'
4+
5+
// This is a simple integration test to verify the basic functionality
6+
// For full unit testing, we would need to refactor the ai.ts module
7+
// to allow dependency injection of the apiKeyCache
8+
9+
ava('resolveModel should create OpenAI model when API key exists', async t => {
10+
// Set API key
11+
process.env.OPENAI_API_KEY = 'test-openai-key'
12+
13+
try {
14+
const model = await resolveModel('gpt-4')
15+
16+
// Verify model was created
17+
t.truthy(model)
18+
t.is(model.provider, 'openai.chat')
19+
t.is(model.modelId, 'gpt-4')
20+
} finally {
21+
delete process.env.OPENAI_API_KEY
22+
}
23+
})
24+
25+
ava('resolveModel should create Anthropic model with prefix', async t => {
26+
// Set API key
27+
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key'
28+
29+
try {
30+
const model = await resolveModel('anthropic:claude-3-opus-20240229')
31+
32+
// Verify model was created
33+
t.truthy(model)
34+
t.is(model.provider, 'anthropic.messages')
35+
t.is(model.modelId, 'claude-3-opus-20240229')
36+
} finally {
37+
delete process.env.ANTHROPIC_API_KEY
38+
}
39+
})
40+
41+
ava('resolveModel should use default provider when no prefix', async t => {
42+
// Set API key for default provider (openai)
43+
process.env.OPENAI_API_KEY = 'test-openai-key'
44+
process.env.AI_DEFAULT_PROVIDER = 'openai'
45+
46+
try {
47+
const model = await resolveModel('some-model')
48+
49+
// Verify model was created with default provider
50+
t.truthy(model)
51+
t.is(model.provider, 'openai.chat')
52+
t.is(model.modelId, 'some-model')
53+
} finally {
54+
delete process.env.OPENAI_API_KEY
55+
delete process.env.AI_DEFAULT_PROVIDER
56+
}
57+
})
58+
59+
ava('resolveModel should handle explicit provider parameter', async t => {
60+
// Set API key
61+
process.env.GOOGLE_API_KEY = 'test-google-key'
62+
63+
try {
64+
const model = await resolveModel('gemini-pro', 'google')
65+
66+
// Verify model was created with explicit provider
67+
t.truthy(model)
68+
t.is(model.provider, 'google.generative-ai')
69+
t.is(model.modelId, 'gemini-pro')
70+
} finally {
71+
delete process.env.GOOGLE_API_KEY
72+
}
73+
})
74+
75+
// Note: Testing the actual prompting behavior would require:
76+
// 1. Mocking the global.env function properly
77+
// 2. Clearing the internal apiKeyCache between tests
78+
// 3. Potentially refactoring the ai.ts module to support dependency injection
79+
//
80+
// For now, these integration tests verify the basic functionality
81+
// when API keys are already set.

0 commit comments

Comments
 (0)