test(persistence): cover sensitive-arg guard on lazily-loaded extension types (swamp-club#484)#1471
Conversation
…on types (swamp-club#484) Follow-up to swamp-club#480 (PR #1469). That fix added a chokepoint in YamlDefinitionRepository.save() that refuses a literal value for a `{ sensitive: true }` global argument, resolving the type schema via the model registry. For an extension type the registry only lazy-registers it, and `model edit` never pre-loads the registry, so modelRegistry.get(type) returned undefined, the guard was silently skipped, and a literal secret leaked to disk. The fix promotes the lazy type (ensureTypeLoaded + an ensureLoaded fallback) before the guard — but no automated test covered it: existing tests register types via defineModel, which eagerly populates the registry, so the lazy-resolution fallback was never exercised. Add an integration test that source-mounts a real extension model (loaded lazily via the bundle catalog, not defineModel) with a sensitive global argument, seeds a definition holding a safe vault.get() expression, then drives `model edit` via stdin to swap in a literal secret — asserting the write is refused with the specific sensitive-arg error and nothing leaks to disk. Reverting the #1469 fix makes this test fail (edit exits 0, literal leaks), confirming it is a genuine regression guard. Also add optional stdin support to the runCliCommand integration helper (additive; existing callers unaffected) so the test can drive `model edit`. No production code changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Clean, well-structured test-only PR that closes an important coverage gap for the secrets-at-rest guard on lazily-loaded extension types. No production code changes.
Blocking Issues
None.
Suggestions
-
Minor refactor outside task scope (
integration/test_helpers.ts): HoistingTextDecoderinto a sharedconst decoderalso touches the non-stdin code path (lines 120–121). CLAUDE.md asks to keep blast radius small. This is harmless and arguably cleaner, but worth noting it slightly exceeds "only touch what's necessary." -
Large docblock (
integration/model_edit_sensitive_lazy_test.ts:21–45): The module-level JSDoc is ~25 lines. CLAUDE.md prefers minimal comments ("one short line max"). That said, this matches the established pattern in sibling tests (model_create_sensitive_test.ts,lazy_registry_promotion_test.ts), and the context it provides for a security regression test is genuinely useful — so this is fine in practice.
Overall: the test design is solid — source-mounting a real extension (not defineModel) forces genuine lazy resolution, the precondition check (line 131–136) confirms the extension actually loaded, and the leak scan (readAllFiles over the entire repo dir) is thorough. The runCliCommand stdin addition is backward-compatible and well-documented. Approving.
Summary
Test-only follow-up to swamp-club#480 (PR #1469). That fix added a chokepoint in
YamlDefinitionRepository.save()that refuses a literal value for a{ sensitive: true }global argument. It resolves the type schema via the model registry — and for an extension type the registry only lazy-registers it.model editnever pre-loads the registry, somodelRegistry.get(type)returnedundefined, the guard was silently skipped, and a literal secret leaked to disk. The fix promotes the lazy type (ensureTypeLoaded+ anensureLoadedfallback) before the guard.No automated test covered that path. Existing unit/integration tests register types via
defineModel, which eagerly populates the registry, soget()succeeds and the lazy-resolution fallback is never exercised. The defect surfaced only in manual clean-room verification.This PR closes the gap:
integration/model_edit_sensitive_lazy_test.ts— source-mounts a real extension model (loaded lazily via the bundle catalog, notdefineModel) with a sensitive global argument, seeds a definition holding a safevault.get()expression, then drivesmodel editvia stdin to swap in a literal secret. Asserts the write is refused with the specific sensitive-arg error and that nothing leaks to disk.integration/test_helpers.ts—runCliCommandgains an optionalstdin?parameter (additive; existing callers unaffected) so the test can drivemodel edit.No production code changes.
Test Plan
runCliCommandconsumer (source_extension_rebundle_test.ts) still passes — helper change is backward-compatible.deno check,deno lint,deno fmt --check,license-headersall clean.🤖 Generated with Claude Code