Skip to content

Commit f4faaf1

Browse files
fix(run): allow caller-supplied taskUUID instead of always generating (#66)
The API supports caller-provided task identifiers but the CLI rejected taskUUID as a reserved field. Demote taskUUID from Protected to auto-only so it is still excluded from completions and validation but no longer blocked as a key=value argument. If the caller supplies an invalid UUID, Run returns an error before submitting. Closes RUN-10706 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent cbe0e78 commit f4faaf1

4 files changed

Lines changed: 90 additions & 10 deletions

File tree

internal/api/client.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ func (c *Client) submit(ctx context.Context, payload map[string]any) ([]json.Raw
132132
// args is a slice of key=value strings (e.g. ["positivePrompt=hello", "width=1024"]).
133133
// They are parsed against the model's fetched JSON Schema so that type coercion
134134
// (string vs number vs bool) is schema-driven rather than best-effort.
135-
// System fields (taskType, taskUUID, deliveryMethod) are injected automatically;
136-
// callers must not include them in rawArgs.
135+
// System fields (taskType) are injected automatically; callers must not include it in args.
136+
// deliveryMethod may be set via --delivery-method or a key=value argument.
137+
// taskUUID is optional: supply it to use a specific identifier, or omit it to have one generated.
137138
//
138139
// For async delivery Run polls until a success result is received or the context
139140
// is cancelled. For sync delivery the submit response is returned directly.
@@ -184,7 +185,7 @@ func (c *Client) Run(ctx context.Context, model string, args []string, opts RunO
184185
}
185186

186187
// Parse args against the real schema so type coercion is schema-driven.
187-
// Protected fields (taskType, taskUUID, model, deliveryMethod) are rejected here.
188+
// Protected fields are rejected here.
188189
payload := make(map[string]any, len(args)+4)
189190
payload[fieldModel] = model
190191
for _, a := range args {
@@ -222,8 +223,21 @@ func (c *Client) Run(ctx context.Context, model string, args []string, opts RunO
222223
payload[fieldDeliveryMethod] = deliveryMethod
223224
}
224225

225-
// Inject system fields.
226-
taskUUID := uuid.New()
226+
// Inject system fields. Use a caller-supplied taskUUID if provided, otherwise generate.
227+
var taskUUID uuid.UUID
228+
if raw, ok := payload[fieldTaskUUID]; ok {
229+
s, ok := raw.(string)
230+
if !ok {
231+
return nil, fmt.Errorf("taskUUID: expected string UUID, got %T", raw)
232+
}
233+
parsed, err := uuid.Parse(s)
234+
if err != nil {
235+
return nil, fmt.Errorf("taskUUID: invalid UUID %q: %w", s, err)
236+
}
237+
taskUUID = parsed
238+
} else {
239+
taskUUID = uuid.New()
240+
}
227241
payload[fieldTaskType] = taskType
228242
payload[fieldTaskUUID] = taskUUID
229243

internal/api/run_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,70 @@ func TestClientRun_RawArgs_InvalidKV(t *testing.T) {
401401
t.Errorf("expected 0 transport calls, got %d", mock.callCount)
402402
}
403403
}
404+
405+
// TestClientRun_UserProvidedTaskUUID: a caller-supplied taskUUID must be accepted and used
406+
// (after UUID validation) instead of being overwritten with a new UUID.
407+
func TestClientRun_UserProvidedTaskUUID(t *testing.T) {
408+
const wantUUID = "d58cbacc-bed6-413e-9e4a-b118e4d84035"
409+
410+
srv := inferenceSchemaServer(t, requestSchemaWithTaskType("imageInference", "sync"))
411+
412+
mock := &mockTransport{
413+
responses: []mockResponse{
414+
{data: []json.RawMessage{successItem(t, nil)}},
415+
},
416+
}
417+
418+
c := NewClient(mock, slog.Default())
419+
c.schemaBaseURLOverride = srv.URL + "/"
420+
421+
_, err := c.Run(context.Background(), testModelAIR, []string{"taskUUID=" + wantUUID}, RunOptions{})
422+
if err != nil {
423+
t.Fatalf("unexpected error: %v", err)
424+
}
425+
426+
if len(mock.captured) == 0 {
427+
t.Fatal("no transport calls captured")
428+
}
429+
tasks := mock.captured[0]
430+
if len(tasks) == 0 {
431+
t.Fatal("submitted tasks slice is empty")
432+
}
433+
taskBytes, err := json.Marshal(tasks[0])
434+
if err != nil {
435+
t.Fatalf("marshal captured task: %v", err)
436+
}
437+
var payload map[string]any
438+
if err := json.Unmarshal(taskBytes, &payload); err != nil {
439+
t.Fatalf("unmarshal captured task: %v", err)
440+
}
441+
got, ok := payload["taskUUID"]
442+
if !ok {
443+
t.Fatal("taskUUID not present in submitted payload")
444+
}
445+
if s, _ := got.(string); s != wantUUID {
446+
t.Errorf("taskUUID: got %q, want %q", s, wantUUID)
447+
}
448+
}
449+
450+
// TestClientRun_UserProvidedTaskUUID_Invalid: an invalid UUID value must return
451+
// an error before any transport call is made.
452+
func TestClientRun_UserProvidedTaskUUID_Invalid(t *testing.T) {
453+
srv := inferenceSchemaServer(t, requestSchemaWithTaskType("imageInference", "sync"))
454+
455+
mock := &mockTransport{}
456+
457+
c := NewClient(mock, slog.Default())
458+
c.schemaBaseURLOverride = srv.URL + "/"
459+
460+
_, err := c.Run(context.Background(), testModelAIR, []string{"taskUUID=not-a-uuid"}, RunOptions{})
461+
if err == nil {
462+
t.Fatal("expected error for invalid taskUUID, got nil")
463+
}
464+
if !strings.Contains(err.Error(), "taskUUID") {
465+
t.Errorf("expected error to mention %q, got: %v", "taskUUID", err)
466+
}
467+
if mock.callCount != 0 {
468+
t.Errorf("expected 0 transport calls, got %d", mock.callCount)
469+
}
470+
}

internal/schema/schema.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ var ManagedFields = map[string]ManagedField{
106106
Hint: "use the --task-type flag instead",
107107
},
108108
"taskUUID": {
109-
Protected: true,
110-
Hint: "this field is system-generated and cannot be set manually",
109+
Protected: false,
111110
},
112111
"deliveryMethod": {
113112
Protected: false,

internal/schema/schema_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -629,10 +629,10 @@ func TestIsProtected_ModelRejected(t *testing.T) {
629629
}
630630
}
631631

632-
func TestIsProtected_TaskUUIDRejected(t *testing.T) {
632+
func TestIsProtected_TaskUUIDAllowed(t *testing.T) {
633633
_, blocked := schema.IsProtected("taskUUID")
634-
if !blocked {
635-
t.Error("expected taskUUID to be protected")
634+
if blocked {
635+
t.Error("taskUUID must not be protected — callers may supply their own task identifier")
636636
}
637637
}
638638

0 commit comments

Comments
 (0)