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
+5
View File
@@ -16,10 +16,15 @@ describe("application input/output boundary", () => {
"diagnostics",
"runtime",
"recovery",
"features",
]);
expect(application).not.toHaveProperty("storage");
expect(application).not.toHaveProperty("telemetry");
expect(application).not.toHaveProperty("releaseInfo");
expect(application.features.has("not-installed")).toBe(false);
expect(() => application.features.get("not-installed")).toThrow(
"Application feature is not installed",
);
await expect(application.runtime.getReleaseSummary()).resolves.toEqual({
buildId: "test-build",
releaseId: "test-release",
+3 -44
View File
@@ -1,52 +1,11 @@
import { describe, expect, it } from "vitest";
import {
mapOperationPayload,
mapResourceDto,
} from "../../src/adapters/http/resource-mapper.js";
import { toResourceViewModel } from "../../src/application/view-models/resource-view-model.js";
import { mapOperationPayload } from "../../src/adapters/http/resource-mapper.js";
describe("DTO to model to view-model mapping", () => {
it("contains raw DTO names at the HTTP boundary", () => {
const model = mapResourceDto({
id: "resource-1",
name: "Example",
createdAt: "2026-07-25T00:00:00.000Z",
backendOnly: "not propagated",
});
expect(model).toEqual({
id: "resource-1",
displayName: "Example",
createdAt: "2026-07-25T00:00:00.000Z",
});
expect(model).not.toHaveProperty("name");
expect(model).not.toHaveProperty("backendOnly");
});
it("maps operation payloads and rejects missing mappers", () => {
expect(
mapOperationPayload("LIST_SAMPLE_RESOURCES", [
{ id: "resource-1", name: "Example" },
]),
).toEqual([
{ id: "resource-1", displayName: "Example", createdAt: null },
]);
describe("platform DTO mapper boundary", () => {
it("fails closed when no feature mapper was injected", () => {
expect(() => mapOperationPayload("UNKNOWN", {})).toThrow(
"No boundary mapper registered",
);
});
it("projects an application-owned render-ready shape", () => {
const model = mapResourceDto({
id: "resource-1",
name: "Example",
createdAt: null,
});
expect(toResourceViewModel(model)).toEqual({
resourceId: "resource-1",
title: "Example",
createdAtLabel: null,
});
});
});
+1 -1
View File
@@ -49,7 +49,7 @@ describe("frontend failure classification", () => {
};
const result = createFailure(
"SERVER_FAILURE",
"LIST_SAMPLE_RESOURCES",
"LIST_ENTITIES",
0,
untrustedDetails,
);
+1 -1
View File
@@ -4,7 +4,7 @@ import {
MANUAL_A11Y_ROUTE_IDS,
validateManualA11yEvidence,
} from "../../scripts/lib/manual-a11y-evidence.mjs";
import { ROUTE_REGISTRY } from "../../src/contracts/routes.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
const reviewed = `Status: reviewed
Route ID: APP_HOME
+16 -107
View File
@@ -1,126 +1,35 @@
import { describe, expect, it } from "vitest";
import {
NAVIGATION_ROUTES,
ROUTE_REGISTRY,
} from "../../src/features/installed-feature-contracts.js";
import {
createRedirectLoopGuard,
decideRouteAccess,
} from "../../src/presentation/routes/navigation-policy.js";
import {
NAVIGATION_ROUTES,
ROUTE_REGISTRY,
} from "../../src/contracts/routes.js";
describe("route registry", () => {
it("matches the stable registry snapshot", () => {
expect(ROUTE_REGISTRY).toMatchInlineSnapshot(`
{
"APP_HOME": {
"access": "public",
"chunkId": "route-home",
"errorSurface": "route-boundary",
"loadingSurface": "app-shell",
"navigationLabel": "시작",
"navigationOrder": 10,
"paramsSchema": null,
"path": "/",
"routeId": "APP_HOME",
"searchSchema": null,
"title": "시작",
},
"EXAMPLES_AUTH": {
"access": "public",
"chunkId": "route-examples-auth",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "인증 연동",
"navigationOrder": 40,
"paramsSchema": null,
"path": "/examples/auth",
"routeId": "EXAMPLES_AUTH",
"searchSchema": null,
"title": "인증 연동",
},
"EXAMPLES_STATES": {
"access": "public",
"chunkId": "route-examples-states",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "화면 상태",
"navigationOrder": 30,
"paramsSchema": null,
"path": "/examples/states",
"routeId": "EXAMPLES_STATES",
"searchSchema": null,
"title": "화면 상태",
},
"EXAMPLES_UI": {
"access": "public",
"chunkId": "route-examples-ui",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "UI 구성요소",
"navigationOrder": 20,
"paramsSchema": null,
"path": "/examples/ui",
"routeId": "EXAMPLES_UI",
"searchSchema": null,
"title": "UI 구성요소",
},
"NOT_FOUND": {
"access": "public",
"chunkId": "route-not-found",
"errorSurface": "not-found",
"loadingSurface": "none",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "NotFoundSplat",
"path": "*",
"routeId": "NOT_FOUND",
"searchSchema": null,
"title": "페이지를 찾을 수 없음",
},
"SAMPLE_RESOURCE_LIST": {
"access": "integration-defined",
"chunkId": "route-sample-resources",
"errorSurface": "feature-boundary",
"loadingSurface": "sample-resource-list",
"navigationLabel": "보호된 연동 지점",
"navigationOrder": 50,
"paramsSchema": null,
"path": "/sample/resources",
"routeId": "SAMPLE_RESOURCE_LIST",
"searchSchema": "SampleResourceListQuery",
"title": "보호된 연동 지점",
},
}
`);
});
describe("installed route registry", () => {
it("derives visible navigation in explicit order", () => {
expect(NAVIGATION_ROUTES.map(({ routeId }) => routeId)).toEqual([
"APP_HOME",
"EXAMPLES_UI",
"EXAMPLES_STATES",
"EXAMPLES_AUTH",
"SAMPLE_RESOURCE_LIST",
]);
const expected = Object.values(ROUTE_REGISTRY)
.filter((route) => route.navigationOrder !== null)
.sort(
(left, right) =>
/** @type {number} */ (left.navigationOrder) -
/** @type {number} */ (right.navigationOrder),
)
.map((route) => route.routeId);
expect(NAVIGATION_ROUTES.map(({ routeId }) => routeId)).toEqual(expected);
});
it("treats client access as a UX hint, not authorization", () => {
it("allows public routes without consulting a product permission", () => {
expect(decideRouteAccess("APP_HOME", "unauthenticated")).toEqual({
allowed: true,
action: "none",
});
expect(decideRouteAccess("SAMPLE_RESOURCE_LIST", "unauthenticated")).toEqual({
allowed: false,
action: "show-sign-in",
});
expect(decideRouteAccess("SAMPLE_RESOURCE_LIST", "authenticated")).toEqual({
allowed: true,
action: "none",
});
});
it("allows at most one automatic redirect per source-target pair", () => {
it("bounds automatic redirects by pair and maximum hops", () => {
const guard = createRedirectLoopGuard(2);
expect(guard.allow("/private", "/signin")).toBe(true);
expect(guard.allow("/private", "/signin")).toBe(false);
+16 -7
View File
@@ -5,18 +5,27 @@ import {
createQueryCacheAdapter,
createQueryClient,
} from "../../src/adapters/query-cache/tanstack-query-cache.js";
import { queryKeys } from "../../src/contracts/query-keys.js";
import { canonicalize } from "../../src/contracts/query-keys.js";
const queryKeys = Object.freeze({
all: () => Object.freeze(["entity", 1]),
list: (filters = {}) =>
Object.freeze(["entity", 1, "list", canonicalize(filters)]),
/** @param {string} entityId */
detail: (entityId) =>
Object.freeze(["entity", 1, "detail", String(entityId)]),
});
describe("query key registry", () => {
it("canonicalizes filter order into the same stable key", () => {
expect(queryKeys.resource.list({ page: 1, status: "open" })).toEqual(
queryKeys.resource.list({ status: "open", page: 1 }),
expect(queryKeys.list({ page: 1, status: "open" })).toEqual(
queryKeys.list({ status: "open", page: 1 }),
);
});
it("contains no raw URL or token material", () => {
expect(JSON.stringify(queryKeys.resource.detail("resource-1"))).toBe(
'["resource",1,"detail","resource-1"]',
expect(JSON.stringify(queryKeys.detail("entity-1"))).toBe(
'["entity",1,"detail","entity-1"]',
);
});
});
@@ -25,7 +34,7 @@ describe("TanStack QueryCachePort adapter", () => {
it("reads, writes, and invalidates only the declared namespace", async () => {
const client = createQueryClient();
const adapter = createQueryCacheAdapter(client);
const listKey = queryKeys.resource.list({ page: 1 });
const listKey = queryKeys.list({ page: 1 });
const otherKey = ["other", 1];
expect(adapter.write(listKey, [{ id: "resource-1" }])).toEqual({ ok: true });
@@ -35,7 +44,7 @@ describe("TanStack QueryCachePort adapter", () => {
value: [{ id: "resource-1" }],
});
await adapter.invalidate(queryKeys.resource.all());
await adapter.invalidate(queryKeys.all());
expect(client.getQueryState(listKey)?.isInvalidated).toBe(true);
expect(client.getQueryState(otherKey)?.isInvalidated).toBe(false);
});
+8 -48
View File
@@ -1,15 +1,14 @@
import { describe, expect, it } from "vitest";
import { ROUTE_RUNTIME_CONTRACT } from "../../src/contracts/route-runtime-contract.js";
import { ROUTE_REGISTRY } from "../../src/contracts/routes.js";
import {
buildRouteUrl,
parseRouteInput,
} from "../../src/presentation/routes/route-codecs.js";
import { ROUTE_RUNTIME } from "../../src/presentation/routes/route-runtime.js";
ROUTE_REGISTRY,
ROUTE_RUNTIME_CONTRACT,
} from "../../src/features/installed-feature-contracts.js";
import { ROUTE_RUNTIME } from "../../src/features/installed-feature-runtimes.js";
import { parseRouteInput } from "../../src/presentation/routes/route-codecs.js";
describe("typed route contract and runtime", () => {
it("keeps contract, runtime contribution, and executable module complete", () => {
describe("typed installed route catalog", () => {
it("keeps contract and executable runtime contributions complete", () => {
expect(Object.keys(ROUTE_RUNTIME_CONTRACT).sort()).toEqual(
Object.keys(ROUTE_REGISTRY).sort(),
);
@@ -18,46 +17,7 @@ describe("typed route contract and runtime", () => {
);
});
it("round-trips canonical search through the registered codec", () => {
const url = buildRouteUrl("SAMPLE_RESOURCE_LIST", {
search: {
tags: ["open", "new"],
cursor: "a/b",
limit: 5,
},
});
expect(url).toBe(
"/sample/resources?cursor=a%2Fb&limit=5&tags=open&tags=new",
);
const parsedUrl = new URL(url, "https://app.test");
expect(
parseRouteInput(
"SAMPLE_RESOURCE_LIST",
{},
parsedUrl.searchParams,
),
).toEqual({
success: true,
data: {
routeId: "SAMPLE_RESOURCE_LIST",
params: {},
search: {
cursor: "a/b",
limit: 5,
tags: ["open", "new"],
},
},
});
});
it("rejects unknown search and accepts the not-found splat owner", () => {
expect(
parseRouteInput(
"SAMPLE_RESOURCE_LIST",
{},
new URLSearchParams("unknown=value"),
),
).toEqual({ success: false, code: "ROUTE_SEARCH_INVALID" });
it("accepts the platform not-found splat owner", () => {
expect(
parseRouteInput(
"NOT_FOUND",
+19 -15
View File
@@ -4,6 +4,7 @@ import {
createRuntimeAdapters,
createRuntimeHttpClient,
} from "../../src/bootstrap/runtime-adapters.js";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.js";
const runtime = {
config: {
@@ -118,23 +119,26 @@ describe("runtime adapter composition", () => {
release,
host: {},
})).outputPorts.session;
const client = createRuntimeHttpClient({
runtime:
/** @type {Parameters<typeof createRuntimeHttpClient>[0]["runtime"]} */ (
runtime
),
authSession:
/** @type {import("../../src/application/ports/auth-session-port.js").AuthSessionPort} */ (
authSession
),
fetcher,
clock: { now: () => 0, sleep: async () => {} },
scheduler,
});
const client = createRuntimeHttpClient(
{
runtime:
/** @type {Parameters<typeof createRuntimeHttpClient>[0]["runtime"]} */ (
runtime
),
authSession:
/** @type {import("../../src/application/ports/auth-session-port.js").AuthSessionPort} */ (
authSession
),
fetcher,
clock: { now: () => 0, sleep: async () => {} },
scheduler,
},
TEST_HTTP_CONTRACT,
);
await client.execute({
operationId: "LIST_SAMPLE_RESOURCES",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
});
expect(fetcher).toHaveBeenCalledOnce();
+1 -1
View File
@@ -13,7 +13,7 @@ const validAttributes = {
error_kind: "SERVER_FAILURE",
http_status_group: "5xx",
attempt_count_bucket: "3",
route_id: "SAMPLE_RESOURCE_LIST",
route_id: "TEST_ROUTE",
};
describe("telemetry registry and redaction", () => {