Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions scripts/mutation/equivalent-mutants.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ src/shared/accounting/rows.ts:262:58 ?? → || # selectById (await …)[0]: T
src/shared/accounting/rows.ts:133:72 ?? → || # renderInsert guard?.args: InValue[]|undefined — an array is always truthy
src/shared/accounting/manual-entries.ts:209:29 ?? → || # guard error message `transfer.kind ?? ""`: string|undefined, "" ?? "" === "" || ""
src/shared/checkout-ledger.ts:35:72 ?? → || # find(...)?.amount: number|undefined; the only falsy-non-null amount is 0 and 0 ?? 0 === 0 || 0
src/features/settings-bundles.ts:264:40 ?? → || # bundle is a readonly string[] or undefined; every present array, including [], is truthy
src/shared/accounting/queries.ts:162:26 || → && # transferActivityBounds: MIN(occurred_at) and MAX(occurred_at) over one table are NULL together (both iff the table is empty), so either-null and both-null coincide

# Collection cache generation values are only compared for equality. Their
Expand Down Expand Up @@ -917,8 +918,8 @@ src/shared/dates.ts:432:30 1 → 0 # bookedRangeLabel's `fallbackDurationDay

# Admin segment dispatch (admin/index.ts) — two provably-equivalent survivors
# from the routeAdmin/adminPathSegment run over the manifest + footer suites.
src/features/admin/index.ts:257:21 ?? → || # `path.split("/")[2] ?? ""`: the RHS is "" and the only falsy string is "", so `x ?? ""` and `x || ""` agree on every string|undefined input
src/features/admin/index.ts:299:55 return null → return undefined # routeAdmin's only consumer folds the result with `?? notFoundResponse()` (features/index.ts routeMainApp), and ?? treats null and undefined identically
src/features/admin/index.ts:107:21 ?? → || # `path.split("/")[2] ?? ""`: the RHS is "" and the only falsy string is "", so `x ?? ""` and `x || ""` agree on every string|undefined input
src/features/admin/index.ts:126:43 ?? → || # areasBySegment values are arrays, so every present value is truthy and a miss is undefined

# Logger (logger.ts) — two provably-equivalent survivors from the run over the
# logger suites.
Expand Down Expand Up @@ -950,11 +951,10 @@ src/shared/db/attendees/select.ts:383:26 ?? → || # query.join: AttendeeJoin
# placeholder-free message (locked by the "fast path renders every
# placeholder-free message exactly as full ICU would" test), so a mutant that
# makes needsIcu always-true is a pure performance change with no observable effect.
src/shared/i18n.ts:160:16 { → "" # needsIcu `msg.includes("{")` → `includes("")`: `includes("")` is always true, so needsIcu returns true for every message and all copy is routed through ICU. ICU output equals the fast-path string for plain copy, so no input changes t()'s output — only whether a formatter is built
src/shared/i18n.ts:160:37 ' → "" # needsIcu `msg.includes("'")` → `includes("")`: same as above — always-true makes every message use ICU, which is output-identical to the fast path; unobservable
src/shared/i18n.ts:166:37 ?? → || # resolveMessage `locales[locale]?.[key] ?? locales.en?.[key]`: `??` and `||` differ only if the left side is "" (falsy but not nullish). No catalog message is "" (verified: 0 empty values), and only "en" is ever registered so left and right are the same object anyway — the left is a non-empty string or undefined on every reachable input, where `??` and `||` agree
src/shared/i18n.ts:105:19 → "mutated" # buildReplacer `const [from = ""` default: `pair.split("|")` always returns at least one element, so element 0 (`from`) is always present and the default is never used — dead code, unobservable
src/shared/i18n.ts:240:21 → "mutated" # parseAcceptLanguage `const [lang = ""` default: `part.trim().split(";")` always returns at least one element, so `lang` (element 0) is always present and the default is never used — dead code, unobservable
src/shared/i18n.ts:237:16 { → "" # needsIcu `msg.includes("{")` → `includes("")`: `includes("")` is always true, so needsIcu returns true for every message and all copy is routed through ICU. ICU output equals the fast-path string for plain copy, so no input changes t()'s output — only whether a formatter is built
src/shared/i18n.ts:237:37 ' → "" # needsIcu `msg.includes("'")` → `includes("")`: same as above — always-true makes every message use ICU, which is output-identical to the fast path; unobservable
src/shared/i18n.ts:180:19 → "mutated" # buildReplacer `const [from = ""` default: `pair.split("|")` always returns at least one element, so element 0 (`from`) is always present and the default is never used — dead code, unobservable
src/shared/i18n.ts:319:21 → "mutated" # parseAcceptLanguage `const [lang = ""` default: `part.trim().split(";")` always returns at least one element, so `lang` (element 0) is always present and the default is never used — dead code, unobservable
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

# listing edit panel (features/admin/listing-page-management-panels.ts) — group-id fallback.
src/features/admin/listing-page-management-panels.ts:46:39 ?? → || # `selectedGroupIds ?? ctxData.selectedGroupIds`: selectedGroupIds is number[]|undefined — a submitted array (even []) is always truthy, so both operators return it unchanged, and undefined maps to the stored ids under both. No input distinguishes them (same class as attributes.tsx:151:61)
Expand Down
7 changes: 3 additions & 4 deletions scripts/mutation/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,10 @@ const runTestBatch = (
"--no-check",
"--allow-all",
"--parallel",
// Load the fast `toContain` override in every isolate — the barrel that
// used to pull it into each test file's graph is gone, so without this
// mutation batches fall back to the slow matcher. See test-harness.ts.
// Match the standard harness: install the fast matcher and preload the
// complete catalog for tests that render templates without routing.
"--preload",
"./test/test-utils/fast-expect.ts",
"./test/test-utils/preload.ts",
"--v8-flags=--expose-gc",
...batch,
],
Expand Down
11 changes: 4 additions & 7 deletions scripts/test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,11 @@ const buildDenoTestArgs = (
"--allow-sys",
"--allow-ffi",
"--parallel",
// Install the fast `toContain` override (test/test-utils/fast-expect.ts) in
// every test isolate. Deno runs each test file in its own isolate — and
// spreads them across worker processes under --parallel — so a side-effect
// import in one file never reaches the others. --preload runs the module
// before every test module, giving the whole suite the fast matcher without
// each file having to import it. See fast-expect.ts for why it exists.
// Install shared test state in every isolate before test-module imports:
// the fast `toContain` override and the full locale catalog used by direct
// template tests that do not enter through a production route loader.
"--preload",
"./test/test-utils/fast-expect.ts",
"./test/test-utils/preload.ts",
// Expose `globalThis.gc` so the test DB layer can reclaim libsql's
// file-descriptor leak (a fresh client per test + every interactive
// transaction leaks an fd that close() never releases — only GC does). Under
Expand Down
13 changes: 4 additions & 9 deletions src/features/admin/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from "#shared/config.ts";
import { settings } from "#shared/db/settings.ts";
import { EMAIL_PROVIDER_LABELS, getHostEmailConfig } from "#shared/email.ts";
import { ensureGuideMessages } from "#shared/guide-messages.ts";
import {
adminFormattingHelpPage,
adminGuidePage,
Expand All @@ -20,9 +19,7 @@ import {
/**
* Handle GET /admin/guide
*/
const handleAdminGuideGet = sessionPage(async (session) => {
// The guide's ~120KB of translations are loaded on demand, not at cold boot.
await ensureGuideMessages();
const handleAdminGuideGet = sessionPage((session) => {
const hostEmail = getHostEmailConfig();
return adminGuidePage(session, {
builderEnabled: isBuilderEnabled(),
Expand All @@ -45,11 +42,9 @@ const handleAdminGuideGet = sessionPage(async (session) => {
* markdown field hints. Content roles (incl. editors) may open it; it shows only
* the editor-safe Text Formatting section, never the full staff guide.
*/
const handleAdminFormattingGet = contentPage(async (session) => {
// The formatting help renders a guide section, so it needs the guide bundle.
await ensureGuideMessages();
return adminFormattingHelpPage(session);
});
const handleAdminFormattingGet = contentPage((session) =>
adminFormattingHelpPage(session),
);

/** Guide routes */
export const adminHandlers = handlersFor("guide")({
Expand Down
114 changes: 74 additions & 40 deletions src/features/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
*/

import { once } from "#fp";
import { withMessageGroups } from "#i18n";
import {
ADMIN_BASE_MESSAGE_GROUPS,
type AdminAreaMessageGroup,
} from "#locales/groups.ts";
import type { MessageGroup } from "#locales/manifest.ts";
import { routeMapForArea } from "#routes/admin/handlers.ts";
import { createRouter } from "#routes/router.ts";
import type { PathMethodRoute } from "#routes/types.ts";
Expand All @@ -30,36 +36,47 @@ type HandlerMap = Record<string, (...args: never[]) => unknown>;
/** One admin area's lazy route-ID handlers. */
export type AdminAreaLoader = {
load: () => Promise<HandlerMap>;
messageGroups: readonly AdminAreaMessageGroup[];
};

/** Declare an area without importing its handlers until that area is requested. */
const area = <M extends { adminHandlers: HandlerMap }>(
load: () => Promise<M>,
): AdminAreaLoader => ({ load: async () => (await load()).adminHandlers });
messageGroups: readonly AdminAreaMessageGroup[] = [],
): AdminAreaLoader => ({
load: async () => (await load()).adminHandlers,
messageGroups,
});

// Import specifiers stay literal so esbuild can still bundle every target.
export const ADMIN_AREA_LOADERS: Record<AdminAreaId, AdminAreaLoader> = {
apiKeys: area(() => import("#routes/admin/api-keys.ts")),
attendeeNotes: area(() => import("#routes/admin/attendee-notes.ts")),
attendeeNotes: area(
() => import("#routes/admin/attendee-notes.ts"),
["notes"],
),
attendeeRefunds: area(() => import("#routes/admin/attendee-refunds.ts")),
attendees: area(() => import("#routes/admin/attendees.ts")),
attendees: area(() => import("#routes/admin/attendees.ts"), ["notes"]),
attributes: area(() => import("#routes/admin/attributes.ts")),
auth: area(() => import("#routes/admin/auth.ts")),
backup: area(() => import("#routes/admin/backup.ts")),
builder: area(() => import("#routes/admin/builder.ts")),
backup: area(() => import("#routes/admin/backup.ts"), ["backup"]),
builder: area(() => import("#routes/admin/builder.ts"), ["builder"]),
builtSites: area(() => import("#routes/admin/built-sites.ts")),
bulkActions: area(() => import("#routes/admin/bulk-actions.ts")),
bulkEmail: area(() => import("#routes/admin/bulk-email.ts")),
bulkActions: area(
() => import("#routes/admin/bulk-actions.ts"),
["bulk-actions"],
),
bulkEmail: area(() => import("#routes/admin/bulk-email.ts"), ["bulk-email"]),
calendar: area(() => import("#routes/admin/calendar.ts")),
catalogTransfer: area(
() => import("#routes/admin/catalog-transfer/routes.ts"),
),
contactHistory: area(() => import("#routes/admin/contact-history.ts")),
dashboard: area(() => import("#routes/admin/dashboard.ts")),
debug: area(() => import("#routes/admin/debug.ts")),
deliveries: area(() => import("#routes/admin/deliveries.ts")),
groups: area(() => import("#routes/admin/groups.ts")),
guide: area(() => import("#routes/admin/guide.ts")),
debug: area(() => import("#routes/admin/debug.ts"), ["debug"]),
deliveries: area(() => import("#routes/admin/deliveries.ts"), ["deliveries"]),
groups: area(() => import("#routes/admin/groups.ts"), ["groups"]),
guide: area(() => import("#routes/admin/guide.ts"), ["builder", "guide"]),
holidays: area(() => import("#routes/admin/holidays.ts")),
images: area(() => import("#routes/admin/images.ts")),
ledger: area(() => import("#routes/admin/ledger.ts")),
Expand All @@ -68,20 +85,20 @@ export const ADMIN_AREA_LOADERS: Record<AdminAreaId, AdminAreaLoader> = {
markdownPreview: area(() => import("#routes/admin/markdown-preview.ts")),
modifiers: area(() => import("#routes/admin/modifiers.ts")),
news: area(() => import("#routes/admin/news.ts")),
privacy: area(() => import("#routes/admin/privacy.ts")),
privacy: area(() => import("#routes/admin/privacy.ts"), ["privacy"]),
questions: area(() => import("#routes/admin/questions.ts")),
scanner: area(() => import("#routes/admin/scanner.ts")),
seeds: area(() => import("#routes/admin/seeds.ts")),
servicing: area(() => import("#routes/admin/servicing.tsx")),
servicing: area(() => import("#routes/admin/servicing.tsx"), ["servicing"]),
sessions: area(() => import("#routes/admin/sessions.ts")),
settings: area(() => import("#routes/admin/settings.ts")),
settingsLogistics: area(() => import("#routes/admin/settings-logistics.ts")),
settingsStatuses: area(() => import("#routes/admin/settings-statuses.ts")),
site: area(() => import("#routes/admin/site.ts")),
sitePages: area(() => import("#routes/admin/site-pages.ts")),
sms: area(() => import("#routes/admin/sms.ts")),
support: area(() => import("#routes/admin/support.ts")),
update: area(() => import("#routes/admin/update.ts")),
support: area(() => import("#routes/admin/support.ts"), ["support"]),
update: area(() => import("#routes/admin/update.ts"), ["update"]),
users: area(() => import("#routes/admin/users.ts")),
};

Expand All @@ -93,10 +110,12 @@ export const adminPathSegment = (path: string): string =>
* One lazy router per segment, derived from the manifest at module load
* (pure and cheap — no area module is imported until its `once` fires).
*/
const buildSegmentRouters = (): Record<
string,
() => Promise<PathMethodRoute>
> => {
type AdminSegment = {
load: () => Promise<PathMethodRoute>;
messageGroups: readonly MessageGroup[];
};

const buildSegmentRouters = (): Record<string, AdminSegment> => {
const areasBySegment: Record<
string,
Array<{ id: AdminAreaId; loader: AdminAreaLoader }>
Expand All @@ -109,16 +128,24 @@ const buildSegmentRouters = (): Record<
areasBySegment[segment] = list;
}
}
const routers: Record<string, () => Promise<PathMethodRoute>> = {};
const routers: Record<string, AdminSegment> = {};
for (const [segment, areas] of Object.entries(areasBySegment)) {
routers[segment] = once(async () => {
const maps = await Promise.all(
areas.map(async ({ id, loader }) =>
routeMapForArea(id, await loader.load()),
),
);
return createRouter(Object.assign({}, ...maps));
});
routers[segment] = {
load: once(async () => {
const maps = await Promise.all(
areas.map(async ({ id, loader }) =>
routeMapForArea(id, await loader.load()),
),
);
return createRouter(Object.assign({}, ...maps));
}),
messageGroups: [
...new Set([
...ADMIN_BASE_MESSAGE_GROUPS,
...areas.flatMap(({ loader }) => loader.messageGroups),
]),
],
};
}
return routers;
};
Expand All @@ -139,19 +166,26 @@ export const routeAdmin: PathMethodRoute = async (
// An unknown segment 404s before any session work, so probing traffic
// never costs a session lookup.
const segment = adminPathSegment(path);
if (!Object.hasOwn(segmentRouters, segment)) return null;
const segmentRouter = Object.hasOwn(segmentRouters, segment)
? segmentRouters[segment]
: undefined;
if (!segmentRouter) return null;

// Query recording is turned on earlier (prepareRequestEnvironment) for admin
// GETs, so the route's settings load is captured. Here we only unlock the
// footer for back-office staff — delivery agents and content editors (who are
// excluded from operational/debug access) never see the SQL/cache debug menu.
const { getAuthenticatedSession } = await import("#routes/auth.ts");
const session = await getAuthenticatedSession(request);
return await withMessageGroups(segmentRouter.messageGroups, async () => {
// Query recording is turned on earlier (prepareRequestEnvironment) for admin
// GETs, so the route's settings load is captured. Here we only unlock the
// footer for back-office staff — delivery agents and content editors (who are
// excluded from operational/debug access) never see the SQL/cache debug menu.
const { getAuthenticatedSession } = await import("#routes/auth.ts");
const session = await getAuthenticatedSession(request);

if (method === "GET" && session && isStaffRole(session.adminLevel)) {
enableFooterDebug();
}
if (method === "GET" && session && isStaffRole(session.adminLevel)) {
enableFooterDebug();
}

const router = await segmentRouters[segment]!();
return router(request, path, method, server);
// Copy is loaded before importing an area: some area modules translate form
// schemas at module evaluation time.
const router = await segmentRouter.load();
return router(request, path, method, server);
});
};
Loading