fix: let the mock's dependency revision observe the Asset store

`createMockStudioGateway`'s default `dependencyRevision.current()` returned a
literal constant and never consulted `dependencies.assets`, so the staleness
guards in `createStudioPreview` and `publishStudioDocument` could not fire for
an asset-store mutation between validate and preview.

The live case: a document references an evidence key with `alt=""` and the
store holds only a `decorative: true` Asset for it, so validation is correctly
VALID with zero issues. A newer `decorative: false` Asset then wins that key.
Preview succeeds, the figure resolves to `decorative: false, alt: ""`, and
publish snapshots it verbatim -- a meaningful image with no accessible name,
validated clean, with nothing anywhere reporting an error.

The default now folds the Asset store into the revision. Each Asset is reduced
to the fields the mock's own validation and projection read -- identity and
resolution order (`id`, `assetKey`, `updatedAt`), resolvability
(`managementStatus`, `publicPath`), the alt rule (`decorative`, `altText`), and
what the published `ResolvedAsset` carries (`mediaType`, `width`, `height`) --
canonicalized with `stableStringify`, sorted, and folded into a 128-bit FNV-1a
digest. Sorting the canonical strings is what makes it order-independent, which
this mock's reproducibility across the suite depends on.

It is a projection rather than the whole record because the excluded fields cost
sensitivity without buying any. `usageCount` is the clearest: it counts
referencing documents, so on a real backend publishing any document that uses an
Asset would invalidate every other author's in-flight validation, while changing
nothing the validator or renderer reads.

An empty store still reports the bare catalog constant -- that is the world the
seeded fixtures were validated against, and `fixtures.ts` now shares the one
definition rather than retyping the literal.

`findResolvableAsset` is untouched: it is a single-point-in-time predicate and
is correct as it stands. Seeing a change *between* two points is the revision's
job. A caller-supplied `dependencyRevision` still wins outright.

The asset-picker test that reached `failureOf`'s `ContentFormatError` branch did
so only because the revision could not move; it now pins its own revision to
keep reaching the projection, and asserts the problem detail so the two 409
paths cannot be confused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-18 18:44:53 +09:00
co-authored by Claude Opus 5
parent cf45bcc7dc
commit d84b57bb3f
6 changed files with 459 additions and 6 deletions
+17 -1
View File
@@ -945,10 +945,23 @@ test("a READY asset whose publicPath satisfies a different key's legacy conventi
// A deterministic content failure between validate and preview is exactly
// what VALIDATION_STALE/409 means, and it is not retryable without
// re-validating first.
//
// The alignment follow-up that taught the *default* dependency revision to
// read the Asset store closed exactly the gap this test used to reach the
// projection through: deleting the Asset now moves the revision, so the guard
// fires first and `failureOf`'s ContentFormatError branch would never run
// again. That branch still has to hold -- a projection can refuse for reasons
// the revision cannot see (a directive the parser rejects against a catalog
// row) -- so this test pins the revision itself and keeps reaching the
// projection, and asserts the *detail* to prove which of the two paths
// produced the 409. `mock-dependency-revision.test.ts` covers the guard path.
test("idempotent() maps a deterministic content failure to VALIDATION_STALE, never lets a raw error escape the port", async () => {
const asset = assetFixture({ assetKey: "race-check" });
const assets = new Map<string, Asset>([[asset.assetKey, asset]]);
const gateway = createMockStudioGateway({ assets });
const gateway = createMockStudioGateway({
assets,
dependencyRevision: { current: () => "pinned-so-the-guard-cannot-fire" },
});
const created = await gateway.createDocument(
{
@@ -986,6 +999,9 @@ test("idempotent() maps a deterministic content failure to VALIDATION_STALE, nev
assert.equal(error.code, "VALIDATION_STALE");
assert.equal(error.status, 409);
assert.equal(error.retryable, false);
// The projection's failure, wrapped by `failureOf` -- not the guard's
// own "Current validation without errors is required."
assert.match(error.problem.detail ?? "", /no longer renders against current dependencies/);
return true;
};