fix: un-collapse evidence gate 1 from gate 2 to close a raw-Error escape
Fix round 2 (review of 9864958):
Round 1's I2 fix made gate 1 (supportsEvidenceKeyIn) read the same
merged catalog gate 2 already checks, so gate 1 stopped asking "does a
real Asset (or the legacy key) back this key" and started asking the
identical question gate 2 asks ("is there any EVIDENCE catalog row for
it"). A real EVIDENCE catalog row that exists for something other than
media (fixtures.ts's row for a QUESTION resolution target) then passed
gate 1 with nothing backing it as evidence. validateDocument reported
VALID; createPreview's resolver -- which only ever knew the legacy key
and real Assets -- threw a raw, unwrapped Error, violating the port's
StudioGatewayError-only contract. I2's disagreement reproduced in the
opposite direction.
Rebuilt gate 1 in all three callers (validate-working-copy.ts,
adapters/mock/project-public-render-model.ts, instant-preview.tsx) as
"legacy key OR an Asset-derived catalog entry only" -- never the
merged document catalog -- while gate 2 keeps reading the merged
catalog as before. Also stopped emitting the real Asset UUID as the
synthesized CatalogEntry's id (prefixed instead), closing a path where
pasting an Asset's real id -- never something the Picker itself
produces -- would have resolved as an evidence key.
Restored content-format.test.ts's domain tests to exercise the same
gate-1 composition production callers now use instead of a bare
hand-injected predicate, and added coverage for: a real non-asset
EVIDENCE row still being rejected, an Asset-backed key being accepted
with no document-catalog row at all, the fixture-UUID case failing
both mock paths with no raw Error crossing the gateway port, and the
EVIDENCE_ALT_REQUIRED decorative-Asset pairing that had no test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
98649585e6
commit
7ff9728a5c
@@ -1,13 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isSupportedEvidenceKey } from "../../../src/features/tech-log/adapters/static/evidence-assets.ts";
|
||||
import { evidenceCatalogEntriesFromAssets } from "../../../src/features/tech-log/domain/content-format/asset-evidence-catalog.ts";
|
||||
import {
|
||||
ContentFormatError,
|
||||
parseCaseContent,
|
||||
} from "../../../src/features/tech-log/domain/content-format/parse-case-content.ts";
|
||||
import { projectWorkingCopy } from "../../../src/features/tech-log/domain/content-format/project-public-render-model.ts";
|
||||
import {
|
||||
evidenceCatalogEntryFor,
|
||||
projectWorkingCopy,
|
||||
} from "../../../src/features/tech-log/domain/content-format/project-public-render-model.ts";
|
||||
import { serializeCaseContent } from "../../../src/features/tech-log/domain/content-format/serialize-case-content.ts";
|
||||
import type { components } from "../../../src/features/tech-log/contracts/studio/generated.ts";
|
||||
import type { Asset } from "../../../src/features/tech-log/contracts/studio/contract.ts";
|
||||
|
||||
/**
|
||||
* Fix round 2. Mirrors the gate-1 composition every production caller now
|
||||
* uses (`validate-working-copy.ts`, `adapters/mock/project-public-render-model.ts`,
|
||||
* `instant-preview.tsx`): the legacy key, or an actually-loaded Asset backs
|
||||
* it -- never "is there any EVIDENCE catalog row for it" (that's gate 2,
|
||||
* `evidenceCatalogEntryFor`, a separate question `projectWorkingCopy` asks
|
||||
* of its own `catalog` argument). Round 1 collapsed gate 1 into gate 2 in
|
||||
* every production caller, which let a real EVIDENCE catalog row backing
|
||||
* something other than media (e.g. a QUESTION resolution-target row) pass
|
||||
* gate 1 with no Asset and no legacy key behind it. Tests below pass no
|
||||
* `assets`, so this reduces to the legacy check alone for them -- the same
|
||||
* outcome bare `isSupportedEvidenceKey` gave before, just now visibly tied
|
||||
* to the real composition instead of standing in for it by coincidence.
|
||||
*/
|
||||
function supportsEvidenceKey(
|
||||
key: string,
|
||||
assets: readonly Asset[] = [],
|
||||
): boolean {
|
||||
return (
|
||||
isSupportedEvidenceKey(key) ||
|
||||
Boolean(evidenceCatalogEntryFor(evidenceCatalogEntriesFromAssets(assets), key))
|
||||
);
|
||||
}
|
||||
|
||||
const rich = `## 측정 결과 {#measurements}
|
||||
|
||||
@@ -354,7 +383,7 @@ describe("Public render model projection", () => {
|
||||
generatedAt: "2026-08-14T00:00:00Z",
|
||||
dependencyRevision: "r1",
|
||||
},
|
||||
isSupportedEvidenceKey,
|
||||
supportsEvidenceKey,
|
||||
);
|
||||
|
||||
expect(model.kind).toBe("CASE");
|
||||
@@ -390,7 +419,7 @@ describe("Public render model projection", () => {
|
||||
input,
|
||||
catalog,
|
||||
{ mode: "PREVIEW", publishedAt: null },
|
||||
isSupportedEvidenceKey,
|
||||
supportsEvidenceKey,
|
||||
);
|
||||
|
||||
expect(model.kind).toBe("QUESTION");
|
||||
@@ -411,7 +440,7 @@ describe("Public render model projection", () => {
|
||||
},
|
||||
catalog,
|
||||
{ mode: "PREVIEW", publishedAt: null },
|
||||
isSupportedEvidenceKey,
|
||||
supportsEvidenceKey,
|
||||
),
|
||||
).toThrow(/EVIDENCE catalog/);
|
||||
});
|
||||
@@ -440,7 +469,7 @@ describe("Public render model projection", () => {
|
||||
caseWithEvidence(key),
|
||||
unsupportedEvidenceCatalog,
|
||||
{ mode: "PREVIEW", publishedAt: null },
|
||||
isSupportedEvidenceKey,
|
||||
supportsEvidenceKey,
|
||||
),
|
||||
).toThrow(/supported local evidence key/);
|
||||
}
|
||||
@@ -456,4 +485,58 @@ describe("Public render model projection", () => {
|
||||
it("rejects a catalog-matching unsupported evidence label", () => {
|
||||
expectUnsupportedEvidence("unsupported-label");
|
||||
});
|
||||
|
||||
// Fix round 2 regression coverage. `catalog` above already carries
|
||||
// "resolution-evidence" -- a real `EVIDENCE` row that exists for a
|
||||
// QUESTION resolution target (see "requires EVIDENCE" above), not media.
|
||||
// A production caller whose gate 1 collapsed into gate 2 (round 1's bug)
|
||||
// would let this key through with no Asset and no legacy key backing it;
|
||||
// this is the exact class of catalog row the reviewer used to reproduce
|
||||
// that regression.
|
||||
it("rejects a real EVIDENCE catalog row that exists for a different purpose than media (round 2 regression)", () => {
|
||||
expect(() =>
|
||||
projectWorkingCopy(
|
||||
caseWithEvidence("resolution-evidence"),
|
||||
catalog,
|
||||
{ mode: "PREVIEW", publishedAt: null },
|
||||
supportsEvidenceKey,
|
||||
),
|
||||
).toThrow(/supported local evidence key/);
|
||||
});
|
||||
|
||||
it("accepts a key backed by a loaded READY Asset even though the document catalog carries no matching row", () => {
|
||||
const assets: Asset[] = [
|
||||
{
|
||||
id: "99999999-9999-4999-8999-999999999999",
|
||||
assetKey: "boundary-check",
|
||||
kind: "DIAGRAM",
|
||||
mediaType: "image/svg+xml",
|
||||
originalFilename: "boundary.svg",
|
||||
byteSize: 10,
|
||||
width: null,
|
||||
height: null,
|
||||
altText: null,
|
||||
decorative: false,
|
||||
managementStatus: "READY",
|
||||
publicPath: "/media/boundary-check.svg",
|
||||
usageCount: 0,
|
||||
version: 1,
|
||||
createdAt: "2026-08-14T00:00:00.000Z",
|
||||
updatedAt: "2026-08-14T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
// `catalog` alone has no row for "boundary-check" -- gate 2 only passes
|
||||
// because the caller merges in `evidenceCatalogEntriesFromAssets`, the
|
||||
// same way every production caller does.
|
||||
const evidenceCatalog = [...catalog, ...evidenceCatalogEntriesFromAssets(assets)];
|
||||
|
||||
const model = projectWorkingCopy(
|
||||
caseWithEvidence("boundary-check"),
|
||||
evidenceCatalog,
|
||||
{ mode: "PREVIEW", publishedAt: null },
|
||||
(key: string) => supportsEvidenceKey(key, assets),
|
||||
);
|
||||
|
||||
expect(model.kind).toBe("CASE");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user