From acc868c6fd3bbb22dd85bc1919696727a8333634 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Sun, 3 May 2026 12:00:08 -0400 Subject: [PATCH] fix(tapis): skip FIXED inputs in job submission FIXED inputs are locked by the app definition; Tapis injects them from the app at submission. createJobFileInputsFromSeed previously required every app fileInput to have a matching model.input_files entry, throwing "Component input not found" for FIXED inputs since they are not exposed to the model component. Skip FIXED inputs in the job request (Tapis fills them in). --- src/classes/tapis/adapters/TapisJobService.ts | 7 ++++ .../tapis/adapters/tests/fixtures/app.ts | 42 +++++++++++++++++++ src/classes/tapis/adapters/tests/jobs.test.ts | 40 +++++++++++++++++- 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/classes/tapis/adapters/TapisJobService.ts b/src/classes/tapis/adapters/TapisJobService.ts index c26da5b..b2c9f9f 100644 --- a/src/classes/tapis/adapters/TapisJobService.ts +++ b/src/classes/tapis/adapters/TapisJobService.ts @@ -107,6 +107,13 @@ export class TapisJobService { ): Jobs.JobFileInput[] { const jobInputs = app.jobAttributes?.fileInputs?.flatMap((fileInput) => { + if (fileInput.inputMode === Apps.FileInputModeEnum.Fixed) { + // FIXED inputs are locked by the app definition; Tapis injects + // them from the app at submission. No model component input or + // user-provided dataset is required. + return []; + } + const modelInput = model.input_files.find((input) => input.name === fileInput.name); if (!modelInput) { diff --git a/src/classes/tapis/adapters/tests/fixtures/app.ts b/src/classes/tapis/adapters/tests/fixtures/app.ts index ec26a8d..0e3c3ef 100644 --- a/src/classes/tapis/adapters/tests/fixtures/app.ts +++ b/src/classes/tapis/adapters/tests/fixtures/app.ts @@ -213,3 +213,45 @@ export const appWithUnknownRequiredInput: Apps.TapisApp = { ] } } as Apps.TapisApp; + +export const appWithFixedInput: Apps.TapisApp = { + ...baseApp, + jobAttributes: { + ...baseApp.jobAttributes, + fileInputs: [ + { + name: "Fixed Input", + description: "Locked by app, user cannot override", + inputMode: "FIXED", + autoMountLocal: true, + sourceUrl: "tapis://ls6/home/${apiUserId}/fixed-input.txt", + targetPath: "fixed.txt" + } + ] + } +} as Apps.TapisApp; + +export const appWithMixedInputs: Apps.TapisApp = { + ...baseApp, + jobAttributes: { + ...baseApp.jobAttributes, + fileInputs: [ + { + name: "optional_file", + description: "An optional supplementary input file", + inputMode: "OPTIONAL", + autoMountLocal: true, + sourceUrl: null, + targetPath: "optional.dat" + }, + { + name: "Fixed Input", + description: "Locked by app, user cannot override", + inputMode: "FIXED", + autoMountLocal: true, + sourceUrl: "tapis://ls6/home/${apiUserId}/fixed-input.txt", + targetPath: "fixed.txt" + } + ] + } +} as Apps.TapisApp; diff --git a/src/classes/tapis/adapters/tests/jobs.test.ts b/src/classes/tapis/adapters/tests/jobs.test.ts index 8b6ab85..fdcb0f9 100644 --- a/src/classes/tapis/adapters/tests/jobs.test.ts +++ b/src/classes/tapis/adapters/tests/jobs.test.ts @@ -1,5 +1,10 @@ import seeds from "./fixtures/seeds"; -import app, { appWithOptionalInput, appWithUnknownRequiredInput } from "./fixtures/app"; +import app, { + appWithOptionalInput, + appWithUnknownRequiredInput, + appWithFixedInput, + appWithMixedInputs +} from "./fixtures/app"; import model, { modelWithOptionalInput } from "./fixtures/model"; import jobFileInputsExpected from "./expected/jobFileInputs"; import { expectedJobParameterSetNonDefault } from "./expected/jobParameterSet"; @@ -62,6 +67,39 @@ test("throws when app fileInput name is not found in model.input_files", () => { ).toThrow("Component input not found"); }); +test("FIXED input is skipped — Tapis injects sourceUrl from app definition", () => { + const jobService = new TapisJobService( + new Jobs.JobsApi(), + new Jobs.SubscriptionsApi(), + new Jobs.ShareApi() + ); + const seedNoDatasets = { ...seeds[0], datasets: {} }; + expect(() => + jobService.createJobFileInputsFromSeed(seedNoDatasets, appWithFixedInput, model) + ).not.toThrow(); + const jobInputs = jobService.createJobFileInputsFromSeed( + seedNoDatasets, + appWithFixedInput, + model + ); + expect(jobInputs.find((i) => i.name === "Fixed Input")).toBeUndefined(); + expect(jobInputs).toHaveLength(0); +}); + +test("FIXED input alongside OPTIONAL input — both safely omitted when unbound", () => { + const jobService = new TapisJobService( + new Jobs.JobsApi(), + new Jobs.SubscriptionsApi(), + new Jobs.ShareApi() + ); + const jobInputs = jobService.createJobFileInputsFromSeed( + seedWithMissingOptionalInput, + appWithMixedInputs, + modelWithOptionalInput + ); + expect(jobInputs).toHaveLength(0); +}); + test("optional input with datasets present is included", () => { const jobService = new TapisJobService( new Jobs.JobsApi(),