@llmindset/hf-mcp 0.2.33 → 0.2.35
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/dist/docs-search/docs-semantic-search.js +1 -1
- package/dist/docs-search/docs-semantic-search.js.map +1 -1
- package/dist/hf-api-call.d.ts.map +1 -1
- package/dist/hf-api-call.js +4 -0
- package/dist/hf-api-call.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jobs/api-client.d.ts +19 -0
- package/dist/jobs/api-client.d.ts.map +1 -0
- package/dist/jobs/api-client.js +95 -0
- package/dist/jobs/api-client.js.map +1 -0
- package/dist/jobs/commands/inspect.d.ts +5 -0
- package/dist/jobs/commands/inspect.d.ts.map +1 -0
- package/dist/jobs/commands/inspect.js +21 -0
- package/dist/jobs/commands/inspect.js.map +1 -0
- package/dist/jobs/commands/logs.d.ts +4 -0
- package/dist/jobs/commands/logs.d.ts.map +1 -0
- package/dist/jobs/commands/logs.js +24 -0
- package/dist/jobs/commands/logs.js.map +1 -0
- package/dist/jobs/commands/ps.d.ts +4 -0
- package/dist/jobs/commands/ps.d.ts.map +1 -0
- package/dist/jobs/commands/ps.js +23 -0
- package/dist/jobs/commands/ps.js.map +1 -0
- package/dist/jobs/commands/run.d.ts +5 -0
- package/dist/jobs/commands/run.d.ts.map +1 -0
- package/dist/jobs/commands/run.js +90 -0
- package/dist/jobs/commands/run.js.map +1 -0
- package/dist/jobs/commands/scheduled.d.ts +10 -0
- package/dist/jobs/commands/scheduled.d.ts.map +1 -0
- package/dist/jobs/commands/scheduled.js +112 -0
- package/dist/jobs/commands/scheduled.js.map +1 -0
- package/dist/jobs/commands/utils.d.ts +20 -0
- package/dist/jobs/commands/utils.d.ts.map +1 -0
- package/dist/jobs/commands/utils.js +120 -0
- package/dist/jobs/commands/utils.js.map +1 -0
- package/dist/jobs/formatters.d.ts +6 -0
- package/dist/jobs/formatters.d.ts.map +1 -0
- package/dist/jobs/formatters.js +98 -0
- package/dist/jobs/formatters.js.map +1 -0
- package/dist/jobs/sse-handler.d.ts +12 -0
- package/dist/jobs/sse-handler.d.ts.map +1 -0
- package/dist/jobs/sse-handler.js +80 -0
- package/dist/jobs/sse-handler.js.map +1 -0
- package/dist/jobs/tool.d.ts +35 -0
- package/dist/jobs/tool.d.ts.map +1 -0
- package/dist/jobs/tool.js +333 -0
- package/dist/jobs/tool.js.map +1 -0
- package/dist/jobs/types.d.ts +295 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +95 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/tool-ids.d.ts +3 -2
- package/dist/tool-ids.d.ts.map +1 -1
- package/dist/tool-ids.js +10 -2
- package/dist/tool-ids.js.map +1 -1
- package/dist/types/tool-result.d.ts +1 -0
- package/dist/types/tool-result.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/docs-search/docs-semantic-search.ts +1 -1
- package/src/hf-api-call.ts +6 -0
- package/src/index.ts +1 -0
- package/src/jobs/api-client.ts +187 -0
- package/src/jobs/commands/inspect.ts +38 -0
- package/src/jobs/commands/logs.ts +36 -0
- package/src/jobs/commands/ps.ts +40 -0
- package/src/jobs/commands/run.ts +135 -0
- package/src/jobs/commands/scheduled.ts +198 -0
- package/src/jobs/commands/utils.ts +191 -0
- package/src/jobs/formatters.ts +149 -0
- package/src/jobs/sse-handler.ts +144 -0
- package/src/jobs/tool.ts +435 -0
- package/src/jobs/types.ts +237 -0
- package/src/tool-ids.ts +11 -1
- package/src/types/tool-result.ts +6 -0
- package/test/jobs/command-translation.spec.ts +331 -0
- package/test/jobs/formatters.spec.ts +267 -0
- package/test/jobs/uv-command.spec.ts +81 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hardware flavors available for jobs
|
|
5
|
+
*/
|
|
6
|
+
export const CPU_FLAVORS = ['cpu-basic', 'cpu-upgrade', 'cpu-performance', 'cpu-xl'] as const;
|
|
7
|
+
|
|
8
|
+
export const GPU_FLAVORS = [
|
|
9
|
+
'sprx8',
|
|
10
|
+
'zero-a10g',
|
|
11
|
+
't4-small',
|
|
12
|
+
't4-medium',
|
|
13
|
+
'l4x1',
|
|
14
|
+
'l4x4',
|
|
15
|
+
'l40sx1',
|
|
16
|
+
'l40sx4',
|
|
17
|
+
'l40sx8',
|
|
18
|
+
'a10g-small',
|
|
19
|
+
'a10g-large',
|
|
20
|
+
'a10g-largex2',
|
|
21
|
+
'a10g-largex4',
|
|
22
|
+
'a100-large',
|
|
23
|
+
'h100',
|
|
24
|
+
'h100x8',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export const SPECIALIZED_FLAVORS = ['inf2x6'] as const;
|
|
28
|
+
|
|
29
|
+
export const ALL_FLAVORS = [...CPU_FLAVORS, ...GPU_FLAVORS, ...SPECIALIZED_FLAVORS] as const;
|
|
30
|
+
|
|
31
|
+
export type JobFlavor = (typeof ALL_FLAVORS)[number];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Job status stages (from OpenAPI spec)
|
|
35
|
+
*/
|
|
36
|
+
export type JobStage = 'RUNNING' | 'COMPLETED' | 'CANCELED' | 'ERROR' | 'DELETED';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Job status object from API
|
|
40
|
+
*/
|
|
41
|
+
export interface JobStatus {
|
|
42
|
+
stage: JobStage;
|
|
43
|
+
message?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Job owner information
|
|
48
|
+
*/
|
|
49
|
+
export interface JobOwner {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
type: 'user' | 'org';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Job information from API
|
|
57
|
+
* Based on OpenAPI schema
|
|
58
|
+
*/
|
|
59
|
+
export interface JobInfo {
|
|
60
|
+
id: string;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
dockerImage?: string;
|
|
63
|
+
spaceId?: string;
|
|
64
|
+
command?: string[];
|
|
65
|
+
arguments?: string[];
|
|
66
|
+
environment: Record<string, string>;
|
|
67
|
+
secrets?: Record<string, string | null>;
|
|
68
|
+
flavor: string;
|
|
69
|
+
status: JobStatus;
|
|
70
|
+
owner: JobOwner;
|
|
71
|
+
createdBy?: JobOwner;
|
|
72
|
+
tags?: string[];
|
|
73
|
+
timeout?: number;
|
|
74
|
+
// Additional fields not in OpenAPI but present in responses
|
|
75
|
+
url?: string;
|
|
76
|
+
endpoint?: string;
|
|
77
|
+
finishedAt?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Job specification for creating jobs
|
|
82
|
+
*/
|
|
83
|
+
export interface JobSpec {
|
|
84
|
+
dockerImage?: string;
|
|
85
|
+
spaceId?: string;
|
|
86
|
+
command: string[];
|
|
87
|
+
arguments?: string[];
|
|
88
|
+
environment?: Record<string, string>;
|
|
89
|
+
secrets?: Record<string, string>;
|
|
90
|
+
flavor: string;
|
|
91
|
+
timeoutSeconds?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scheduled job specification
|
|
96
|
+
*/
|
|
97
|
+
export interface ScheduledJobSpec {
|
|
98
|
+
schedule: string;
|
|
99
|
+
suspend?: boolean;
|
|
100
|
+
jobSpec: JobSpec;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scheduled job information from API
|
|
105
|
+
*/
|
|
106
|
+
export interface ScheduledJobInfo {
|
|
107
|
+
id: string;
|
|
108
|
+
schedule: string;
|
|
109
|
+
suspend: boolean;
|
|
110
|
+
jobSpec: JobSpec;
|
|
111
|
+
lastRun?: string;
|
|
112
|
+
nextRun?: string;
|
|
113
|
+
owner: JobOwner;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Log event from SSE stream
|
|
119
|
+
*/
|
|
120
|
+
export interface LogEvent {
|
|
121
|
+
timestamp: string;
|
|
122
|
+
data: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Zod schemas for command arguments
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
// Common args shared across commands
|
|
130
|
+
const commonArgsSchema = z.object({
|
|
131
|
+
namespace: z.string().optional().describe('Target namespace (username or organization). Defaults to current user.'),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Run command args
|
|
135
|
+
export const runArgsSchema = commonArgsSchema.extend({
|
|
136
|
+
image: z.string().describe('Docker image or HF Space URL (e.g., "python:3.12" or "hf.co/spaces/user/space")'),
|
|
137
|
+
command: z
|
|
138
|
+
.union([z.string(), z.array(z.string())])
|
|
139
|
+
.describe(
|
|
140
|
+
'Command to execute. Array format recommended (e.g., ["python", "script.py"]). ' +
|
|
141
|
+
'String format is parsed with POSIX shell semantics (quotes, escaping). ' +
|
|
142
|
+
'For multiline scripts, use array with newlines in arguments.'
|
|
143
|
+
),
|
|
144
|
+
flavor: z
|
|
145
|
+
.enum(ALL_FLAVORS)
|
|
146
|
+
.optional()
|
|
147
|
+
.default('cpu-basic')
|
|
148
|
+
.describe(`Hardware flavor. Options: ${ALL_FLAVORS.join(', ')}`),
|
|
149
|
+
env: z.record(z.string()).optional().describe('Environment variables as key-value pairs'),
|
|
150
|
+
secrets: z.record(z.string()).optional().describe('Secret environment variables (encrypted server-side)'),
|
|
151
|
+
timeout: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe('Max duration (e.g., "5m", "2h", "30s"). Default: 30m')
|
|
155
|
+
.default('30m'),
|
|
156
|
+
detach: z
|
|
157
|
+
.boolean()
|
|
158
|
+
.optional()
|
|
159
|
+
.default(true)
|
|
160
|
+
.describe('Run in background and return immediately (default: true)'),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// UV command args
|
|
164
|
+
export const uvArgsSchema = commonArgsSchema.extend({
|
|
165
|
+
script: z
|
|
166
|
+
.string()
|
|
167
|
+
.describe('Python script: local file path, URL, or inline code. UV will handle dependencies automatically.'),
|
|
168
|
+
repo: z.string().optional().describe('Persistent repository name for script storage'),
|
|
169
|
+
with_deps: z.array(z.string()).optional().describe('Additional package dependencies'),
|
|
170
|
+
script_args: z.array(z.string()).optional().describe('Arguments to pass to the script'),
|
|
171
|
+
python: z.string().optional().describe('Python interpreter version (e.g., "3.12")'),
|
|
172
|
+
flavor: z.enum(ALL_FLAVORS).optional().default('cpu-basic').describe('Hardware flavor'),
|
|
173
|
+
env: z.record(z.string()).optional().describe('Environment variables'),
|
|
174
|
+
secrets: z.record(z.string()).optional().describe('Secret environment variables'),
|
|
175
|
+
timeout: z.string().optional().default('30m').describe('Max duration'),
|
|
176
|
+
detach: z.boolean().optional().default(true).describe('Run in background'),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// PS command args
|
|
180
|
+
export const psArgsSchema = commonArgsSchema.extend({
|
|
181
|
+
all: z.boolean().optional().default(false).describe('Show all jobs (default: only running)'),
|
|
182
|
+
status: z.string().optional().describe('Filter by status (e.g., "RUNNING", "COMPLETED")'),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Logs command args
|
|
186
|
+
export const logsArgsSchema = commonArgsSchema.extend({
|
|
187
|
+
job_id: z.string().describe('Job ID to fetch logs from'),
|
|
188
|
+
tail: z.number().optional().default(20).describe('Number of lines to return (default: 20)'),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Inspect command args
|
|
192
|
+
export const inspectArgsSchema = commonArgsSchema.extend({
|
|
193
|
+
job_id: z.union([z.string(), z.array(z.string())]).describe('Job ID(s) to inspect'),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Cancel command args
|
|
197
|
+
export const cancelArgsSchema = commonArgsSchema.extend({
|
|
198
|
+
job_id: z.string().describe('Job ID to cancel'),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Scheduled run args
|
|
202
|
+
export const scheduledRunArgsSchema = runArgsSchema.extend({
|
|
203
|
+
schedule: z
|
|
204
|
+
.string()
|
|
205
|
+
.describe('Schedule: cron expression or shorthand (@hourly, @daily, @weekly, @monthly, @yearly)'),
|
|
206
|
+
suspend: z.boolean().optional().default(false).describe('Create in suspended state'),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Scheduled UV args
|
|
210
|
+
export const scheduledUvArgsSchema = uvArgsSchema.extend({
|
|
211
|
+
schedule: z.string().describe('Schedule: cron expression or shorthand'),
|
|
212
|
+
suspend: z.boolean().optional().default(false).describe('Create in suspended state'),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Scheduled PS args
|
|
216
|
+
export const scheduledPsArgsSchema = commonArgsSchema.extend({
|
|
217
|
+
all: z.boolean().optional().default(false).describe('Show all scheduled jobs (default: hide suspended)'),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Scheduled inspect/delete/suspend/resume args
|
|
221
|
+
export const scheduledJobArgsSchema = commonArgsSchema.extend({
|
|
222
|
+
scheduled_job_id: z.string().describe('Scheduled job ID'),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Export type aliases for use in commands
|
|
227
|
+
*/
|
|
228
|
+
export type RunArgs = z.infer<typeof runArgsSchema>;
|
|
229
|
+
export type UvArgs = z.infer<typeof uvArgsSchema>;
|
|
230
|
+
export type PsArgs = z.infer<typeof psArgsSchema>;
|
|
231
|
+
export type LogsArgs = z.infer<typeof logsArgsSchema>;
|
|
232
|
+
export type InspectArgs = z.infer<typeof inspectArgsSchema>;
|
|
233
|
+
export type CancelArgs = z.infer<typeof cancelArgsSchema>;
|
|
234
|
+
export type ScheduledRunArgs = z.infer<typeof scheduledRunArgsSchema>;
|
|
235
|
+
export type ScheduledUvArgs = z.infer<typeof scheduledUvArgsSchema>;
|
|
236
|
+
export type ScheduledPsArgs = z.infer<typeof scheduledPsArgsSchema>;
|
|
237
|
+
export type ScheduledJobArgs = z.infer<typeof scheduledJobArgsSchema>;
|
package/src/tool-ids.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
DOCS_SEMANTIC_SEARCH_CONFIG,
|
|
22
22
|
DOC_FETCH_CONFIG,
|
|
23
23
|
USE_SPACE_TOOL_CONFIG,
|
|
24
|
+
HF_JOBS_TOOL_CONFIG,
|
|
24
25
|
} from './index.js';
|
|
25
26
|
|
|
26
27
|
// Extract tool IDs from their configs (single source of truth)
|
|
@@ -41,6 +42,7 @@ export const USER_SUMMARY_PROMPT_ID = USER_SUMMARY_PROMPT_CONFIG.name;
|
|
|
41
42
|
export const PAPER_SUMMARY_PROMPT_ID = PAPER_SUMMARY_PROMPT_CONFIG.name;
|
|
42
43
|
export const MODEL_DETAIL_PROMPT_ID = MODEL_DETAIL_PROMPT_CONFIG.name;
|
|
43
44
|
export const DATASET_DETAIL_PROMPT_ID = DATASET_DETAIL_PROMPT_CONFIG.name;
|
|
45
|
+
export const HF_JOBS_TOOL_ID = HF_JOBS_TOOL_CONFIG.name;
|
|
44
46
|
|
|
45
47
|
// Complete list of all built-in tool IDs
|
|
46
48
|
export const ALL_BUILTIN_TOOL_IDS = [
|
|
@@ -57,6 +59,7 @@ export const ALL_BUILTIN_TOOL_IDS = [
|
|
|
57
59
|
DOCS_SEMANTIC_SEARCH_TOOL_ID,
|
|
58
60
|
DOC_FETCH_TOOL_ID,
|
|
59
61
|
USE_SPACE_TOOL_ID,
|
|
62
|
+
HF_JOBS_TOOL_ID,
|
|
60
63
|
] as const;
|
|
61
64
|
// Grouped tool IDs for bouquet configurations
|
|
62
65
|
export const TOOL_ID_GROUPS = {
|
|
@@ -67,7 +70,13 @@ export const TOOL_ID_GROUPS = {
|
|
|
67
70
|
PAPER_SEARCH_TOOL_ID,
|
|
68
71
|
DOCS_SEMANTIC_SEARCH_TOOL_ID,
|
|
69
72
|
] as const,
|
|
70
|
-
spaces: [
|
|
73
|
+
spaces: [
|
|
74
|
+
SPACE_SEARCH_TOOL_ID,
|
|
75
|
+
DUPLICATE_SPACE_TOOL_ID,
|
|
76
|
+
SPACE_INFO_TOOL_ID,
|
|
77
|
+
SPACE_FILES_TOOL_ID,
|
|
78
|
+
USE_SPACE_TOOL_ID,
|
|
79
|
+
] as const,
|
|
71
80
|
detail: [MODEL_DETAIL_TOOL_ID, DATASET_DETAIL_TOOL_ID, HUB_INSPECT_TOOL_ID] as const,
|
|
72
81
|
docs: [DOCS_SEMANTIC_SEARCH_TOOL_ID, DOC_FETCH_TOOL_ID] as const,
|
|
73
82
|
hf_api: [
|
|
@@ -77,6 +86,7 @@ export const TOOL_ID_GROUPS = {
|
|
|
77
86
|
PAPER_SEARCH_TOOL_ID,
|
|
78
87
|
HUB_INSPECT_TOOL_ID,
|
|
79
88
|
DOCS_SEMANTIC_SEARCH_TOOL_ID,
|
|
89
|
+
// HF_JOBS_TOOL_ID,
|
|
80
90
|
] as const,
|
|
81
91
|
all: [...ALL_BUILTIN_TOOL_IDS] as const,
|
|
82
92
|
} as const;
|
package/src/types/tool-result.ts
CHANGED
|
@@ -21,4 +21,10 @@ export interface ToolResult {
|
|
|
21
21
|
* For prompts: 1 if generated successfully
|
|
22
22
|
*/
|
|
23
23
|
resultsShared: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Indicates whether this result represents an error condition
|
|
27
|
+
* When true, formatted contains an error message
|
|
28
|
+
*/
|
|
29
|
+
isError?: boolean;
|
|
24
30
|
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseTimeout, parseImageSource, parseCommand, createJobSpec } from '../../src/jobs/commands/utils.js';
|
|
3
|
+
|
|
4
|
+
describe('Jobs Command Translation', () => {
|
|
5
|
+
describe('parseTimeout', () => {
|
|
6
|
+
it('should parse timeout with seconds', () => {
|
|
7
|
+
expect(parseTimeout('30s')).toBe(30);
|
|
8
|
+
expect(parseTimeout('45s')).toBe(45);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should parse timeout with minutes', () => {
|
|
12
|
+
expect(parseTimeout('5m')).toBe(300);
|
|
13
|
+
expect(parseTimeout('10m')).toBe(600);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should parse timeout with hours', () => {
|
|
17
|
+
expect(parseTimeout('2h')).toBe(7200);
|
|
18
|
+
expect(parseTimeout('1h')).toBe(3600);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should parse timeout with days', () => {
|
|
22
|
+
expect(parseTimeout('1d')).toBe(86400);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should parse plain number as seconds', () => {
|
|
26
|
+
expect(parseTimeout('300')).toBe(300);
|
|
27
|
+
expect(parseTimeout('60')).toBe(60);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle decimal values', () => {
|
|
31
|
+
expect(parseTimeout('1.5h')).toBe(5400);
|
|
32
|
+
expect(parseTimeout('2.5m')).toBe(150);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should throw error for invalid format', () => {
|
|
36
|
+
expect(() => parseTimeout('invalid')).toThrow(/Invalid timeout format/);
|
|
37
|
+
expect(() => parseTimeout('xyz')).toThrow(/Invalid timeout format/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should parse partial numbers (parseInt behavior)', () => {
|
|
41
|
+
// parseInt stops at first non-numeric, so '5x' becomes 5
|
|
42
|
+
expect(parseTimeout('5x')).toBe(5);
|
|
43
|
+
expect(parseTimeout('10abc')).toBe(10);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('parseImageSource', () => {
|
|
48
|
+
it('should detect Docker Hub images', () => {
|
|
49
|
+
const result = parseImageSource('python:3.12');
|
|
50
|
+
expect(result.dockerImage).toBe('python:3.12');
|
|
51
|
+
expect(result.spaceId).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should detect HuggingFace Space URLs with https://huggingface.co/', () => {
|
|
55
|
+
const result = parseImageSource('https://huggingface.co/spaces/user/app');
|
|
56
|
+
expect(result.spaceId).toBe('user/app');
|
|
57
|
+
expect(result.dockerImage).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should detect HuggingFace Space URLs with https://hf.co/', () => {
|
|
61
|
+
const result = parseImageSource('https://hf.co/spaces/user/app');
|
|
62
|
+
expect(result.spaceId).toBe('user/app');
|
|
63
|
+
expect(result.dockerImage).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should detect HuggingFace Space URLs without https prefix', () => {
|
|
67
|
+
const result = parseImageSource('huggingface.co/spaces/user/app');
|
|
68
|
+
expect(result.spaceId).toBe('user/app');
|
|
69
|
+
expect(result.dockerImage).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should detect HuggingFace Space URLs with hf.co shorthand', () => {
|
|
73
|
+
const result = parseImageSource('hf.co/spaces/user/app');
|
|
74
|
+
expect(result.spaceId).toBe('user/app');
|
|
75
|
+
expect(result.dockerImage).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should treat complex docker images as docker images', () => {
|
|
79
|
+
const result = parseImageSource('ghcr.io/owner/repo:tag');
|
|
80
|
+
expect(result.dockerImage).toBe('ghcr.io/owner/repo:tag');
|
|
81
|
+
expect(result.spaceId).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('parseCommand', () => {
|
|
86
|
+
it('should return array commands as-is', () => {
|
|
87
|
+
const result = parseCommand(['python', '-c', 'print("hello")']);
|
|
88
|
+
expect(result.command).toEqual(['python', '-c', 'print("hello")']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should split simple string commands', () => {
|
|
92
|
+
const result = parseCommand('python script.py');
|
|
93
|
+
expect(result.command).toEqual(['python', 'script.py']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle single word commands', () => {
|
|
97
|
+
const result = parseCommand('ls');
|
|
98
|
+
expect(result.command).toEqual(['ls']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle double-quoted strings', () => {
|
|
102
|
+
const result = parseCommand('python -c "print(\'Hello world!\')"');
|
|
103
|
+
expect(result.command).toEqual(['python', '-c', "print('Hello world!')"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle single-quoted strings', () => {
|
|
107
|
+
const result = parseCommand("python -c 'print(\"Hello world!\")'");
|
|
108
|
+
expect(result.command).toEqual(['python', '-c', 'print("Hello world!")']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle escaped quotes', () => {
|
|
112
|
+
const result = parseCommand('echo "He said \\"hello\\""');
|
|
113
|
+
expect(result.command).toEqual(['echo', 'He said "hello"']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should handle mixed quotes and spaces', () => {
|
|
117
|
+
const result = parseCommand('python -c "print(\'hello world\')"');
|
|
118
|
+
expect(result.command).toEqual(['python', '-c', "print('hello world')"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle multiple quoted arguments', () => {
|
|
122
|
+
const result = parseCommand('cmd "arg one" "arg two" "arg three"');
|
|
123
|
+
expect(result.command).toEqual(['cmd', 'arg one', 'arg two', 'arg three']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle quoted strings with no spaces', () => {
|
|
127
|
+
const result = parseCommand('echo "hello"');
|
|
128
|
+
expect(result.command).toEqual(['echo', 'hello']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle empty quotes', () => {
|
|
132
|
+
const result = parseCommand('echo "" test');
|
|
133
|
+
expect(result.command).toEqual(['echo', '', 'test']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle real-world example from docs', () => {
|
|
137
|
+
const result = parseCommand('python -c "print(\'Hello world!\')"');
|
|
138
|
+
expect(result.command).toEqual(['python', '-c', "print('Hello world!')"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle commands with multiple spaces', () => {
|
|
142
|
+
const result = parseCommand('python script.py --arg');
|
|
143
|
+
expect(result.command).toEqual(['python', 'script.py', '--arg']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle commands with tabs', () => {
|
|
147
|
+
const result = parseCommand('python\tscript.py\t--arg');
|
|
148
|
+
expect(result.command).toEqual(['python', 'script.py', '--arg']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle mixed whitespace', () => {
|
|
152
|
+
const result = parseCommand('cmd \t arg1\t\t arg2');
|
|
153
|
+
expect(result.command).toEqual(['cmd', 'arg1', 'arg2']);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle backslash escaping (POSIX shell semantics)', () => {
|
|
157
|
+
// In POSIX shells, \n in double quotes is literal, not a newline
|
|
158
|
+
const result = parseCommand('echo "hello\\nworld"');
|
|
159
|
+
expect(result.command).toEqual(['echo', 'hello\\nworld']);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle backslash literally in single quotes', () => {
|
|
163
|
+
const result = parseCommand("echo 'hello\\nworld'");
|
|
164
|
+
expect(result.command).toEqual(['echo', 'hello\\nworld']);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should throw error for shell operators', () => {
|
|
168
|
+
expect(() => parseCommand('echo hello | grep world')).toThrow(/Unsupported shell syntax/);
|
|
169
|
+
expect(() => parseCommand('ls && pwd')).toThrow(/Unsupported shell syntax/);
|
|
170
|
+
expect(() => parseCommand('cat file > output.txt')).toThrow(/Unsupported shell syntax/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw error for empty command', () => {
|
|
174
|
+
expect(() => parseCommand('')).toThrow(/Command cannot be empty/);
|
|
175
|
+
expect(() => parseCommand(' ')).toThrow(/Command cannot be empty/);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle environment variables as literal references', () => {
|
|
179
|
+
// shell-quote parses $HOME as an env token, which we format as literal string
|
|
180
|
+
// This preserves the variable reference for the job runtime to handle
|
|
181
|
+
const result = parseCommand('echo $HOME');
|
|
182
|
+
expect(result.command).toEqual(['echo', '$HOME']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should preserve complex environment variable syntax', () => {
|
|
186
|
+
const result = parseCommand('echo ${FOO:-bar} ${BAR?missing}');
|
|
187
|
+
expect(result.command).toEqual(['echo', '${FOO:-bar}', '${BAR?missing}']);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should retain special parameter references', () => {
|
|
191
|
+
const result = parseCommand('echo $$ $1 $@ $* $- $! $_');
|
|
192
|
+
expect(result.command).toEqual(['echo', '$$', '$1', '$@', '$*', '$-', '$!', '$_']);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should treat escaped dollars as literal variables', () => {
|
|
196
|
+
const result = parseCommand('echo \\$FOO "$BAR"');
|
|
197
|
+
expect(result.command).toEqual(['echo', '$FOO', '$BAR']);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle multiline Python in array format', () => {
|
|
201
|
+
const result = parseCommand(['python', '-c', 'import sys\nprint("hello")\nprint("world")']);
|
|
202
|
+
expect(result.command).toEqual(['python', '-c', 'import sys\nprint("hello")\nprint("world")']);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('createJobSpec', () => {
|
|
207
|
+
it('should create a basic job spec', () => {
|
|
208
|
+
const spec = createJobSpec({
|
|
209
|
+
image: 'python:3.12',
|
|
210
|
+
command: 'python -c "print(123)"',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(spec.dockerImage).toBe('python:3.12');
|
|
214
|
+
expect(spec.command).toEqual(['python', '-c', 'print(123)']);
|
|
215
|
+
expect(spec.flavor).toBe('cpu-basic');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle Space URLs', () => {
|
|
219
|
+
const spec = createJobSpec({
|
|
220
|
+
image: 'hf.co/spaces/user/app',
|
|
221
|
+
command: 'python run.py',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(spec.spaceId).toBe('user/app');
|
|
225
|
+
expect(spec.dockerImage).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should include environment variables', () => {
|
|
229
|
+
const spec = createJobSpec({
|
|
230
|
+
image: 'python:3.12',
|
|
231
|
+
command: 'python script.py',
|
|
232
|
+
env: { FOO: 'bar', BAZ: 'qux' },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(spec.environment).toEqual({ FOO: 'bar', BAZ: 'qux' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should include secrets', () => {
|
|
239
|
+
const spec = createJobSpec({
|
|
240
|
+
image: 'python:3.12',
|
|
241
|
+
command: 'python script.py',
|
|
242
|
+
secrets: { API_KEY: 'secret123' },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(spec.secrets).toEqual({ API_KEY: 'secret123' });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should keep HF_TOKEN literal when no expansion token provided', () => {
|
|
249
|
+
const spec = createJobSpec({
|
|
250
|
+
image: 'python:3.12',
|
|
251
|
+
command: 'echo $HF_TOKEN',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(spec.command).toEqual(['echo', '$HF_TOKEN']);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should keep command literal even when hfToken provided', () => {
|
|
258
|
+
const spec = createJobSpec({
|
|
259
|
+
image: 'python:3.12',
|
|
260
|
+
command: 'echo $HF_TOKEN',
|
|
261
|
+
hfToken: 'hf_secret_999',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(spec.command).toEqual(['echo', '$HF_TOKEN']);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should inject HF_TOKEN into secrets when placeholder provided', () => {
|
|
268
|
+
const spec = createJobSpec({
|
|
269
|
+
image: 'python:3.12',
|
|
270
|
+
command: 'python script.py',
|
|
271
|
+
secrets: { HF_TOKEN: '$HF_TOKEN', OTHER: 'keep' },
|
|
272
|
+
hfToken: 'hf_secret_123',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(spec.secrets).toEqual({ HF_TOKEN: 'hf_secret_123', OTHER: 'keep' });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should inject HF_TOKEN into env when placeholder provided', () => {
|
|
279
|
+
const spec = createJobSpec({
|
|
280
|
+
image: 'python:3.12',
|
|
281
|
+
command: 'python script.py',
|
|
282
|
+
env: { HF_TOKEN: '${HF_TOKEN}', NAME: 'demo' },
|
|
283
|
+
hfToken: 'hf_env_456',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(spec.environment).toEqual({ HF_TOKEN: 'hf_env_456', NAME: 'demo' });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should leave other env values unchanged', () => {
|
|
290
|
+
const spec = createJobSpec({
|
|
291
|
+
image: 'python:3.12',
|
|
292
|
+
command: 'python script.py',
|
|
293
|
+
env: { NAME: 'demo' },
|
|
294
|
+
secrets: { API_KEY: 'secret123' },
|
|
295
|
+
hfToken: 'hf_env_456',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(spec.environment).toEqual({ NAME: 'demo' });
|
|
299
|
+
expect(spec.secrets).toEqual({ API_KEY: 'secret123' });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should parse and include timeout', () => {
|
|
303
|
+
const spec = createJobSpec({
|
|
304
|
+
image: 'python:3.12',
|
|
305
|
+
command: 'python script.py',
|
|
306
|
+
timeout: '5m',
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(spec.timeoutSeconds).toBe(300);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should use specified flavor', () => {
|
|
313
|
+
const spec = createJobSpec({
|
|
314
|
+
image: 'python:3.12',
|
|
315
|
+
command: 'python script.py',
|
|
316
|
+
flavor: 'a10g-small',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(spec.flavor).toBe('a10g-small');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should handle array commands', () => {
|
|
323
|
+
const spec = createJobSpec({
|
|
324
|
+
image: 'ubuntu',
|
|
325
|
+
command: ['bash', '-c', 'echo hello'],
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(spec.command).toEqual(['bash', '-c', 'echo hello']);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|