feat: add removable reference feature vertical slice

This commit is contained in:
donghyeon-ka
2026-07-26 14:56:34 +09:00
parent 980981bc86
commit c11be43f20
87 changed files with 1881 additions and 1114 deletions
+41 -39
View File
@@ -7,46 +7,48 @@ import { createApplication } from "../../src/application/create-application.js";
* preferences?: import("../../src/application/ports/storage-port.js").StoragePort,
* diagnostics?: import("../../src/application/ports/telemetry-port.js").TelemetryPort,
* releaseInfo?: import("../../src/application/ports/release-info-port.js").ReleaseInfoPort,
* navigation?: { reload(): void }
* navigation?: { reload(): void },
* featureInputs?: Readonly<Record<string, unknown>>
* }} [overrides]
*/
export function createTestApplication(overrides = {}) {
return createApplication({
session: overrides.session ?? createAnonymousSessionAdapter(),
preferences:
overrides.preferences ??
{
read: () => ({ ok: /** @type {const} */ (true), value: "system" }),
write: () => ({ ok: /** @type {const} */ (true) }),
remove: () => ({ ok: /** @type {const} */ (true) }),
},
diagnostics: overrides.diagnostics ?? { emit: () => {} },
releaseInfo:
overrides.releaseInfo ??
{
getCurrent: async () => ({
buildId: "test-build",
releaseId: "test-release",
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: "test-hash",
routeChunks: {
"route-home": "assets/home.js",
"route-sample-resources": "assets/sample.js",
},
}),
refresh: async () => ({
buildId: "test-build",
releaseId: "test-release",
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: "test-hash",
routeChunks: {
"route-home": "assets/home.js",
"route-sample-resources": "assets/sample.js",
},
}),
},
navigation: overrides.navigation ?? { reload: () => {} },
});
return createApplication(
{
session: overrides.session ?? createAnonymousSessionAdapter(),
preferences:
overrides.preferences ??
{
read: () => ({ ok: /** @type {const} */ (true), value: "system" }),
write: () => ({ ok: /** @type {const} */ (true) }),
remove: () => ({ ok: /** @type {const} */ (true) }),
},
diagnostics: overrides.diagnostics ?? { emit: () => {} },
releaseInfo:
overrides.releaseInfo ??
{
getCurrent: async () => ({
buildId: "test-build",
releaseId: "test-release",
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: "test-hash",
routeChunks: {
"route-home": "assets/home.js",
},
}),
refresh: async () => ({
buildId: "test-build",
releaseId: "test-release",
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: "test-hash",
routeChunks: {
"route-home": "assets/home.js",
},
}),
},
navigation: overrides.navigation ?? { reload: () => {} },
},
overrides.featureInputs,
);
}
+108
View File
@@ -0,0 +1,108 @@
import { z } from "zod";
import type { createHttpClient } from "../../src/adapters/http/client.js";
import { canonicalize } from "../../src/contracts/query-keys.js";
const entitySchema = z
.object({
id: z.string().min(1),
name: z.string().min(1),
})
.passthrough();
const payloadSchemas = {
EntityListPayload: z.array(entitySchema),
EntityPayload: entitySchema,
};
const requestSchemas = {
EntityListQuery: z
.object({
cursor: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
tags: z.array(z.string().trim().min(1)).optional(),
})
.strict(),
CreateEntityCommand: z
.object({ name: z.string().trim().min(1).max(120) })
.strict(),
};
export const TEST_OPERATIONS = Object.freeze({
LIST_ENTITIES: Object.freeze({
method: "GET",
path: "/api/entities",
operationId: "LIST_ENTITIES",
auth: "external-session",
timeoutMs: null,
idempotency: "safe",
retry: "runtime",
requestSource: "search",
requestSchema: "EntityListQuery",
responseSchema: "EntityListPayload",
owner: "test-fixture",
}),
CREATE_ENTITY: Object.freeze({
method: "POST",
path: "/api/entities",
operationId: "CREATE_ENTITY",
auth: "external-session",
timeoutMs: null,
idempotency: "keyed",
retry: "runtime",
requestSource: "body",
requestSchema: "CreateEntityCommand",
responseSchema: "EntityPayload",
owner: "test-fixture",
}),
});
export const entityQueryKeys = Object.freeze({
list: (filters: Readonly<Record<string, unknown>> = {}) =>
Object.freeze(["entity", 1, canonicalize(filters)]),
});
type Validation =
| Readonly<{ success: true; data: unknown }>
| Readonly<{ success: false }>;
function project(schema: z.ZodType | undefined, value: unknown): Validation {
const result = schema?.safeParse(value);
if (!result?.success) return { success: false };
return { success: true, data: structuredClone(result.data) };
}
type HttpDependencies = Parameters<typeof createHttpClient>[0];
export const TEST_HTTP_CONTRACT = Object.freeze({
getOperation(operationId: string) {
const operation =
TEST_OPERATIONS[operationId as keyof typeof TEST_OPERATIONS];
if (!operation) throw new Error(`Unknown test operation: ${operationId}`);
return operation;
},
validatePayload(schemaId: string, value: unknown) {
return project(
payloadSchemas[schemaId as keyof typeof payloadSchemas],
value,
);
},
validateRequest(schemaId: string, value: unknown) {
return project(
requestSchemas[schemaId as keyof typeof requestSchemas],
value,
);
},
mapPayload(operationId: string, payload: unknown) {
const mapOne = (value: unknown) => {
const entity = value as { id: string; name: string };
return { id: entity.id, displayName: entity.name };
};
if (operationId === "LIST_ENTITIES") {
return (payload as readonly unknown[]).map(mapOne);
}
if (operationId === "CREATE_ENTITY") return mapOne(payload);
throw new Error(`Unknown test mapper: ${operationId}`);
},
}) satisfies Pick<
HttpDependencies,
"getOperation" | "validatePayload" | "validateRequest" | "mapPayload"
>;