fix: forward every documented Studio query filter and pin canonicalInputIdentity

Two review Minor findings against the brief itself, both closed:

- tests/mocks/handlers/tech-log-studio.ts silently dropped documented
  query filters instead of mirroring the mock gateway it wraps:
  listStudioDocuments forwarded only q/limit (dropping kind,
  publicationStatus, nextAction, projectId, sort, cursor),
  listStudioCatalog forwarded only type (dropping q/cursor/limit), and
  listStudioPublications ignored all four of its parameters outright.
  Added a shared queryParams() helper and forward every field each
  operation's projectRequest actually emits, plus a regression test
  that narrows the fixture set by kind through the real HTTP gateway
  (confirmed it fails without the fix).

- canonicalInputIdentity (on the idempotency-safety path, reused by
  Task 6) had no test. Added three tests against mutationIntent(): a
  large Korean payload stays within the byte bound, the same input
  retried twice yields an identical identity, and a mid-codepoint
  truncation cut leaves no replacement character (confirmed the third
  fails without the strip). The known collision limitation of any
  bounded-length identity scheme is documented in the test file rather
  than solved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-18 01:43:51 +09:00
co-authored by Claude Opus 5
parent 1d01a522de
commit 7424ed4594
2 changed files with 120 additions and 10 deletions
+42 -9
View File
@@ -23,6 +23,30 @@ async function json(work: () => Promise<unknown>, status = 200) {
}
}
/**
* Forwards every query parameter a `projectRequest` puts on the wire, not
* just the ones a first pass happened to reach for. A handler that silently
* drops a documented filter lets a later filter test pass for the wrong
* reason — it would observe the *unfiltered* mock result and never learn the
* parameter never left the URL.
*/
function queryParams(
url: URL,
stringNames: readonly string[],
numberNames: readonly string[] = [],
): Record<string, string | number> {
const result: Record<string, string | number> = {};
for (const name of stringNames) {
const value = url.searchParams.get(name);
if (value !== null) result[name] = value;
}
for (const name of numberNames) {
const value = url.searchParams.get(name);
if (value !== null) result[name] = Number(value);
}
return result;
}
export function createTechLogStudioHandlers(
gateway = createMockStudioGateway(),
baseUrl = BASE,
@@ -47,12 +71,14 @@ export function createTechLogStudioHandlers(
),
http.get(`${baseUrl}/api/v1/studio/documents`, ({ request }) => {
const url = new URL(request.url);
const limit = url.searchParams.get("limit");
return json(() =>
gateway.listDocuments({
...(url.searchParams.get("q") ? { q: url.searchParams.get("q")! } : {}),
...(limit ? { limit: Number(limit) } : {}),
} as never),
gateway.listDocuments(
queryParams(
url,
["q", "kind", "publicationStatus", "nextAction", "projectId", "sort", "cursor"],
["limit"],
) as never,
),
);
}),
http.post(`${baseUrl}/api/v1/studio/documents`, async ({ request }) =>
@@ -117,9 +143,14 @@ export function createTechLogStudioHandlers(
),
),
),
http.get(`${baseUrl}/api/v1/studio/publications`, () =>
json(() => gateway.listPublications({})),
),
http.get(`${baseUrl}/api/v1/studio/publications`, ({ request }) => {
const url = new URL(request.url);
return json(() =>
gateway.listPublications(
queryParams(url, ["q", "type", "cursor"], ["limit"]) as never,
),
);
}),
http.post(
`${baseUrl}/api/v1/studio/publications/:publicationId/unpublish`,
async ({ request, params }) =>
@@ -139,7 +170,9 @@ export function createTechLogStudioHandlers(
http.get(`${baseUrl}/api/v1/studio/catalog`, ({ request }) => {
const url = new URL(request.url);
return json(() =>
gateway.getCatalog({ type: url.searchParams.get("type") as never }),
gateway.getCatalog(
queryParams(url, ["type", "q", "cursor"], ["limit"]) as never,
),
);
}),
],