diff --git a/server/src/app-config/parsers.ts b/server/src/app-config/parsers.ts index d42bcd0f5..009a929d6 100644 --- a/server/src/app-config/parsers.ts +++ b/server/src/app-config/parsers.ts @@ -1,7 +1,6 @@ /** * Pure env-string coercion helpers shared by the whole config layer (derive.ts, - * env.schema.ts) and — until their call sites migrate — by legacy readers like - * src/mcp/config.ts. Kept free of imports so units can test them in isolation. + * env.schema.ts). Kept free of imports so units can test them in isolation. * * Each helper reproduces a coercion family that already exists in the codebase. * Parity is law: do NOT "fix" a family's quirks here (e.g. `numberOr` treating @@ -102,7 +101,6 @@ export function parseDurationMs(value: string): number | null { /** * Session idle TTL in SECONDS via MCP_SESSION_TTL, default 1 hour, clamped to * 24h so a milliseconds-value typo can't produce a 1000-hour session. - * (Same contract as src/mcp/config.ts, which commit 7 retires in favor of this.) */ export function resolveSessionTtlMs(raw: string | undefined): number { const parsed = Number.parseInt(raw ?? ''); diff --git a/server/src/mcp/config.ts b/server/src/mcp/config.ts deleted file mode 100644 index 09a4bc4ca..000000000 --- a/server/src/mcp/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * MCP tuning knobs from the environment (#1414). The parsers moved into the - * app-config layer (src/app-config/parsers.ts) — this module re-exports them - * so existing import paths keep working. mcp/index.ts consumes the derived - * values via readEnv().mcp. - */ -export { resolveSessionTtlMs, resolveKeepaliveMs } from '../app-config/parsers'; diff --git a/server/src/nest/plugins/host/rpc-kit/row-shape.ts b/server/src/nest/plugins/host/rpc-kit/row-shape.ts deleted file mode 100644 index 74524bdb3..000000000 --- a/server/src/nest/plugins/host/rpc-kit/row-shape.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * `toCamelCase` — the R-facade utility from Plan 3j's Task 6 (design spec D8, program - * `docs/superpowers/plans/2026-09-25-orm-phase3j-plugins.md`). It is DORMANT: built, - * tested and exported, but wired into ZERO of the 23 `*.rpc.ts` plugin-host handlers. - * `plugin-sdk/src/index.ts`'s published row types (`User`, `Place`, `Day`, `Reservation`, - * `PackingItem`, `TripFile`, `BudgetItem`, …) are snake_case today, and neither the SDK - * nor its tests carry any camelCase precedent — so applying this additively to a - * handler's return value would grow the plugin contract's visible surface with fields no - * published type, plugin author or test fixture expects, for no proven benefit. Whether - * TREK should start publishing camelCase copies going forward is a real product decision - * (see the plan's "For the user" note), not one an ORM-migration task makes unilaterally. - * This function exists so that decision, if it is ever made, is a one-line change at - * `HostSurfaceRpc#getUser`'s single return statement rather than new plumbing to design - * and land. It is a pure function — no class, no DI, no constructor — matching the - * `num`/`str`/`asPayload` shape in the sibling `rpc-params.ts` every one of the 23 - * `*.rpc.ts` files already imports the same way, specifically so it never ripples through - * `tests/helpers/plugin-host.ts`/`rpc-host-deps.ts`'s hand-construction of all 23 classes. - * - * Scope, operate ONLY on a DB-row-shaped object the HOST itself assembled (e.g. - * `SELECT id, username, display_name, avatar FROM users …`). NEVER apply it to an opaque - * JSON-text value a PLUGIN wrote and the host merely parsed (`MetaRpc#get`/`#list`'s - * values, a plugin's own `config`) — a generic key transform would silently rewrite keys - * inside that stored blob, which is exactly why a generic deep-key transform was rejected - * project-wide (design spec D1/D8). - */ - -/** - * Returns a shallow, additive camelCase COPY of `value`'s keys: every key containing `_` - * gets a camelCase sibling holding the same value (`display_name` -> `displayName`, - * `content_base64` -> `contentBase64`); the original snake_case key is never removed or - * replaced. A key with no underscore is left exactly as it is — no identity-transform - * copy is ever added for it. The conversion is ONE level deep only: a nested object, a - * `Date`, a `Buffer` or any other VALUE under a key is passed through completely - * unchanged (never recursed into, never spread) — only the row's own TOP-LEVEL key names - * are touched. This is the scope choice inventory §13 flagged as an open ambiguity in the - * design spec's "shallow" wording; a future caller wiring this in should expect a - * `user_profile: { first_name: 'A' }` row to gain `userProfile` pointing at the SAME - * nested object, with `first_name` inside it untouched — never a recursively-mapped - * `userProfile: { firstName: 'A' }`. - * - * An array is mapped element-wise, each element run back through `toCamelCase`. `null`, - * `undefined`, a `Date`, a `Buffer` and any other non-plain-object value passed as the - * top-level argument pass through completely unchanged, defensively — a caller mistake - * here (this is never called with anything but a row object in the codebase today, since - * it is unwired) must not throw into an RPC dispatch. - */ -export function toCamelCase>(value: readonly T[]): (T & Record)[]; -export function toCamelCase>(value: T): T & Record; -export function toCamelCase(value: V): V; -export function toCamelCase(value: unknown): unknown { - if (Array.isArray(value)) return value.map((item) => toCamelCase(item)); - - if ( - value === null || - typeof value !== 'object' || - value instanceof Date || - Buffer.isBuffer(value) - ) { - return value; - } - - const row = value as Record; - const camelCopies: Record = {}; - for (const key of Object.keys(row)) { - if (!key.includes('_')) continue; - const camelKey = snakeKeyToCamel(key); - if (camelKey !== key) camelCopies[camelKey] = row[key]; - } - return { ...row, ...camelCopies }; -} - -/** - * `display_name` -> `displayName`; `content_base64` -> `contentBase64`. A digit right - * after an underscore follows the same rule and needs no special case — uppercasing a - * digit is a no-op, so `place_2_id` -> `place2Id`. - */ -function snakeKeyToCamel(key: string): string { - return key.replace(/_([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase()); -} diff --git a/server/tests/unit/mcp/config.test.ts b/server/tests/unit/mcp/config.test.ts deleted file mode 100644 index 5ddef4dad..000000000 --- a/server/tests/unit/mcp/config.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * MCP env tuning knobs (#1414): MCP_SESSION_TTL + MCP_SSE_KEEPALIVE parsing. - */ -import { describe, it, expect } from 'vitest'; -import { resolveSessionTtlMs, resolveKeepaliveMs } from '../../../src/mcp/config'; - -describe('resolveSessionTtlMs', () => { - it('defaults to 1 hour when unset or invalid', () => { - expect(resolveSessionTtlMs(undefined)).toBe(60 * 60 * 1000); - expect(resolveSessionTtlMs('')).toBe(60 * 60 * 1000); - expect(resolveSessionTtlMs('nope')).toBe(60 * 60 * 1000); - expect(resolveSessionTtlMs('0')).toBe(60 * 60 * 1000); - expect(resolveSessionTtlMs('-5')).toBe(60 * 60 * 1000); - }); - - it('reads seconds from MCP_SESSION_TTL', () => { - expect(resolveSessionTtlMs('3600')).toBe(3600 * 1000); - expect(resolveSessionTtlMs('120')).toBe(120 * 1000); - }); - - it('clamps to 24h so a milliseconds typo cannot yield a 1000-hour session', () => { - expect(resolveSessionTtlMs('3600000')).toBe(24 * 60 * 60 * 1000); - }); -}); - -describe('resolveKeepaliveMs', () => { - it('defaults to 25s when unset or invalid', () => { - expect(resolveKeepaliveMs(undefined)).toBe(25_000); - expect(resolveKeepaliveMs('abc')).toBe(25_000); - expect(resolveKeepaliveMs('-1')).toBe(25_000); - }); - - it('reads seconds and allows 0 to disable', () => { - expect(resolveKeepaliveMs('30')).toBe(30_000); - expect(resolveKeepaliveMs('0')).toBe(0); - }); -}); diff --git a/server/tests/unit/plugins/rpc-kit/row-shape.test.ts b/server/tests/unit/plugins/rpc-kit/row-shape.test.ts deleted file mode 100644 index ee1645394..000000000 --- a/server/tests/unit/plugins/rpc-kit/row-shape.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * `toCamelCase` (Plan 3j Task 6, design spec D8) — the DORMANT plugin-RPC facade - * utility. This is its entire test surface: it is wired into no `*.rpc.ts` handler, so - * no RPC-handler test changes alongside it. See `row-shape.ts`'s own docstring for why - * it exists unwired and what it must never be applied to. - */ -import { describe, it, expect, expectTypeOf } from 'vitest'; -import { toCamelCase } from '../../../../src/nest/plugins/host/rpc-kit/row-shape'; - -describe('toCamelCase', () => { - it('ROWSHAPE-001 a flat row with several snake_case keys gets both cases, originals unchanged', () => { - const row = { id: 1, display_name: 'Ada', avatar_url: null as string | null }; - const mapped = toCamelCase(row); - expect(mapped).toEqual({ - id: 1, - display_name: 'Ada', - avatar_url: null, - displayName: 'Ada', - avatarUrl: null, - }); - // Originals present and unchanged, not moved. - expect(mapped.display_name).toBe('Ada'); - expect(mapped.avatar_url).toBeNull(); - }); - - it('ROWSHAPE-002 a key with no underscore passes through untouched — no identity-transform copy', () => { - const row = { id: 1, username: 'ada' }; - const mapped = toCamelCase(row); - expect(mapped).toEqual({ id: 1, username: 'ada' }); - expect(Object.keys(mapped).sort()).toEqual(['id', 'username']); - }); - - it('ROWSHAPE-003 an array of rows maps element-wise', () => { - const rows = [ - { id: 1, display_name: 'Ada' }, - { id: 2, display_name: 'Bea' }, - ]; - const mapped = toCamelCase(rows); - expect(mapped).toEqual([ - { id: 1, display_name: 'Ada', displayName: 'Ada' }, - { id: 2, display_name: 'Bea', displayName: 'Bea' }, - ]); - }); - - it('ROWSHAPE-004 a nested object stays untouched — proves the shallow, one-level scope', () => { - const profile = { first_name: 'Ada', last_name: 'Lovelace' }; - const row = { id: 1, user_profile: profile }; - const mapped = toCamelCase(row); - // The camel sibling points at the exact SAME object — never a recursively-mapped copy. - expect(mapped.userProfile).toBe(profile); - expect(mapped.user_profile).toBe(profile); - // The nested object itself gained no camelCase siblings. - expect(Object.keys(profile).sort()).toEqual(['first_name', 'last_name']); - expect((profile as Record).firstName).toBeUndefined(); - }); - - it('ROWSHAPE-005 null, undefined and a primitive pass through safely, unchanged', () => { - expect(toCamelCase(null)).toBeNull(); - expect(toCamelCase(undefined)).toBeUndefined(); - expect(toCamelCase('a string')).toBe('a string'); - expect(toCamelCase(42)).toBe(42); - expect(toCamelCase(true)).toBe(true); - }); - - it('ROWSHAPE-006 a Date or Buffer value — at the top level or nested under a key — passes through unchanged', () => { - const when = new Date('2026-09-24T00:00:00.000Z'); - const bytes = Buffer.from('hello'); - - // Top-level argument. - expect(toCamelCase(when)).toBe(when); - expect(toCamelCase(bytes)).toBe(bytes); - - // Nested under a row key — never spread, never recursed into. - const row = { id: 1, created_at: when, blob_data: bytes }; - const mapped = toCamelCase(row); - expect(mapped.createdAt).toBe(when); - expect(mapped.created_at).toBe(when); - expect(mapped.blobData).toBe(bytes); - expect(mapped.blob_data).toBe(bytes); - }); - - it('ROWSHAPE-007 a digit immediately after an underscore follows the same rule (no special case)', () => { - const row = { place_2_id: 7 }; - const mapped = toCamelCase(row); - expect(mapped).toEqual({ place_2_id: 7, place2Id: 7 }); - }); - - it('ROWSHAPE-008 an empty object round-trips to an equal empty object', () => { - expect(toCamelCase({})).toEqual({}); - }); - - it('ROWSHAPE-009 an empty array round-trips to an empty array', () => { - expect(toCamelCase([])).toEqual([]); - }); - - it('ROWSHAPE-010 type-level: the SDK-shaped User row keeps its snake_case members AND exposes the camel siblings', () => { - // Mirrors plugin-sdk/src/index.ts's published `User` shape exactly (id, username, - // display_name, avatar, plus its index signature) — the one row HostSurfaceRpc#getUser - // (HR1) would wrap if the facade were ever wired in. - interface SdkUserRow { - id: number; - username?: string; - display_name?: string | null; - avatar?: string | null; - [k: string]: unknown; - } - const row: SdkUserRow = { id: 1, username: 'ada', display_name: 'Ada Lovelace', avatar: null }; - const mapped = toCamelCase(row); - - // The mapped type is `SdkUserRow & Record`: every original member - // stays typed, and any camelCase sibling (`displayName`, unlisted on `SdkUserRow` - // itself) resolves through the added index signature rather than failing to compile — - // exactly the shape a plugin-sdk follow-up release's row types would need. - expectTypeOf(mapped).toEqualTypeOf>(); - expectTypeOf(mapped.display_name).toEqualTypeOf(); - expectTypeOf(mapped.displayName).toEqualTypeOf(); - - expect(mapped.display_name).toBe('Ada Lovelace'); - expect(mapped.displayName).toBe('Ada Lovelace'); - }); -});