Repository navigation
Description
A FILE instrument can be assigned remotely, but a remote assignment has no way to deliver the files. The patient picks files on the gateway and submits. The gateway's handleSubmit sends only result.data and drops the uploadMap, so the files go nowhere. When the synchronizer imports the submission, InstrumentRecordsService.create treats it like any other file record, with two outcomes:
- Storage enabled: the record is written with
pending: trueand never leaves that state, because nothing will ever callupload-completefor it. The assignment is markedCOMPLETE, but the record is hidden from every view that filters out pending records (the subject's table and graph, the subject hub counts). The clinician sees a completed assignment with no data, and the patient's files are lost. - Storage disabled:
createthrowsServiceUnavailableException. The synchronizer rolls back, keeps the assignmentOUTSTANDING, and repeats the same create-session, fail, delete-session cycle on every pass (every 10 s by default) indefinitely.
Bulk upload already refuses FILE instruments for the same reason (instrument-records.service.ts:495-502: "Refuse it rather than write a record that can only ever be pending"). The assignment paths have no such check, and the web pickers offer FILE instruments for remote assignment.
Where
apps/api/src/assignments/assignments.service.ts:274-285: the only instrument checks are the group's opt-in list and archival:
const accessibleInstrumentIds = new Set(group.accessibleInstrumentIds);
const instrumentIds = timepoints.map(({ instrumentId }) => instrumentId);
const archivedInstrumentIds = new Set(await this.findArchivedInstrumentIds(instrumentIds));
const unavailableInstrumentIds = [
...new Set(instrumentIds.filter((id) => !accessibleInstrumentIds.has(id) || archivedInstrumentIds.has(id)))
];apps/gateway/src/Root.tsx:64-69: a scalar result's files are not sent:
} else if (target.kind !== 'SERIES') {
updateData = {
data: result.data,
kind: 'SCALAR',
status: 'COMPLETE'
};apps/api/src/instrument-records/instrument-records.service.ts:93-97 and :140: the import then either throws or writes a pending record:
if (instrument.kind === 'FILE' && !this.storageService.isEnabled) {
throw new ServiceUnavailableException(/* ... */);
}
// ...
pending: instrument.kind === 'FILE',apps/web/src/utils/administrable-instruments.ts:15-25 (used by /session/remote-assignment and /group/remote-assignments) does not filter on kind.
Reproduce
- With storage enabled, start a session and open Remote Assignment. Pick a file instrument and create the assignment.
- Open the link, attach a file, submit, and wait one
GATEWAY_REFRESH_INTERVAL.
Actual: the assignment shows COMPLETE, the subject has no new record in the datahub, no file was uploaded, and an InstrumentRecord with pending: true stays in the database. With storage disabled, the assignment stays OUTSTANDING, and the API logs Failed to synchronize remote assignment on every pass.
Expected: a file instrument cannot be assigned remotely. POST /v1/assignments and /v1/assignments/bulk refuse it with INSTRUMENT_UNAVAILABLE, and the remote-assignment pickers do not list it.
Tests
apps/api/src/assignments/__tests__/assignments.service.test.ts:it('should refuse to assign a file instrument, since the gateway has no way to upload its files')for bothcreateandcreateBulk.apps/web/src/utils/__tests__/administrable-instruments.test.ts(or a test for a new remote-assignment selector): aFILEinstrument is excluded from the remote-assignment list but still offered for in-person sessions.testing/src/specs/remote-assignment.spec.ts: a file instrument is not offered on the Remote Assignment page, andPOST /v1/assignmentsfor one returns 422.
Suggested fix
In resolveBulkRequest and the group-less branch of create, look up the kind of each requested instrument and report FILE instruments (and series containing a FILE item, which the series renderer cannot show either) as INSTRUMENT_UNAVAILABLE. Filter the same kinds out of the two remote-assignment pickers in apps/web.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e). No open PR touchesassignments.service.tsoradministrable-instruments.ts.Where the kind check goes
Instrumentstores nokind(apps/api/prisma/schema.prisma:212-235), so it is only known after the bundle is evaluated.AssignmentsServicedoes not depend onInstrumentsService, and reads theInstrumentmodel only infindArchivedInstrumentIds(assignments.service.ts:244-250).InstrumentsService.findByIdwon't do: it throwsNotFoundExceptionon an unknown id (instruments.service.ts:371), andresolveBulkRequesthas to keep answering an unknown id withINSTRUMENT_UNAVAILABLE(testing/src/specs/bulk-remote-assignments.spec.ts:140).Plan: replace
findArchivedInstrumentIdswith oneInstrumentsServicemethod, e.g.findUnassignableIds(ids). It selectsarchivedAt,bundleandidfor the ids, evaluates each row withgetInstrumentInstance, and returns the ids that are archived orFILE. A series bundle does not say what kind its items are, so for a series it also mapsgetSeriesInstrumentItemsthroughgenerateScalarInstrumentIdand evaluates those items. Unknown ids are skipped, not thrown. Both call sites (:88and:279) then get the rule.AssignmentsModuleimportsInstrumentsModule, which is no cycle becauseInstrumentsModuleimports onlyVirtualizationModule. Once #1408 storeskind, the scalar half becomes awhere.A series with a FILE item is broken in person too
validateSeriesInstrument(instruments.service.ts:799-839) checks the item count and that each item exists, never its kind, andSeriesInstrumentRendererrendersnullfor aFILEitem (SeriesInstrumentRenderer.tsx:225). RefusingFILEitems there is the structural fix. It selects onlyidtoday, so it would have to evaluate the items too. The assignment check still needs its series branch for series already stored. The web series builder is passed only forms and interactive instruments (group/manage.tsx:599), so #1774's new FILE bucket must stay out of that prop.Web
selectAdministrableInstrumentsalso feeds the in-person picker (instruments/accessible-instruments.tsx:35), so the filter cannot go inside it. Add a sibling that also dropskind === 'FILE', and use it atsession/remote-assignment.tsx:131andgroup/remote-assignments.tsx:79. Leave series to the API.seriesItemsis{ id }[](instrument.base.ts:218), and/v1/instruments/inforeturns only the latest edition of each scalar by default (instruments.service.ts:546,:613-620), so an item pinned to an older edition has no kind to look up on the client.Existing assignments
The fix leaves them alone. An outstanding
FILEassignment can be cancelled from the subject's Assignments tab; cancelling deletes the gateway copy, which also ends the retry loop when storage is off. A completed one left apending: truerecord whose data is{}(FileInstrumentContent/store.ts:78), so nothing in it can be recovered.Tests
instruments.service.test.ts: the new method reports archived,FILEand FILE-containing series, and skips unknown ids.assignments.service.test.ts: the two archived-series cases (:171,:291) mockinstrumentModel.findManytoday and move to a mockedInstrumentsService. Cover theFILErefusal in the group-less branch ofcreateas well as throughresolveBulkRequest.administrable-instruments.test.ts:FILEis dropped from the remote list and kept in the in-person list.groupId) inbulk-remote-assignments.spec.ts, usingapi.findInstrumentId('FILE').api.createGroupgrants every instrument,FILEincluded, so the 422 comes from the new rule and not the opt-in list. ItsinstrumentIdsForhelper (:46-55) already keeps onlyFORMandINTERACTIVE, so the existing cases are unaffected.remote-assignment.spec.tschecks the picker, andaccessible-instruments.spec.tschecks that Arbitrary File Upload is still offered in person.Triage note written with Claude Code. Code references point at
main@d86740e.fix(api): refuse remote assignment of file instruments
fix(web): hide file instruments from remote assignment
test(testing): cover refusing remote assignment of file instruments