fix(validation): load lazy types before cross-model expression checks#1166
fix(validation): load lazy types before cross-model expression checks#1166
Conversation
validateModelPathReference called modelRegistry.get() without first awaiting ensureTypeLoaded(), so cross-model CEL expressions referencing lazy-registered types failed with a misleading "Unknown model type" error even though the type was registered and worked at execution time. Missed call site from PR #1063 (lazy per-bundle loading) — the execution path was wired up but the validation path was not. ensureTypeLoaded is a no-op on already-loaded types, so the perf win from lazy loading is preserved. Closes swamp-club #89. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Clean, well-scoped fix for a missed ensureTypeLoaded call site from lazy per-bundle loading (#1063). The one-liner production change is correct — ensureTypeLoaded is already the established pattern (used in method_execution_service.ts:646) and is a no-op for already-loaded types, so there's no performance regression.
Blocking Issues
None.
Suggestions
None — this is a model fix. The regression test is thorough (asserts both the result and the mechanism, uses a unique type to avoid global singleton collision, and cleans up the type loader in a finally block). Production change is minimal and correctly placed. LGTM.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None.
Medium
- Test singleton pollution (
validation_service_test.ts:1022-1074): The test callsmodelRegistry.registerLazy()and thenpromoteFromLazy()on the global singleton, permanently adding@test/issue-89-lazy-regressiontomodelRegistry.models. Thefinallyblock restores the type loader but cannot clean up the registered model (nounregistermethod exists). If this test were re-run in the same process (e.g.,--watchmode), the second run would fail:ensureTypeLoadedwould see the type already inthis.models, return immediately without calling the loader, and theloaderCalledassertion at line 1068 would fail. In normal CI this is a non-issue since tests run once, but it's worth noting as a fragility if the registry ever gains areset()for test isolation.
Low
- Unhandled loader errors propagate as crashes (
validation_service.ts:706): IfensureTypeLoadedthrows (e.g., corrupt bundle file, transient I/O error), the error propagates throughPromise.allat line 566 and causesvalidateModelto throw instead of returning a structured validation result. However, this is consistent with the execution path (method_execution_service.ts:646) which also lets loader errors propagate, and transient I/O errors are a reasonable thing to surface as exceptions rather than validation failures. Not a regression introduced by this PR.
Verdict
PASS — The fix is a correct one-liner (await modelRegistry.ensureTypeLoaded(result.type)) placed immediately before the existing modelRegistry.get() call in validateModelPathReference. It follows the exact same pattern already established in the execution path (method_execution_service.ts:646). ensureTypeLoaded is a no-op for already-loaded types, so there is no regression for non-lazy types and the perf win from #1063 is preserved. The regression test is well-designed: it simulates the lazy state by using registerLazy directly (rather than defineModel which eagerly registers), asserts both that validation passes AND that the loader was actually invoked (pinning the fix to ensureTypeLoaded being called, not to the type happening to be loaded already).
Summary
validateModelPathReferencecalledmodelRegistry.get()without first awaitingensureTypeLoaded(). For a type that is catalog-known but not yet imported (lazy-registered),has(type)returnstruewhileget(type)returnsundefined, so cross-model CEL expressions like${{ model.llm.definition.globalArguments.ollamaUrl }}failed validation with a misleadingUnknown model type "@keeb/ollama"error — even though the type was registered and worked at execution time (because the execution path already awaitsensureTypeLoaded).This is a missed call site from #1063 (lazy per-bundle loading). The execution path was wired up; the validation path was not. The fix is the one-liner suggested in the issue — an
await modelRegistry.ensureTypeLoaded(result.type)before the existingget()call.ensureTypeLoadedis a no-op on already-loaded types, so the perf win from #1063 is fully preserved.cc @Paulstack — this is the missed call site from #1063 (lazy per-bundle loading).
Closes swamp-club #89.
Test plan
validateModel loads lazy types before resolving cross-model referencesinvalidation_service_test.ts. Registers a unique type viaregisterLazy, wires asetTypeLoaderthatpromoteFromLazys when invoked, and asserts both that the Expression paths check passes and that the type loader was actually called (pinning the behavior to "ensureTypeLoaded was awaited" rather than "the type happened to already be loaded").git stashthe fix → test fails with the exact "Unknown model type" error path; restore → passes).deno check— cleandeno lint— cleandeno fmt— cleandeno run test— 4297 passed, 0 faileddeno run compile— binary rebuilt🤖 Generated with Claude Code