diff --git a/__tests__/bundle-initial-load.test.ts b/__tests__/bundle-initial-load.test.ts
new file mode 100644
index 0000000..346163c
--- /dev/null
+++ b/__tests__/bundle-initial-load.test.ts
@@ -0,0 +1,161 @@
+import { describe, expect, it } from "vitest";
+import {
+ checkBudgets,
+ loadExceptions,
+ loadBaseline,
+ analyzeRouteInitialLoad,
+ formatReport,
+ DEFAULT_BUDGET_KB,
+ ROUTE_INITIAL_LOAD_BUDGET_KB
+} from "@/scripts/bundle-budget.mjs";
+
+describe("Initial-Load Performance Budgets & SDK Exception Path (#159)", () => {
+ const sampleExceptions = {
+ "xdr-inspector": {
+ budgetKB: 350,
+ justification: "Deep recursive XDR parser and Soroban contract type schemas",
+ approved: true
+ },
+ "soroban-decoder": {
+ budgetKB: 300,
+ justification: "Full Soroban ScVal decoding and contract auth types",
+ approved: true
+ }
+ };
+
+ const sampleBaseline = {
+ "address-validator": { totalSize: 102400, sizeKB: 100 },
+ "_route_initial_load": { totalSize: 184320, sizeKB: 180 }
+ };
+
+ it("approves standard feature within normal budget", () => {
+ const featureSizes = {
+ "address-validator": { totalSize: 110 * 1024, chunks: [] }
+ };
+ const routeInitialLoad = { totalSize: 150 * 1024, sizeKB: 150, chunks: [] };
+
+ const { results, routeResult, hasViolations } = checkBudgets(
+ featureSizes,
+ routeInitialLoad,
+ sampleBaseline,
+ sampleExceptions
+ );
+
+ expect(hasViolations).toBe(false);
+ expect(results[0].status).toBe("OK");
+ expect(results[0].withinBudget).toBe(true);
+ expect(routeResult.status).toBe("OK");
+ });
+
+ it("flags violation when tool exceeds budget without approved exception", () => {
+ const featureSizes = {
+ "random-heavy-tool": { totalSize: 180 * 1024, chunks: [] } // Exceeds default 150 KB
+ };
+ const routeInitialLoad = { totalSize: 150 * 1024, sizeKB: 150, chunks: [] };
+
+ const { results, hasViolations } = checkBudgets(
+ featureSizes,
+ routeInitialLoad,
+ sampleBaseline,
+ sampleExceptions
+ );
+
+ expect(hasViolations).toBe(true);
+ expect(results[0].status).toBe("EXCEEDED");
+ expect(results[0].withinBudget).toBe(false);
+ });
+
+ it("approves SDK-heavy feature that exceeds standard budget via documented exception path", () => {
+ const featureSizes = {
+ "xdr-inspector": { totalSize: 280 * 1024, chunks: [] } // 280 KB > 150 KB standard, but < 350 KB exception ceiling
+ };
+ const routeInitialLoad = { totalSize: 150 * 1024, sizeKB: 150, chunks: [] };
+
+ const { results, hasViolations } = checkBudgets(
+ featureSizes,
+ routeInitialLoad,
+ sampleBaseline,
+ sampleExceptions
+ );
+
+ expect(hasViolations).toBe(false);
+ expect(results[0].status).toBe("EXCEPTION_APPROVED");
+ expect(results[0].withinBudget).toBe(true);
+ expect(results[0].isException).toBe(true);
+ expect(results[0].justification).toContain("Deep recursive XDR parser");
+ });
+
+ it("rejects feature that exceeds its approved exception ceiling", () => {
+ const featureSizes = {
+ "xdr-inspector": { totalSize: 380 * 1024, chunks: [] } // 380 KB > 350 KB exception ceiling!
+ };
+ const routeInitialLoad = { totalSize: 150 * 1024, sizeKB: 150, chunks: [] };
+
+ const { results, hasViolations } = checkBudgets(
+ featureSizes,
+ routeInitialLoad,
+ sampleBaseline,
+ sampleExceptions
+ );
+
+ expect(hasViolations).toBe(true);
+ expect(results[0].status).toContain("EXCEEDED");
+ expect(results[0].withinBudget).toBe(false);
+ });
+
+ it("enforces route-level initial-load budget limit", () => {
+ const featureSizes = {
+ "address-validator": { totalSize: 100 * 1024, chunks: [] }
+ };
+ // Exceeds 220 KB route initial-load budget
+ const heavyRouteInitialLoad = { totalSize: 260 * 1024, sizeKB: 260, chunks: [] };
+
+ const { routeResult, hasViolations } = checkBudgets(
+ featureSizes,
+ heavyRouteInitialLoad,
+ sampleBaseline,
+ sampleExceptions
+ );
+
+ expect(hasViolations).toBe(true);
+ expect(routeResult.status).toBe("EXCEEDED");
+ expect(routeResult.withinBudget).toBe(false);
+ });
+
+ it("loads documented exceptions catalog from repository file", () => {
+ const exceptions = loadExceptions();
+ expect(exceptions["xdr-inspector"]).toBeDefined();
+ expect(exceptions["xdr-inspector"].approved).toBe(true);
+ expect(exceptions["xdr-inspector"].budgetKB).toBeGreaterThan(DEFAULT_BUDGET_KB);
+ });
+
+ it("formats comprehensive report containing route and exception information", () => {
+ const results = [
+ {
+ feature: "xdr-inspector",
+ sizeKB: 280,
+ budgetKB: 350,
+ isException: true,
+ justification: "Soroban bindings",
+ status: "EXCEPTION_APPROVED",
+ delta: 0,
+ baselineSizeKB: 280,
+ withinBudget: true
+ }
+ ];
+ const routeResult = {
+ name: "Route Initial-Load (/tools/[slug])",
+ sizeKB: 180,
+ budgetKB: ROUTE_INITIAL_LOAD_BUDGET_KB,
+ baselineSizeKB: 180,
+ delta: 0,
+ status: "OK",
+ withinBudget: true
+ };
+
+ const report = formatReport(results, routeResult, false);
+ expect(report).toContain("UTIX INITIAL-LOAD & FEATURE BUNDLE BUDGET REPORT");
+ expect(report).toContain("ROUTE INITIAL-LOAD COSTS");
+ expect(report).toContain("Exception Approved: Soroban bindings");
+ });
+});
diff --git a/__tests__/storage-isolation.test.tsx b/__tests__/storage-isolation.test.tsx
new file mode 100644
index 0000000..1528129
--- /dev/null
+++ b/__tests__/storage-isolation.test.tsx
@@ -0,0 +1,126 @@
+import { cleanup, screen, act } from "@testing-library/react";
+import { afterEach, describe, expect, it } from "vitest";
+import { renderFeature, createIsolatedStorage } from "@/core/testing";
+import { useNetwork } from "@/core/network/NetworkProvider";
+import { useNotifications } from "@/core/notifications/NotificationProvider";
+import { NETWORK_STORAGE_KEY } from "@/core/network/config";
+
+function TestConsumer() {
+ const { network, setNetwork } = useNetwork();
+ const { notifications, unreadCount, publish } = useNotifications();
+
+ return (
+
+
{network}
+
{unreadCount}
+
+
+
+ {notifications.map((n) => (
+ - {n.title}
+ ))}
+
+
+ );
+}
+
+describe("Deterministic Browser Storage Isolation (#155)", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe("Failing-order regression test demonstration", () => {
+ it("demonstrates state contamination when sharing unisolated storage across tests", () => {
+ // Unisolated shared storage simulation
+ const leakySharedStorage = createIsolatedStorage();
+
+ // Test 1: Mutates shared storage to 'mainnet'
+ leakySharedStorage.setItem(NETWORK_STORAGE_KEY, "mainnet");
+
+ // Test 2 (running after Test 1): Expects default clean state, but gets contaminated by Test 1!
+ const leakedValue = leakySharedStorage.getItem(NETWORK_STORAGE_KEY);
+ expect(leakedValue).toBe("mainnet"); // Demonstrates leakage hazard
+ });
+ });
+
+ describe("Deterministic isolation with renderFeature and injected storage", () => {
+ it("Step A: Render 1 modifies network and publishes notifications in its isolated fixture", async () => {
+ const { user, storage } = renderFeature();
+
+ expect(screen.getByTestId("network-status").textContent).toBe("testnet");
+ expect(screen.getByTestId("unread-count").textContent).toBe("0");
+
+ // Mutate network to mainnet
+ await user.click(screen.getByRole("button", { name: "Switch to Mainnet" }));
+ expect(screen.getByTestId("network-status").textContent).toBe("mainnet");
+ expect(storage.getItem(NETWORK_STORAGE_KEY)).toBe("mainnet");
+
+ // Send notification
+ await user.click(screen.getByRole("button", { name: "Send Notification" }));
+ expect(screen.getByTestId("unread-count").textContent).toBe("1");
+ });
+
+ it("Step B: Render 2 starts with pristine testnet and zero notifications (independent of Step A)", () => {
+ // Render 2 runs after Step A. Without isolation, Step A's mainnet and notifications would leak.
+ // With deterministic isolation, Render 2 owns a brand new storage fixture.
+ const { storage } = renderFeature();
+
+ expect(screen.getByTestId("network-status").textContent).toBe("testnet");
+ expect(screen.getByTestId("unread-count").textContent).toBe("0");
+ expect(storage.getItem(NETWORK_STORAGE_KEY)).toBeNull();
+ });
+
+ it("supports explicitly sharing an injected storage fixture when persistence is under test", async () => {
+ const sharedFixture = createIsolatedStorage();
+
+ // First session: write to the explicit fixture
+ const session1 = renderFeature(, { storage: sharedFixture });
+ await session1.user.click(session1.getByRole("button", { name: "Switch to Mainnet" }));
+ expect(session1.getByTestId("network-status").textContent).toBe("mainnet");
+ cleanup();
+
+ // Second session: re-mount with the SAME fixture to test persistence
+ const session2 = renderFeature(, { storage: sharedFixture });
+ expect(session2.getByTestId("network-status").textContent).toBe("mainnet");
+ });
+
+ it("createIsolatedStorage provides complete Storage interface fidelity", () => {
+ const storage = createIsolatedStorage({ initialKey: "initialValue" });
+
+ expect(storage.length).toBe(1);
+ expect(storage.getItem("initialKey")).toBe("initialValue");
+ expect(storage.key(0)).toBe("initialKey");
+
+ storage.setItem("key2", "value2");
+ expect(storage.length).toBe(2);
+ expect(storage.getItem("key2")).toBe("value2");
+
+ storage.removeItem("initialKey");
+ expect(storage.length).toBe(1);
+ expect(storage.getItem("initialKey")).toBeNull();
+
+ storage.clear();
+ expect(storage.length).toBe(0);
+ expect(storage.getItem("key2")).toBeNull();
+ });
+ });
+});
diff --git a/app/globals.css b/app/globals.css
index e489c6f..67ca483 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -94,3 +94,25 @@ textarea {
html {
scroll-behavior: smooth;
}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+
+ .animate-fade-in-up,
+ .animate-float,
+ .animate-pulse {
+ animation: none !important;
+ }
+}
+
diff --git a/bundle-baseline.json b/bundle-baseline.json
index de4820f..5c35dd2 100644
--- a/bundle-baseline.json
+++ b/bundle-baseline.json
@@ -33,5 +33,10 @@
"totalSize": 81920,
"sizeKB": 80,
"timestamp": "2026-09-25T00:00:00.000Z"
+ },
+ "_route_initial_load": {
+ "totalSize": 184320,
+ "sizeKB": 180,
+ "timestamp": "2026-09-25T00:00:00.000Z"
}
}
diff --git a/core/auth/__tests__/signed-administrative-approvals.test.ts b/core/auth/__tests__/signed-administrative-approvals.test.ts
new file mode 100644
index 0000000..aacbc25
--- /dev/null
+++ b/core/auth/__tests__/signed-administrative-approvals.test.ts
@@ -0,0 +1,347 @@
+import { beforeEach, describe, expect, it } from "vitest";
+import { Keypair } from "@stellar/stellar-sdk";
+import {
+ ACTION_THRESHOLDS,
+ executeProtectedAction,
+ MaintainerApprovalManager,
+ verifyProtectedActionApproval,
+ type HighImpactAction,
+ type ProtectedActionRequest
+} from "../maintainer-approval";
+
+const MAINTAINER_ALICE = Keypair.fromRawEd25519Seed(Buffer.alloc(32, 1));
+const MAINTAINER_BOB = Keypair.fromRawEd25519Seed(Buffer.alloc(32, 2));
+const MAINTAINER_CAROL = Keypair.fromRawEd25519Seed(Buffer.alloc(32, 3));
+const ROGUE_ACTOR = Keypair.fromRawEd25519Seed(Buffer.alloc(32, 99));
+
+describe("Issue #180: Signed Administrative Action Approvals for High-Impact Changes", () => {
+ let manager: MaintainerApprovalManager;
+ let mockCurrentTime: number;
+
+ beforeEach(() => {
+ mockCurrentTime = 1700000000000;
+ manager = new MaintainerApprovalManager({
+ now: () => new Date(mockCurrentTime),
+ defaultExpiryMs: 15 * 60 * 1000 // 15 minutes
+ });
+ });
+
+ function signProposal(intentId: string, signer: Keypair) {
+ const intent = manager.getIntent(intentId);
+ if (!intent) throw new Error("Intent not found for test helper");
+ const sigHex = signer.sign(Buffer.from(intent.intentHash, "hex")).toString("hex");
+ return manager.signIntent(intentId, signer.publicKey(), sigHex);
+ }
+
+ describe("Thresholds & Action Types", () => {
+ it("defines proper multi-maintainer thresholds for ticketing and event access actions", () => {
+ const expectedThresholds: Record = {
+ emergency_killswitch: 2,
+ protocol_rollback: 2,
+ reconciliation_repair: 2,
+ admin_config_override: 2,
+ quota_override: 1,
+ dead_letter_purge: 1,
+ event_capacity_override: 2,
+ event_cancellation: 2,
+ fraud_lock_quarantine: 1,
+ batch_ticket_burn: 2,
+ organizer_fee_override: 2
+ };
+
+ for (const [action, threshold] of Object.entries(expectedThresholds)) {
+ expect(ACTION_THRESHOLDS[action as HighImpactAction]).toBe(threshold);
+ }
+ });
+ });
+
+ describe("Protected Action Verification & Guardrails", () => {
+ it("fails when approval ID is missing or not found (approval_not_found)", async () => {
+ const request: ProtectedActionRequest = {
+ action: "event_cancellation",
+ target: "event-stadium-2026",
+ approvalId: "intent-does-not-exist",
+ executorPublicKey: MAINTAINER_ALICE.publicKey()
+ };
+
+ const verification = verifyProtectedActionApproval(request, manager);
+ expect(verification.valid).toBe(false);
+ if (!verification.valid) {
+ expect(verification.error).toBe("approval_not_found");
+ expect(verification.message).toContain("was not found");
+ }
+
+ await expect(
+ executeProtectedAction(request, () => ({ success: true }), manager)
+ ).rejects.toThrow("approval_not_found");
+ });
+
+ it("rejects pending actions when approval threshold is not yet met (approval_not_ready)", async () => {
+ const intent = manager.proposeIntent({
+ action: "event_cancellation",
+ target: "event-stadium-2026",
+ reason: "Severe hurricane alert issued by authorities",
+ requestedBy: MAINTAINER_ALICE.publicKey()
+ });
+
+ // Only 1 of 2 required maintainers signs
+ signProposal(intent.id, MAINTAINER_ALICE);
+
+ const request: ProtectedActionRequest = {
+ action: "event_cancellation",
+ target: "event-stadium-2026",
+ approvalId: intent.id,
+ executorPublicKey: MAINTAINER_ALICE.publicKey()
+ };
+
+ const verification = verifyProtectedActionApproval(request, manager);
+ expect(verification.valid).toBe(false);
+ if (!verification.valid) {
+ expect(verification.error).toBe("approval_not_ready");
+ }
+
+ await expect(
+ executeProtectedAction(request, () => ({ success: true }), manager)
+ ).rejects.toThrow("approval_not_ready");
+ });
+
+ it("rejects expired approvals (approval_expired)", async () => {
+ const intent = manager.proposeIntent({
+ action: "event_capacity_override",
+ target: "event-arena-101",
+ parameters: { maxTickets: 25000 },
+ reason: "Fire marshal permit granted for floor expansion",
+ requestedBy: MAINTAINER_ALICE.publicKey(),
+ expiresInMs: 5000
+ });
+
+ // Alice and Bob sign to approve
+ signProposal(intent.id, MAINTAINER_ALICE);
+ signProposal(intent.id, MAINTAINER_BOB);
+ expect(manager.getIntent(intent.id)?.status).toBe("approved");
+
+ // Advance clock past expiration
+ mockCurrentTime += 10000;
+
+ const request: ProtectedActionRequest = {
+ action: "event_capacity_override",
+ target: "event-arena-101",
+ approvalId: intent.id,
+ executorPublicKey: MAINTAINER_ALICE.publicKey()
+ };
+
+ const verification = verifyProtectedActionApproval(request, manager);
+ expect(verification.valid).toBe(false);
+ if (!verification.valid) {
+ expect(verification.error).toBe("approval_expired");
+ }
+
+ await expect(
+ executeProtectedAction(request, () => ({ expanded: true }), manager)
+ ).rejects.toThrow("approval_expired");
+ });
+
+ it("rejects mismatched actions and targets (action_mismatch, target_mismatch)", async () => {
+ const intent = manager.proposeIntent({
+ action: "event_cancellation",
+ target: "event-main-stage",
+ reason: "Force majeure event",
+ requestedBy: MAINTAINER_ALICE.publicKey()
+ });
+
+ signProposal(intent.id, MAINTAINER_ALICE);
+ signProposal(intent.id, MAINTAINER_BOB);
+
+ // 1. Action Mismatch
+ const actionMismatchReq: ProtectedActionRequest = {
+ action: "event_capacity_override", // Mismatched action!
+ target: "event-main-stage",
+ approvalId: intent.id,
+ executorPublicKey: MAINTAINER_ALICE.publicKey()
+ };
+ const vAction = verifyProtectedActionApproval(actionMismatchReq, manager);
+ expect(vAction.valid).toBe(false);
+ if (!vAction.valid) {
+ expect(vAction.error).toBe("action_mismatch");
+ }
+
+ await expect(
+ executeProtectedAction(actionMismatchReq, () => {}, manager)
+ ).rejects.toThrow("action_mismatch");
+
+ // 2. Target Mismatch
+ const targetMismatchReq: ProtectedActionRequest = {
+ action: "event_cancellation",
+ target: "event-different-stage", // Mismatched target!
+ approvalId: intent.id,
+ executorPublicKey: MAINTAINER_ALICE.publicKey()
+ };
+ const vTarget = verifyProtectedActionApproval(targetMismatchReq, manager);
+ expect(vTarget.valid).toBe(false);
+ if (!vTarget.valid) {
+ expect(vTarget.error).toBe("target_mismatch");
+ }
+
+ await expect(
+ executeProtectedAction(targetMismatchReq, () => {}, manager)
+ ).rejects.toThrow("target_mismatch");
+ });
+
+ it("rejects mismatched parameters (parameter_mismatch)", async () => {
+ const intent = manager.proposeIntent({
+ action: "organizer_fee_override",
+ target: "organizer-fest-co",
+ parameters: { feeBasisPoints: 150 },
+ reason: "Approved partnership promotional discount",
+ requestedBy: MAINTAINER_ALICE.publicKey()
+ });
+
+ signProposal(intent.id, MAINTAINER_ALICE);
+ signProposal(intent.id, MAINTAINER_BOB);
+
+ const alteredParamsReq: ProtectedActionRequest = {
+ action: "organizer_fee_override",
+ target: "organizer-fest-co",
+ parameters: { feeBasisPoints: 0 }, // Altered from 150 to 0!
+ approvalId: intent.id,
+ executorPublicKey: MAINTAINER_ALICE.publicKey()
+ };
+
+ const vParam = verifyProtectedActionApproval(alteredParamsReq, manager);
+ expect(vParam.valid).toBe(false);
+ if (!vParam.valid) {
+ expect(vParam.error).toBe("parameter_mismatch");
+ }
+
+ await expect(
+ executeProtectedAction(alteredParamsReq, () => {}, manager)
+ ).rejects.toThrow("parameter_mismatch");
+ });
+
+ it("rejects unauthorized actors and invalid Ed25519 public keys (unauthorized_actor)", async () => {
+ const intent = manager.proposeIntent({
+ action: "fraud_lock_quarantine",
+ target: "ticket-batch-90210",
+ reason: "Suspected credit card chargeback cluster",
+ requestedBy: MAINTAINER_ALICE.publicKey()
+ });
+
+ signProposal(intent.id, MAINTAINER_ALICE);
+
+ // 1. Invalid key format
+ const invalidKeyReq: ProtectedActionRequest = {
+ action: "fraud_lock_quarantine",
+ target: "ticket-batch-90210",
+ approvalId: intent.id,
+ executorPublicKey: "MALFORMED_STELLAR_KEY"
+ };
+
+ const vInvalid = verifyProtectedActionApproval(invalidKeyReq, manager);
+ expect(vInvalid.valid).toBe(false);
+ if (!vInvalid.valid) {
+ expect(vInvalid.error).toBe("unauthorized_actor");
+ }
+
+ await expect(
+ executeProtectedAction(invalidKeyReq, () => {}, manager)
+ ).rejects.toThrow("unauthorized_actor");
+
+ // 2. Key not in authorized maintainers list
+ const unauthorizedReq: ProtectedActionRequest = {
+ action: "fraud_lock_quarantine",
+ target: "ticket-batch-90210",
+ approvalId: intent.id,
+ executorPublicKey: ROGUE_ACTOR.publicKey()
+ };
+
+ const vRogue = verifyProtectedActionApproval(unauthorizedReq, manager, {
+ authorizedMaintainers: [MAINTAINER_ALICE.publicKey(), MAINTAINER_BOB.publicKey()]
+ });
+ expect(vRogue.valid).toBe(false);
+ if (!vRogue.valid) {
+ expect(vRogue.error).toBe("unauthorized_actor");
+ }
+
+ await expect(
+ executeProtectedAction(unauthorizedReq, () => {}, manager, {
+ authorizedMaintainers: [MAINTAINER_ALICE.publicKey(), MAINTAINER_BOB.publicKey()]
+ })
+ ).rejects.toThrow("unauthorized_actor");
+
+ // 3. Executor required to be a signer
+ const notSignerReq: ProtectedActionRequest = {
+ action: "fraud_lock_quarantine",
+ target: "ticket-batch-90210",
+ approvalId: intent.id,
+ executorPublicKey: MAINTAINER_CAROL.publicKey()
+ };
+
+ const vNotSigner = verifyProtectedActionApproval(notSignerReq, manager, {
+ requireSignerAsExecutor: true
+ });
+ expect(vNotSigner.valid).toBe(false);
+ if (!vNotSigner.valid) {
+ expect(vNotSigner.error).toBe("unauthorized_actor");
+ }
+ });
+
+ it("executes valid multi-maintainer signed approval and prevents replay (already_executed)", async () => {
+ const intent = manager.proposeIntent({
+ action: "batch_ticket_burn",
+ target: "event-2026-fraudulent-run",
+ parameters: { ticketIds: ["t-1", "t-2", "t-3"], burnReason: "Counterfeit barcodes" },
+ reason: "Court ordered ticket invalidation",
+ requestedBy: MAINTAINER_ALICE.publicKey()
+ });
+
+ // Both Alice and Bob sign
+ signProposal(intent.id, MAINTAINER_ALICE);
+ signProposal(intent.id, MAINTAINER_BOB);
+
+ const request: ProtectedActionRequest = {
+ action: "batch_ticket_burn",
+ target: "event-2026-fraudulent-run",
+ parameters: { ticketIds: ["t-1", "t-2", "t-3"], burnReason: "Counterfeit barcodes" },
+ approvalId: intent.id,
+ executorPublicKey: MAINTAINER_BOB.publicKey()
+ };
+
+ // 1. Verification succeeds
+ const vOk = verifyProtectedActionApproval(request, manager);
+ expect(vOk.valid).toBe(true);
+ if (vOk.valid) {
+ expect(vOk.intent.id).toBe(intent.id);
+ }
+
+ // 2. Execution succeeds
+ let executedBurn = false;
+ const executionResult = await executeProtectedAction(
+ request,
+ async (approvedIntent) => {
+ executedBurn = true;
+ return { burnedCount: (approvedIntent.parameters.ticketIds as string[]).length };
+ },
+ manager
+ );
+
+ expect(executedBurn).toBe(true);
+ expect(executionResult.result.burnedCount).toBe(3);
+ expect(executionResult.receipt.receiptDigest).toBeDefined();
+ expect(executionResult.receipt.receiptDigest).toHaveLength(64);
+ expect(executionResult.receipt.signers).toContain(MAINTAINER_ALICE.publicKey());
+ expect(executionResult.receipt.signers).toContain(MAINTAINER_BOB.publicKey());
+ expect(executionResult.receipt.status).toBe("executed");
+
+ // 3. Replay protection: Attempting to verify or execute again is blocked
+ const vReplay = verifyProtectedActionApproval(request, manager);
+ expect(vReplay.valid).toBe(false);
+ if (!vReplay.valid) {
+ expect(vReplay.error).toBe("already_executed");
+ }
+
+ await expect(
+ executeProtectedAction(request, () => ({ burnedCount: 0 }), manager)
+ ).rejects.toThrow("already_executed");
+ });
+ });
+});
diff --git a/core/auth/maintainer-approval.ts b/core/auth/maintainer-approval.ts
index b124f90..8e48e47 100644
--- a/core/auth/maintainer-approval.ts
+++ b/core/auth/maintainer-approval.ts
@@ -23,7 +23,12 @@ export type HighImpactAction =
| "protocol_rollback"
| "quota_override"
| "dead_letter_purge"
- | "admin_config_override";
+ | "admin_config_override"
+ | "event_capacity_override"
+ | "event_cancellation"
+ | "fraud_lock_quarantine"
+ | "batch_ticket_burn"
+ | "organizer_fee_override";
export type IntentStatus = "pending" | "approved" | "executing" | "executed" | "rejected" | "expired" | "failed";
@@ -84,14 +89,50 @@ export interface ApprovalFlowOptions {
defaultExpiryMs?: number;
}
+export interface ProtectedActionRequest {
+ action: HighImpactAction;
+ target: string;
+ parameters?: Record;
+ approvalId: string;
+ executorPublicKey: string;
+}
+
+export type ProtectedActionErrorCode =
+ | "approval_not_found"
+ | "approval_not_ready"
+ | "approval_expired"
+ | "already_executed"
+ | "action_mismatch"
+ | "target_mismatch"
+ | "parameter_mismatch"
+ | "unauthorized_actor";
+
+export type ProtectedActionVerificationResult =
+ | { valid: true; intent: IntentRequest }
+ | {
+ valid: false;
+ error: ProtectedActionErrorCode;
+ message: string;
+ };
+
+export interface ProtectedActionExecutionOptions {
+ authorizedMaintainers?: string[];
+ requireSignerAsExecutor?: boolean;
+}
+
const DEFAULT_EXPIRY_MS = 30 * 60 * 1000; // 30 minutes
-const ACTION_THRESHOLDS: Record = {
+export const ACTION_THRESHOLDS: Record = {
emergency_killswitch: 2,
protocol_rollback: 2,
reconciliation_repair: 2,
admin_config_override: 2,
quota_override: 1,
- dead_letter_purge: 1
+ dead_letter_purge: 1,
+ event_capacity_override: 2,
+ event_cancellation: 2,
+ fraud_lock_quarantine: 1,
+ batch_ticket_burn: 2,
+ organizer_fee_override: 2
};
/**
@@ -403,6 +444,143 @@ export class MaintainerApprovalManager {
return Array.from(this.intents.values()).filter(i => i.status === "pending" && i.expiresAt > now);
}
+ /**
+ * Returns the current timestamp according to the manager's clock configuration.
+ */
+ currentTime(): number {
+ return this.now();
+ }
+
+ /**
+ * Determines whether an intent is expired.
+ */
+ isExpired(intent: IntentRequest): boolean {
+ return intent.status === "expired" || this.now() > intent.expiresAt;
+ }
+
+ /**
+ * Verifies that a protected high-impact administrative action has a valid,
+ * unexpired, signed approval matching the target, action, parameters, and authorized actor.
+ * Time complexity: O(1).
+ * Space complexity: O(1).
+ */
+ verifyProtectedAction(
+ request: ProtectedActionRequest,
+ options?: ProtectedActionExecutionOptions
+ ): ProtectedActionVerificationResult {
+ if (!StrKey.isValidEd25519PublicKey(request.executorPublicKey)) {
+ return {
+ valid: false,
+ error: "unauthorized_actor",
+ message: `Invalid Ed25519 public key format for executor: ${request.executorPublicKey}`
+ };
+ }
+
+ if (options?.authorizedMaintainers && !options.authorizedMaintainers.includes(request.executorPublicKey)) {
+ return {
+ valid: false,
+ error: "unauthorized_actor",
+ message: `Executor ${request.executorPublicKey} is not in authorized maintainer allowlist`
+ };
+ }
+
+ const intent = this.intents.get(request.approvalId);
+ if (!intent) {
+ return {
+ valid: false,
+ error: "approval_not_found",
+ message: `Approval intent with ID '${request.approvalId}' was not found`
+ };
+ }
+
+ if (options?.requireSignerAsExecutor) {
+ const isSigner = intent.signatures.some(s => s.signerPublicKey === request.executorPublicKey) || intent.requestedBy === request.executorPublicKey;
+ if (!isSigner) {
+ return {
+ valid: false,
+ error: "unauthorized_actor",
+ message: `Executor ${request.executorPublicKey} is not a valid signer or requester for intent ${request.approvalId}`
+ };
+ }
+ }
+
+ if (intent.status === "executed" || intent.status === "executing" || this.receiptsByIntent.has(request.approvalId)) {
+ return {
+ valid: false,
+ error: "already_executed",
+ message: `Approval intent '${request.approvalId}' has already been executed (replay prevented)`
+ };
+ }
+
+ if (this.isExpired(intent)) {
+ intent.status = "expired";
+ return {
+ valid: false,
+ error: "approval_expired",
+ message: `Approval intent '${request.approvalId}' has expired`
+ };
+ }
+
+ if (intent.status !== "approved") {
+ return {
+ valid: false,
+ error: "approval_not_ready",
+ message: `Approval intent '${request.approvalId}' is not approved (current status: ${intent.status}, required: ${intent.requiredSignatures}, signed: ${intent.signatures.length})`
+ };
+ }
+
+ if (intent.action !== request.action) {
+ return {
+ valid: false,
+ error: "action_mismatch",
+ message: `Action mismatch: approval authorizes '${intent.action}', but request attempted '${request.action}'`
+ };
+ }
+
+ if (intent.target !== request.target) {
+ return {
+ valid: false,
+ error: "target_mismatch",
+ message: `Target mismatch: approval authorizes target '${intent.target}', but request attempted '${request.target}'`
+ };
+ }
+
+ if (request.parameters) {
+ for (const [key, val] of Object.entries(request.parameters)) {
+ if (JSON.stringify(intent.parameters[key]) !== JSON.stringify(val)) {
+ return {
+ valid: false,
+ error: "parameter_mismatch",
+ message: `Parameter '${key}' mismatch between approval intent and requested action`
+ };
+ }
+ }
+ }
+
+ return {
+ valid: true,
+ intent
+ };
+ }
+
+ /**
+ * Executes a protected administrative action after strictly validating cryptographic approval.
+ * Time complexity: O(1) + actionHandler execution time.
+ * Space complexity: O(1).
+ */
+ async executeProtectedAction(
+ request: ProtectedActionRequest,
+ actionHandler: (intent: IntentRequest) => Promise | T,
+ options?: ProtectedActionExecutionOptions
+ ): Promise<{ result: T; receipt: ApprovalReceipt }> {
+ const verification = this.verifyProtectedAction(request, options);
+ if (!verification.valid) {
+ throw new Error(`Protected action rejected [${verification.error}]: ${verification.message}`);
+ }
+
+ return this.executeIntent(request.approvalId, request.executorPublicKey, actionHandler);
+ }
+
/**
* Clears in-memory data (useful for test isolation).
*/
@@ -414,3 +592,27 @@ export class MaintainerApprovalManager {
}
export const maintainerApprovalManager = new MaintainerApprovalManager();
+
+/**
+ * Top-level convenience wrapper for verifying protected action approvals against the default or provided manager.
+ */
+export function verifyProtectedActionApproval(
+ request: ProtectedActionRequest,
+ manager: MaintainerApprovalManager = maintainerApprovalManager,
+ options?: ProtectedActionExecutionOptions
+): ProtectedActionVerificationResult {
+ return manager.verifyProtectedAction(request, options);
+}
+
+/**
+ * Top-level convenience wrapper for executing protected actions after approval verification.
+ */
+export async function executeProtectedAction(
+ request: ProtectedActionRequest,
+ actionHandler: (intent: IntentRequest) => Promise | T,
+ manager: MaintainerApprovalManager = maintainerApprovalManager,
+ options?: ProtectedActionExecutionOptions
+): Promise<{ result: T; receipt: ApprovalReceipt }> {
+ return manager.executeProtectedAction(request, actionHandler, options);
+}
+
diff --git a/core/network/NetworkProvider.tsx b/core/network/NetworkProvider.tsx
index 6c68c18..97620a9 100644
--- a/core/network/NetworkProvider.tsx
+++ b/core/network/NetworkProvider.tsx
@@ -49,9 +49,14 @@ function subscribe(onStoreChange: () => void): () => void {
};
}
-function getSnapshot(): StellarNetwork {
+export function resetNetworkStorageListeners(): void {
+ listeners.clear();
+}
+
+function getSnapshotForStorage(storage?: Storage): StellarNetwork {
try {
- return resolveNetwork(window.localStorage.getItem(NETWORK_STORAGE_KEY), DEFAULT_NETWORK);
+ const s = storage ?? (typeof window !== "undefined" ? window.localStorage : undefined);
+ return resolveNetwork(s?.getItem(NETWORK_STORAGE_KEY), DEFAULT_NETWORK);
} catch {
// Private windows and blocked site data both throw here.
return DEFAULT_NETWORK;
@@ -62,9 +67,10 @@ function getServerSnapshot(): StellarNetwork {
return DEFAULT_NETWORK;
}
-function writeNetwork(network: StellarNetwork): void {
+function writeNetworkForStorage(network: StellarNetwork, storage?: Storage): void {
try {
- window.localStorage.setItem(NETWORK_STORAGE_KEY, network);
+ const s = storage ?? (typeof window !== "undefined" ? window.localStorage : undefined);
+ s?.setItem(NETWORK_STORAGE_KEY, network);
} catch {
// The choice still applies for this session even if it cannot be persisted.
}
@@ -73,12 +79,16 @@ function writeNetwork(network: StellarNetwork): void {
export function NetworkProvider({
children,
- initialNetwork
+ initialNetwork,
+ storage: injectedStorage
}: {
children: React.ReactNode;
/** Forces a network regardless of stored preference. Used by tests. */
initialNetwork?: StellarNetwork;
+ /** Custom storage engine for test isolation and deterministic persistence (#155). */
+ storage?: Storage;
}) {
+ const getSnapshot = useCallback(() => getSnapshotForStorage(injectedStorage), [injectedStorage]);
const stored = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const [override, setOverride] = useState(initialNetwork);
const [epoch, setEpoch] = useState(0);
@@ -89,7 +99,7 @@ export function NetworkProvider({
setEpoch((current) => current + 1);
// Keep the in-memory choice authoritative even when storage is blocked.
setOverride(next);
- writeNetwork(next);
+ writeNetworkForStorage(next, injectedStorage);
// The Horizon client is memoised per network; a pruning job is enqueued as
// delayed maintenance work so rapid switches settle before the cache is torn down.
getMaintenanceFramework().enqueue({
@@ -97,7 +107,7 @@ export function NetworkProvider({
params: { network: next },
delayMs: PRUNE_HORIZON_CLIENTS_DELAY_MS
});
- }, [network]);
+ }, [network, injectedStorage]);
const value = useMemo(
() => ({
diff --git a/core/testing/index.ts b/core/testing/index.ts
index e711e05..e1024cc 100644
--- a/core/testing/index.ts
+++ b/core/testing/index.ts
@@ -1,4 +1,4 @@
-export { renderFeature, screen, waitFor, within, userEvent } from "@/core/testing/render";
+export { renderFeature, createIsolatedStorage, screen, waitFor, within, userEvent } from "@/core/testing/render";
export {
renderFeatureSlice,
FEATURE_CONTRACT_STATES,
diff --git a/core/testing/render.tsx b/core/testing/render.tsx
index 674e536..6ce8cf1 100644
--- a/core/testing/render.tsx
+++ b/core/testing/render.tsx
@@ -1,36 +1,64 @@
import { render, type RenderOptions, type RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactElement, ReactNode } from "react";
-import { NetworkProvider } from "@/core/network/NetworkProvider";
+import { NetworkProvider, resetNetworkStorageListeners } from "@/core/network/NetworkProvider";
import { NotificationProvider } from "@/core/notifications/NotificationProvider";
+import { NotificationStore } from "@/core/notifications/store";
import type { StellarNetwork } from "@/core/network/types";
+/**
+ * Creates an in-memory Storage fixture for deterministic test isolation (#155).
+ * Prevents persisted network, notification, and session test state from leaking across component tests.
+ */
+export function createIsolatedStorage(initialData: Record = {}): Storage {
+ const store = new Map(Object.entries(initialData));
+ return {
+ getItem: (key: string) => store.get(key) ?? null,
+ setItem: (key: string, value: string) => store.set(key, String(value)),
+ removeItem: (key: string) => store.delete(key),
+ clear: () => store.clear(),
+ get length() {
+ return store.size;
+ },
+ key: (index: number) => Array.from(store.keys())[index] ?? null
+ };
+}
+
export interface RenderFeatureOptions extends Omit {
network?: StellarNetwork;
+ /** Explicit storage fixture. Defaults to a fresh isolated memory storage instance (#155). */
+ storage?: Storage;
+ notificationStore?: NotificationStore;
}
/**
* Renders a feature component inside the same providers the real app uses,
- * and returns a bound `userEvent` instance.
+ * and returns a bound `userEvent` instance and the isolated storage fixture.
*
* Every slice's component tests should go through this helper so provider
- * wiring stays identical between the app and the test suite.
+ * wiring stays identical between the app and the test suite, while isolating
+ * storage state across runs.
*/
export function renderFeature(
ui: ReactElement,
- { network = "testnet", ...options }: RenderFeatureOptions = {}
-): RenderResult & { user: ReturnType } {
+ { network, storage: providedStorage, notificationStore, ...options }: RenderFeatureOptions = {}
+): RenderResult & { user: ReturnType; storage: Storage } {
+ const storage = providedStorage ?? createIsolatedStorage();
+ const notifStore = notificationStore ?? new NotificationStore(storage);
+
function Wrapper({ children }: { children: ReactNode }) {
return (
-
- {children}
+
+ {children}
);
}
const user = userEvent.setup();
- return { ...render(ui, { wrapper: Wrapper, ...options }), user };
+ return { ...render(ui, { wrapper: Wrapper, ...options }), user, storage };
}
+export { resetNetworkStorageListeners };
export * from "@testing-library/react";
export { userEvent };
+
diff --git a/core/ui/Button.tsx b/core/ui/Button.tsx
index 4829021..8e6c81c 100644
--- a/core/ui/Button.tsx
+++ b/core/ui/Button.tsx
@@ -27,7 +27,7 @@ export function Button({ className, variant = "primary", size = "md", ...props }
return (
);
}
diff --git a/core/ui/StatusMessage.tsx b/core/ui/StatusMessage.tsx
index 47f5c7e..b91e211 100644
--- a/core/ui/StatusMessage.tsx
+++ b/core/ui/StatusMessage.tsx
@@ -50,7 +50,7 @@ export const StatusMessage = forwardRef(
role={isError ? "alert" : "status"}
aria-live={isError ? "assertive" : "polite"}
className={cn(
- "flex gap-3 rounded-lg border p-4 shadow-[4px_4px_0_rgba(255,139,122,0.12)] focus:outline-none focus:ring-2 focus:ring-[#47a8c7]",
+ "flex gap-3 rounded-lg border p-4 shadow-[4px_4px_0_rgba(255,139,122,0.12)] focus:outline-none focus:ring-2 focus:ring-[#47a8c7] motion-reduce:transition-none motion-reduce:transform-none",
styles[type],
className
)}
diff --git a/core/ui/__tests__/reduced-motion.test.tsx b/core/ui/__tests__/reduced-motion.test.tsx
new file mode 100644
index 0000000..5a9ac82
--- /dev/null
+++ b/core/ui/__tests__/reduced-motion.test.tsx
@@ -0,0 +1,117 @@
+import { render, renderHook, screen, act } from "@testing-library/react";
+import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
+import { prefersReducedMotion, useReducedMotion } from "../accessibility";
+import { Skeleton } from "../Skeleton";
+import { Button } from "../Button";
+import { StatusMessage } from "../StatusMessage";
+
+describe("Reduced-Motion Preference Coverage (#153)", () => {
+ let originalMatchMedia: typeof window.matchMedia;
+
+ beforeEach(() => {
+ originalMatchMedia = window.matchMedia;
+ });
+
+ afterEach(() => {
+ window.matchMedia = originalMatchMedia;
+ });
+
+ function mockMatchMedia(matches: boolean) {
+ const listeners = new Set<(e: { matches: boolean }) => void>();
+ const mql = {
+ matches,
+ media: "(prefers-reduced-motion: reduce)",
+ onchange: null,
+ addEventListener: vi.fn((event: string, handler: (e: { matches: boolean }) => void) => {
+ if (event === "change") listeners.add(handler);
+ }),
+ removeEventListener: vi.fn((event: string, handler: (e: { matches: boolean }) => void) => {
+ if (event === "change") listeners.delete(handler);
+ }),
+ addListener: vi.fn((handler: (e: { matches: boolean }) => void) => {
+ listeners.add(handler);
+ }),
+ removeListener: vi.fn((handler: (e: { matches: boolean }) => void) => {
+ listeners.delete(handler);
+ }),
+ dispatchEvent: vi.fn(),
+ triggerChange: (nextMatches: boolean) => {
+ mql.matches = nextMatches;
+ listeners.forEach((listener) => listener({ matches: nextMatches }));
+ }
+ };
+
+ window.matchMedia = vi.fn().mockReturnValue(mql);
+ return mql;
+ }
+
+ describe("prefersReducedMotion() helper", () => {
+ it("returns false by default when media query matches is false", () => {
+ mockMatchMedia(false);
+ expect(prefersReducedMotion()).toBe(false);
+ });
+
+ it("returns true when media query matches is true", () => {
+ mockMatchMedia(true);
+ expect(prefersReducedMotion()).toBe(true);
+ });
+
+ it("falls back to defaultValue when matchMedia throws or is missing", () => {
+ // @ts-expect-error Intentionally invalid matchMedia for test
+ delete window.matchMedia;
+ expect(prefersReducedMotion(false)).toBe(false);
+ expect(prefersReducedMotion(true)).toBe(true);
+ });
+ });
+
+ describe("useReducedMotion() hook", () => {
+ it("returns initial reduced-motion state correctly", () => {
+ mockMatchMedia(true);
+ const { result } = renderHook(() => useReducedMotion());
+ expect(result.current).toBe(true);
+ });
+
+ it("updates dynamically when media query changes", () => {
+ const mql = mockMatchMedia(false);
+ const { result } = renderHook(() => useReducedMotion());
+ expect(result.current).toBe(false);
+
+ act(() => {
+ mql.triggerChange(true);
+ });
+
+ expect(result.current).toBe(true);
+ });
+
+ it("cleans up listener on unmount", () => {
+ const mql = mockMatchMedia(false);
+ const { unmount } = renderHook(() => useReducedMotion());
+ unmount();
+ expect(mql.removeEventListener).toHaveBeenCalledWith("change", expect.any(Function));
+ });
+ });
+
+ describe("Component reduced-motion class assertions", () => {
+ it("Skeleton includes motion-reduce:animate-none while retaining data-contract-state", () => {
+ const { container } = render();
+ const skeleton = container.querySelector('[data-contract-state="loading"]');
+ expect(skeleton).not.toBeNull();
+ expect(skeleton).toHaveAttribute("data-contract-state", "loading");
+ expect(skeleton?.className).toContain("motion-reduce:animate-none");
+ expect(skeleton?.className).toContain("animate-pulse");
+ });
+
+ it("Button includes motion-reduce:transition-none", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Submit" });
+ expect(button.className).toContain("motion-reduce:transition-none");
+ });
+
+ it("StatusMessage includes motion-reduce:transition-none and motion-reduce:transform-none", () => {
+ render();
+ const status = screen.getByRole("status");
+ expect(status.className).toContain("motion-reduce:transition-none");
+ expect(status.className).toContain("motion-reduce:transform-none");
+ });
+ });
+});
diff --git a/core/ui/accessibility.ts b/core/ui/accessibility.ts
index 3248264..6e05e78 100644
--- a/core/ui/accessibility.ts
+++ b/core/ui/accessibility.ts
@@ -8,6 +8,8 @@
* - Screen reader-friendly loading/error/success states
*/
+import { useEffect, useState } from "react";
+
export type AriaLive = 'polite' | 'assertive' | 'off';
export type AsyncStatus = 'idle' | 'loading' | 'success' | 'error';
@@ -168,3 +170,53 @@ export function getFocusableElements(container: HTMLElement): HTMLElement[] {
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter(isFocusable);
}
+
+/**
+ * Query whether the user has requested reduced motion (#153).
+ * Returns defaultValue (false by default) if window or matchMedia is unavailable.
+ */
+export function prefersReducedMotion(defaultValue = false): boolean {
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
+ return defaultValue;
+ }
+ try {
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ } catch {
+ return defaultValue;
+ }
+}
+
+/**
+ * React hook to observe user reduced-motion preference changes (#153).
+ * Preserves state visibility without relying on animation completion.
+ */
+export function useReducedMotion(defaultValue = false): boolean {
+ const [reducedMotion, setReducedMotion] = useState(() =>
+ prefersReducedMotion(defaultValue)
+ );
+
+ useEffect(() => {
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
+ return;
+ }
+
+ const mediaQueryList = window.matchMedia("(prefers-reduced-motion: reduce)");
+ setReducedMotion(mediaQueryList.matches);
+
+ const listener = (event: MediaQueryListEvent | MediaQueryList) => {
+ setReducedMotion(event.matches);
+ };
+
+ if (typeof mediaQueryList.addEventListener === "function") {
+ mediaQueryList.addEventListener("change", listener as (e: MediaQueryListEvent) => void);
+ return () =>
+ mediaQueryList.removeEventListener("change", listener as (e: MediaQueryListEvent) => void);
+ } else if (typeof (mediaQueryList as any).addListener === "function") {
+ (mediaQueryList as any).addListener(listener);
+ return () => (mediaQueryList as any).removeListener(listener);
+ }
+ }, []);
+
+ return reducedMotion;
+}
+
diff --git a/core/ui/index.ts b/core/ui/index.ts
index 3670090..200e3f4 100644
--- a/core/ui/index.ts
+++ b/core/ui/index.ts
@@ -22,3 +22,5 @@ export { Skeleton, SkeletonRows } from "@/core/ui/Skeleton";
export { StatusMessage, type StatusMessageProps, type StatusType } from "@/core/ui/StatusMessage";
export { ToolCard } from "@/core/ui/ToolCard";
export { VirtualizedList, type VirtualizedListProps } from "@/core/ui/VirtualizedList";
+export { prefersReducedMotion, useReducedMotion } from "@/core/ui/accessibility";
+
diff --git a/docs/BUNDLE_BUDGET.md b/docs/BUNDLE_BUDGET.md
index 65847e7..717f464 100644
--- a/docs/BUNDLE_BUDGET.md
+++ b/docs/BUNDLE_BUDGET.md
@@ -17,6 +17,31 @@ With an architecture built around independent, lazy-loaded feature slices, a gen
This covers typical tool implementations including form validation, API calls, formatting, and result display.
+## Route-Level Initial-Load Budget (#159)
+
+**220 KB for the shared route shell** (`/tools/[slug]`)
+
+Per-feature slice budgets do not expose shared-chunk regression or the cost paid before a user reaches a tool. Utix measures route-level initial-load overhead from the build manifest:
+- Shared entrypoint chunks (`/_app`, webpack, root layout files)
+- Dynamic route container (`/tools/[slug]/page.tsx`)
+- Baseline tracking with stable tolerance ($\pm 5\%$) to prevent false positives from build hashes.
+
+## Documented Exception Path for SDK-Heavy Tools (#159)
+
+Tools that perform deep cryptographic decoding or compile Soroban contract ASTs require heavier dependencies (such as `@stellar/stellar-sdk` and `@stellar/stellar-xdr`). These tools have an explicit, documented exception path governed by [`scripts/bundle-budget-exceptions.json`](../scripts/bundle-budget-exceptions.json):
+
+| Feature | Exception Budget | Justification |
+| :--- | :--- | :--- |
+| `xdr-inspector` | 350 KB | Deep recursive XDR parser and Soroban contract type schemas |
+| `soroban-decoder` | 300 KB | Full Soroban ScVal decoding and contract auth types |
+| `transaction-decoder` | 250 KB | Transaction envelope and operation XDR schemas |
+| `soroban-spec-viewer` | 280 KB | Contract function specifications and environment interface bindings |
+
+### Requesting an Exception
+1. Ensure the heavy dependency cannot be split or loaded on-demand.
+2. Add an entry in `scripts/bundle-budget-exceptions.json` with `budgetKB`, `justification`, and `approved: true`.
+3. If the tool stays within its exception ceiling, `scripts/bundle-budget.mjs` marks it as `EXCEPTION_APPROVED`. If it exceeds the ceiling, it fails with `EXCEEDED`.
+
## Feature-Specific Budgets
Some tools have adjusted budgets based on their requirements:
diff --git a/docs/MAINTAINER_APPROVAL_FLOW.md b/docs/MAINTAINER_APPROVAL_FLOW.md
index 732252b..92ed2f5 100644
--- a/docs/MAINTAINER_APPROVAL_FLOW.md
+++ b/docs/MAINTAINER_APPROVAL_FLOW.md
@@ -16,6 +16,11 @@ The approval flow ensures that privileged operations cannot be executed unilater
| `admin_config_override` | Modifies core runtime administrative configuration | 2 maintainers |
| `quota_override` | Temporarily raises or lowers tenant rate limits / quotas | 1 maintainer |
| `dead_letter_purge` | Purges or requeues quarantined worker jobs | 1 maintainer |
+| `event_capacity_override` | Overrides event maximum inventory / venue capacity limit | 2 maintainers |
+| `event_cancellation` | Cancels event and triggers escrow refund / settlement procedures | 2 maintainers |
+| `fraud_lock_quarantine` | Quarantines ticket batches or accounts under fraud investigation | 1 maintainer |
+| `batch_ticket_burn` | Cryptographically burns batches of counterfeit / revoked tickets | 2 maintainers |
+| `organizer_fee_override` | Overrides custom protocol fee basis points for organizers | 2 maintainers |
## Architecture & Lifecycle
@@ -82,8 +87,49 @@ const receipts = approvalManager.listReceipts({ action: "emergency_killswitch" }
const receipt = approvalManager.getReceipt(receiptId);
```
+### 6. Protected Administrative Action Verification
+For high-impact mutations (e.g. event capacity overrides, cancellations, fraud quarantines, ticket burns, fee adjustments), operations verify approval validity prior to mutation execution:
+
+```typescript
+import {
+ verifyProtectedActionApproval,
+ executeProtectedAction,
+ type ProtectedActionRequest
+} from "@/core/auth/maintainer-approval";
+
+const request: ProtectedActionRequest = {
+ action: "event_capacity_override",
+ target: "event-arena-101",
+ parameters: { maxTickets: 30000 },
+ approvalId: "intent-1700000000000-xyz",
+ executorPublicKey: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H"
+};
+
+// 1. Explicit verification check (O(1) time and space complexity)
+const verification = verifyProtectedActionApproval(request);
+if (!verification.valid) {
+ console.error(`Rejected with error [${verification.error}]: ${verification.message}`);
+}
+
+// 2. Direct guarded execution
+const { result, receipt } = await executeProtectedAction(request, async (intent) => {
+ return await applyCapacityOverride(intent.target, intent.parameters.maxTickets);
+});
+```
+
+#### Guardrail Verification Error Codes:
+- `approval_not_found`: Approval intent ID does not exist.
+- `approval_not_ready`: Threshold of required maintainer signatures not yet reached.
+- `approval_expired`: Intent lifetime exceeded (`Date.now() > expiresAt`).
+- `already_executed`: Action was already executed or receipt already generated (replay prevention).
+- `action_mismatch`: Approval action does not match requested action.
+- `target_mismatch`: Approval target does not match requested target.
+- `parameter_mismatch`: Request parameters deviate from approved parameter values.
+- `unauthorized_actor`: Executor key is malformed or not permitted in the maintainer allowlist.
+
## Operational Edge Cases and Recovery
1. **Maintainer Key Compromise**: If a maintainer's key is suspected compromised, other maintainers must reject any pending intents signed by that key and update the authorized maintainer allowlist.
2. **Expired Intents**: Intents not signed before their expiration time automatically transition to `expired` and cannot be executed. A new intent with fresh nonce must be proposed.
3. **Execution Failure Recovery**: If an action fails mid-execution, the receipt records `status: "failed"` with full stack trace and error message, allowing operators to diagnose root cause and plan recovery.
+
diff --git a/docs/REDUCED_MOTION.md b/docs/REDUCED_MOTION.md
new file mode 100644
index 0000000..5e78b9c
--- /dev/null
+++ b/docs/REDUCED_MOTION.md
@@ -0,0 +1,97 @@
+# Reduced-Motion Preference Support
+
+Utix provides comprehensive reduced-motion accessibility support across all shared UI primitives, dynamic routes, and feature slices in compliance with **WCAG 2.1 AA (Success Criterion 2.3.3: Animation from Interactions)**.
+
+## Core Principle: State Visibility Without Animation Dependency
+
+Users who configure their operating system or browser to prefer reduced motion (`prefers-reduced-motion: reduce`) must not experience vestibular disorientation or delayed feedback:
+
+1. **Immediate State Visibility**: Async loading, status reveals, and interactive feedback do not depend on animation completion.
+2. **Static Affordances**: Loading skeletons preserve their exact dimensional layout and `data-contract-state="loading"` attribute as solid, non-pulsing placeholders.
+3. **Instant Transitions**: Hover, active, focus, and status transition delays are reduced to zero.
+
+---
+
+## Shared Testable Utilities (`core/ui/accessibility.ts`)
+
+### 1. `prefersReducedMotion(defaultValue = false): boolean`
+
+Synchronous query evaluating the media query `(prefers-reduced-motion: reduce)`.
+
+```typescript
+import { prefersReducedMotion } from "@/core/ui";
+
+const shouldAnimate = !prefersReducedMotion();
+```
+
+- **Default Behavior**: Defaults to `false` (motion enabled) when `window` or `window.matchMedia` is unavailable (e.g. during server-side rendering).
+- **Safe Fallback**: Any media-query evaluation error gracefully returns the default value without throwing.
+
+### 2. `useReducedMotion(defaultValue = false): boolean`
+
+React hook that subscribes to OS and browser preference changes in real time.
+
+```typescript
+import { useReducedMotion } from "@/core/ui";
+
+export function CustomVisualizer() {
+ const reducedMotion = useReducedMotion();
+
+ return (
+
+ Content
+
+ );
+}
+```
+
+- **Lifecycle**: Attaches a change listener to `window.matchMedia('(prefers-reduced-motion: reduce)')` and automatically cleans up on unmount.
+- **Cross-Browser**: Supports modern `addEventListener('change', ...)` as well as legacy `addListener(...)`.
+
+---
+
+## Component Level Guarantees
+
+| Component | Normal Motion | Reduced Motion (`prefers-reduced-motion: reduce`) |
+| :--- | :--- | :--- |
+| **`Skeleton`** | `animate-pulse` | `motion-reduce:animate-none` (static solid placeholder) |
+| **`Button`** | `transition duration-200` | `motion-reduce:transition-none` (instant state switch) |
+| **`StatusMessage`** | Smooth reveal & shadow | `motion-reduce:transition-none motion-reduce:transform-none` |
+| **Global Shell** | Smooth scroll & keyframe animations | Instant scroll (`scroll-behavior: auto !important`), zero animation duration |
+
+---
+
+## Global CSS Overrides (`app/globals.css`)
+
+```css
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+
+ .animate-fade-in-up,
+ .animate-float,
+ .animate-pulse {
+ animation: none !important;
+ }
+}
+```
+
+---
+
+## Verification & Testing
+
+Run the dedicated reduced-motion test suite:
+
+```bash
+npx vitest run core/ui/__tests__/reduced-motion.test.tsx
+```
diff --git a/scripts/bundle-budget-exceptions.json b/scripts/bundle-budget-exceptions.json
new file mode 100644
index 0000000..abda6d8
--- /dev/null
+++ b/scripts/bundle-budget-exceptions.json
@@ -0,0 +1,25 @@
+{
+ "$comment": "Documented exception path for SDK-heavy tools (#159). Tools requiring deep XDR parsing or Soroban contract bindings have an explicit approved ceiling.",
+ "exceptions": {
+ "xdr-inspector": {
+ "budgetKB": 350,
+ "justification": "Deep recursive XDR parser and Soroban contract type schemas",
+ "approved": true
+ },
+ "soroban-decoder": {
+ "budgetKB": 300,
+ "justification": "Full Soroban ScVal decoding and contract auth types",
+ "approved": true
+ },
+ "transaction-decoder": {
+ "budgetKB": 250,
+ "justification": "Transaction envelope and operation XDR schemas",
+ "approved": true
+ },
+ "soroban-spec-viewer": {
+ "budgetKB": 280,
+ "justification": "Contract function specifications and environment interface bindings",
+ "approved": true
+ }
+ }
+}
diff --git a/scripts/bundle-budget.mjs b/scripts/bundle-budget.mjs
index 0cb9f5d..841d5df 100644
--- a/scripts/bundle-budget.mjs
+++ b/scripts/bundle-budget.mjs
@@ -1,13 +1,13 @@
#!/usr/bin/env node
/**
- * Per-feature bundle size budget enforcement
+ * Per-feature and route-level initial-load bundle size budget enforcement (#159)
*
- * Analyzes Next.js build output to compute per-feature-slice bundle sizes
- * and enforces documented budgets.
+ * Analyzes Next.js build output to compute:
+ * 1. Per-feature-slice bundle sizes with documented exception path for SDK-heavy tools.
+ * 2. Route-level initial-load costs capturing shared chunk regression and pre-flight overhead.
*
- * This prevents any single feature from quietly pulling in heavy dependencies
- * that bloat its lazy-loaded chunk.
+ * Prevents stealth dependency bloat while providing an objective review path for SDK-heavy tools.
*/
import { readFileSync, existsSync, writeFileSync } from 'fs';
@@ -19,12 +19,15 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, '..');
-const BASELINE_PATH = join(ROOT, 'bundle-baseline.json');
-const BUILD_MANIFEST_PATH = join(ROOT, '.next/build-manifest.json');
+export const BASELINE_PATH = join(ROOT, 'bundle-baseline.json');
+export const BUILD_MANIFEST_PATH = join(ROOT, '.next/build-manifest.json');
+export const EXCEPTIONS_PATH = join(ROOT, 'scripts/bundle-budget-exceptions.json');
-const DEFAULT_BUDGET_KB = 150;
+export const DEFAULT_BUDGET_KB = 150;
+export const ROUTE_INITIAL_LOAD_BUDGET_KB = 220;
+export const STABLE_TOLERANCE_PERCENT = 5;
-const FEATURE_BUDGETS = {
+export const FEATURE_BUDGETS = {
'address-validator': 120,
'balance-viewer': 150,
'trustline-checker': 100,
@@ -34,29 +37,43 @@ const FEATURE_BUDGETS = {
'testnet-faucet': 90,
};
-function loadBaseline() {
- if (!existsSync(BASELINE_PATH)) {
+export function loadBaseline(baselinePath = BASELINE_PATH) {
+ if (!existsSync(baselinePath)) {
return {};
}
try {
- return JSON.parse(readFileSync(BASELINE_PATH, 'utf-8'));
+ return JSON.parse(readFileSync(baselinePath, 'utf-8'));
} catch (error) {
- console.warn(`Warning: Could not load baseline from ${BASELINE_PATH}`);
+ console.warn(`Warning: Could not load baseline from ${baselinePath}`);
return {};
}
}
-function loadBuildManifest() {
- if (!existsSync(BUILD_MANIFEST_PATH)) {
- throw new Error(`Build manifest not found at ${BUILD_MANIFEST_PATH}. Run 'npm run build' first.`);
+export function loadExceptions(exceptionsPath = EXCEPTIONS_PATH) {
+ if (!existsSync(exceptionsPath)) {
+ return {};
}
- return JSON.parse(readFileSync(BUILD_MANIFEST_PATH, 'utf-8'));
+ try {
+ const data = JSON.parse(readFileSync(exceptionsPath, 'utf-8'));
+ return data.exceptions || {};
+ } catch (error) {
+ console.warn(`Warning: Could not load exceptions from ${exceptionsPath}`);
+ return {};
+ }
}
-function computeChunkSize(chunkPath) {
- const fullPath = join(ROOT, '.next', chunkPath);
+export function loadBuildManifest(manifestPath = BUILD_MANIFEST_PATH) {
+ if (!existsSync(manifestPath)) {
+ throw new Error(`Build manifest not found at ${manifestPath}. Run 'npm run build' first.`);
+ }
+
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
+}
+
+export function computeChunkSize(chunkPath, rootDir = ROOT) {
+ const fullPath = join(rootDir, '.next', chunkPath);
if (!existsSync(fullPath)) {
return 0;
@@ -66,7 +83,45 @@ function computeChunkSize(chunkPath) {
return stats.length;
}
-function analyzeFeatureSlices(manifest) {
+export function analyzeRouteInitialLoad(manifest, rootDir = ROOT) {
+ const sharedChunkSet = new Set();
+ const chunkDetails = [];
+
+ // 1. Shared app entrypoint chunks
+ const appChunks = manifest.pages?.['/_app'] || [];
+ for (const chunk of appChunks) {
+ sharedChunkSet.add(chunk);
+ }
+
+ // 2. Root main files if exposed by Next.js manifest
+ const rootFiles = manifest.rootMainFiles || [];
+ for (const chunk of rootFiles) {
+ sharedChunkSet.add(chunk);
+ }
+
+ // 3. Common dynamic route host shell
+ const toolRouteChunks = manifest.pages?.['/tools/[slug]'] || [];
+ for (const chunk of toolRouteChunks) {
+ sharedChunkSet.add(chunk);
+ }
+
+ let totalSize = 0;
+ for (const chunk of sharedChunkSet) {
+ const size = computeChunkSize(chunk, rootDir);
+ if (size > 0) {
+ totalSize += size;
+ chunkDetails.push({ path: chunk, size });
+ }
+ }
+
+ return {
+ totalSize,
+ sizeKB: Math.round(totalSize / 1024),
+ chunks: chunkDetails,
+ };
+}
+
+export function analyzeFeatureSlices(manifest, rootDir = ROOT) {
const featureSizes = {};
for (const [route, chunks] of Object.entries(manifest.pages || {})) {
@@ -85,7 +140,7 @@ function analyzeFeatureSlices(manifest) {
continue;
}
- const size = computeChunkSize(chunk);
+ const size = computeChunkSize(chunk, rootDir);
if (size > 0) {
totalSize += size;
chunkDetails.push({ path: chunk, size });
@@ -107,7 +162,7 @@ function analyzeFeatureSlices(manifest) {
if (!match) continue;
const featureName = match[1];
- const size = computeChunkSize(`static/chunks/${chunk}.js`);
+ const size = computeChunkSize(`static/chunks/${chunk}.js`, rootDir);
if (size > 0) {
if (!featureSizes[featureName]) {
@@ -128,27 +183,51 @@ function analyzeFeatureSlices(manifest) {
return featureSizes;
}
-function checkBudgets(featureSizes, baseline) {
+export function checkBudgets(featureSizes, routeInitialLoad = { totalSize: 0, sizeKB: 0, chunks: [] }, baseline = {}, exceptions = {}) {
const results = [];
let hasViolations = false;
for (const [feature, data] of Object.entries(featureSizes)) {
+ if (feature.startsWith('_')) continue;
+
const sizeKB = Math.round(data.totalSize / 1024);
- const budgetKB = FEATURE_BUDGETS[feature] || DEFAULT_BUDGET_KB;
+ const standardBudgetKB = FEATURE_BUDGETS[feature] || DEFAULT_BUDGET_KB;
const baselineSizeKB = baseline[feature] ? Math.round(baseline[feature].totalSize / 1024) : 0;
const delta = sizeKB - baselineSizeKB;
- const status = sizeKB > budgetKB ? 'EXCEEDED' : 'OK';
- const withinBudget = sizeKB <= budgetKB;
-
- if (!withinBudget) {
+ const exception = exceptions[feature];
+ const isApprovedException = Boolean(exception?.approved);
+ const exceptionCeilingKB = exception?.budgetKB || standardBudgetKB;
+
+ let status = 'OK';
+ let withinBudget = true;
+ let justification = null;
+
+ if (sizeKB <= standardBudgetKB) {
+ status = 'OK';
+ withinBudget = true;
+ } else if (isApprovedException && sizeKB <= exceptionCeilingKB) {
+ status = 'EXCEPTION_APPROVED';
+ withinBudget = true;
+ justification = exception.justification;
+ } else if (isApprovedException && sizeKB > exceptionCeilingKB) {
+ status = 'EXCEEDED (over exception ceiling)';
+ withinBudget = false;
+ hasViolations = true;
+ justification = exception.justification;
+ } else {
+ status = 'EXCEEDED';
+ withinBudget = false;
hasViolations = true;
}
results.push({
feature,
sizeKB,
- budgetKB,
+ budgetKB: isApprovedException ? exceptionCeilingKB : standardBudgetKB,
+ standardBudgetKB,
+ isException: isApprovedException,
+ justification,
baselineSizeKB,
delta,
status,
@@ -156,18 +235,57 @@ function checkBudgets(featureSizes, baseline) {
chunks: data.chunks,
});
}
+
+ // Check Route-level Initial-Load budget (#159)
+ const routeBaselineKB = baseline._route_initial_load?.sizeKB || 0;
+ const routeDelta = routeInitialLoad.sizeKB - routeBaselineKB;
+ const routeWithinBudget = routeInitialLoad.sizeKB <= ROUTE_INITIAL_LOAD_BUDGET_KB;
+ const routeStatus = routeWithinBudget ? 'OK' : 'EXCEEDED';
+
+ if (!routeWithinBudget) {
+ hasViolations = true;
+ }
+
+ const routeResult = {
+ name: 'Route Initial-Load (/tools/[slug])',
+ sizeKB: routeInitialLoad.sizeKB,
+ budgetKB: ROUTE_INITIAL_LOAD_BUDGET_KB,
+ baselineSizeKB: routeBaselineKB,
+ delta: routeDelta,
+ status: routeStatus,
+ withinBudget: routeWithinBudget,
+ chunks: routeInitialLoad.chunks,
+ };
- return { results, hasViolations };
+ return { results, routeResult, hasViolations };
}
-function formatReport(results, hasViolations) {
+export function formatReport(results, routeResult, hasViolations) {
const lines = [];
lines.push('='.repeat(80));
- lines.push('BUNDLE SIZE BUDGET REPORT');
+ lines.push('UTIX INITIAL-LOAD & FEATURE BUNDLE BUDGET REPORT (#159)');
lines.push('='.repeat(80));
lines.push('');
+ // 1. Route-Level Initial-Load Section
+ lines.push('ROUTE INITIAL-LOAD COSTS');
+ lines.push('-'.repeat(80));
+ if (routeResult) {
+ const routeName = routeResult.name.padEnd(35);
+ const size = `${routeResult.sizeKB} KB`.padEnd(15);
+ const budget = `${routeResult.budgetKB} KB`.padEnd(15);
+ lines.push(`${routeName}${size}${budget}${routeResult.status}`);
+ if (routeResult.delta !== 0 && routeResult.baselineSizeKB > 0) {
+ const deltaStr = routeResult.delta > 0 ? `+${routeResult.delta}` : `${routeResult.delta}`;
+ lines.push(` (${deltaStr} KB from baseline: ${routeResult.baselineSizeKB} KB)`);
+ }
+ }
+ lines.push('');
+
+ // 2. Per-Feature Slices Section
+ lines.push('FEATURE SLICE BUDGETS');
+ lines.push('-'.repeat(80));
if (results.length === 0) {
lines.push('No feature slices detected in build output.');
lines.push('This is normal for a fresh build without features.');
@@ -183,6 +301,10 @@ function formatReport(results, hasViolations) {
lines.push(`${featureName}${size}${budget}${status}`);
+ if (result.isException && result.justification) {
+ lines.push(` [Exception Approved: ${result.justification}]`);
+ }
+
if (result.delta !== 0 && result.baselineSizeKB > 0) {
const deltaStr = result.delta > 0 ? `+${result.delta}` : `${result.delta}`;
lines.push(` (${deltaStr} KB from baseline)`);
@@ -198,16 +320,16 @@ function formatReport(results, hasViolations) {
lines.push('BUDGET VIOLATIONS DETECTED');
lines.push('');
lines.push('To resolve:');
- lines.push(' 1. Review dependencies added to the feature');
- lines.push(' 2. Consider code-splitting or lazy loading');
- lines.push(' 3. Request budget increase if justified (update FEATURE_BUDGETS)');
+ lines.push(' 1. Review dependencies added to the feature or shared route layout');
+ lines.push(' 2. Ensure SDK imports are isolated to lazy-loaded panel chunks');
+ lines.push(' 3. If feature is SDK-heavy (e.g. XDR/Soroban), submit an exception in bundle-budget-exceptions.json');
lines.push('');
}
return lines.join('\n');
}
-function saveBaseline(featureSizes) {
+export function saveBaseline(featureSizes, routeInitialLoad = { totalSize: 0, sizeKB: 0 }) {
const baseline = {};
for (const [feature, data] of Object.entries(featureSizes)) {
@@ -217,6 +339,14 @@ function saveBaseline(featureSizes) {
timestamp: new Date().toISOString(),
};
}
+
+ if (routeInitialLoad && routeInitialLoad.totalSize > 0) {
+ baseline._route_initial_load = {
+ totalSize: routeInitialLoad.totalSize,
+ sizeKB: routeInitialLoad.sizeKB,
+ timestamp: new Date().toISOString(),
+ };
+ }
writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2));
console.log(`\nBaseline saved to ${BASELINE_PATH}`);
@@ -229,18 +359,20 @@ async function main() {
try {
const baseline = loadBaseline();
+ const exceptions = loadExceptions();
const manifest = loadBuildManifest();
const featureSizes = analyzeFeatureSlices(manifest);
- const { results, hasViolations } = checkBudgets(featureSizes, baseline);
+ const routeInitialLoad = analyzeRouteInitialLoad(manifest);
+ const { results, routeResult, hasViolations } = checkBudgets(featureSizes, routeInitialLoad, baseline, exceptions);
if (jsonOutput) {
- console.log(JSON.stringify({ results, hasViolations }, null, 2));
+ console.log(JSON.stringify({ results, routeResult, hasViolations }, null, 2));
} else {
- console.log(formatReport(results, hasViolations));
+ console.log(formatReport(results, routeResult, hasViolations));
}
if (updateBaseline) {
- saveBaseline(featureSizes);
+ saveBaseline(featureSizes, routeInitialLoad);
}
if (hasViolations && !updateBaseline) {
@@ -253,4 +385,7 @@ async function main() {
}
}
-main();
+// Only invoke CLI runner if executed directly
+if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
+ main();
+}
diff --git a/vitest.config.ts b/vitest.config.ts
index 29273a1..6ec29a1 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -13,7 +13,8 @@ export default defineConfig({
setupFiles: ["./core/testing/setup.ts"],
include: [
"features/**/__tests__/**/*.test.{ts,tsx}",
- "core/**/__tests__/**/*.test.{ts,tsx}"
+ "core/**/__tests__/**/*.test.{ts,tsx}",
+ "__tests__/**/*.test.{ts,tsx}"
],
exclude: ["**/node_modules/**", "**/e2e/**", "**/.next/**"],
coverage: {