open-design/apps/daemon/tests/active-context-routes.test.ts
Golden Obsidian LLC aa0616062d
refactor(daemon): introduce HTTP Request Adapter + typed Deps (#2636)
* refactor(daemon): introduce HTTP Request Adapter + typed Deps (proof on active-context-routes)

Adds a typed HTTP boundary Adapter under apps/daemon/src/http/ that replaces
the untyped ServerContext service-locator pattern (30+ fields, mostly any)
for route handlers. Routes become pure (input, deps) -> Result<output>
functions, unit-testable without Express or supertest.

Six new modules under apps/daemon/src/http/:
  - types.ts        Result<T,E>, ok(), err(), JsonRouteSpec, Handler,
                    RouteInputContext, HttpMethod, InputParser
  - parse.ts        rawInput(req), validationError(message, issues?)
  - response.ts     sendJson(), sendApiError(), statusForError() +
                    ERROR_STATUS_BY_CODE map
  - origin-guard.ts guardSameOrigin(req, origin) wrapping isLocalSameOrigin
                    as a Result
  - adapter.ts      defineJsonRoute(), mountJsonRoute() (only place that
                    knows about req/res)
  - index.ts        barrel

active-context-routes.ts migrated as proof of pattern. parsePostActive(),
handlePostActive(), handleGetActive() are now pure functions; postActiveRoute
and getActiveRoute are exported route specs. The wire signature
registerActiveContextRoutes(app, ctx) is preserved so server.ts is untouched.

Spec at specs/current/daemon-http-adapter.md captures the strangler migration
order for the remaining route files (mcp-routes, chat-routes, artifact
routes, etc.) and a StreamRoute follow-up where the Run Orchestrator lands.

Wire-format note: cross-origin response moves from the legacy
{ error: 'cross-origin request rejected' } shape to the structured
{ error: { code: 'FORBIDDEN', message: ... } } shape. Backwards-compatible
via the existing CompatibleErrorResponse = ApiErrorResponse | LegacyErrorResponse
union in @open-design/contracts.

Validation:
  - pnpm install (post-rebase, exit 0)
  - pnpm --filter @open-design/daemon typecheck (both tsconfig.json and
    tsconfig.tests.json silent => pass)
  - pnpm --filter @open-design/daemon test: 15 new tests pass
    (tests/http/adapter.test.ts + tests/active-context-routes.test.ts).
    84 pre-existing failures across 23 files are unchanged and unrelated
    to this PR (Windows symlink / short-name / colon-in-filename, upstream
    behavior drift, missing plugin marketplace fixtures, and a freshly-
    added tools-connectors-cli suite of 38 failures that landed during
    the rebase).

Sharpens W4/W5 of specs/current/maintainability-roadmap.md and unlocks
W6 (Run Orchestrator).

* chore: add core-js, electron-winstaller, protobufjs, sharp to pnpm.onlyBuiltDependencies
2026-05-22 15:20:15 +08:00

126 lines
4.3 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { getActiveRoute, postActiveRoute } from '../src/active-context-routes.js';
import { ACTIVE_CONTEXT_TTL_MS } from '../src/constants.js';
interface MockStore {
current:
| {
projectId: string;
fileName: string | null;
ts: number;
}
| null;
}
function makeDeps(now = 1_000) {
const store: MockStore = { current: null };
// Annotated return type widens the mock so `.mockReturnValue(null)` is
// allowed by the inferred Mock type later in the file.
const getProject = vi.fn(
(_db: unknown, id: string): { name?: string | null } | null | undefined => ({
name: `Project ${id}`,
}),
);
return {
store,
db: { fake: true },
getProject,
now: () => now,
};
}
const EMPTY_INPUT = { body: {}, query: {}, params: {} };
describe('active context — POST /api/active', () => {
it('clears the store when body.active === false', async () => {
const deps = makeDeps();
deps.store.current = { projectId: 'p1', fileName: 'a.html', ts: 1 };
const parsed = postActiveRoute.parse({ ...EMPTY_INPUT, body: { active: false } });
expect(parsed.ok).toBe(true);
if (!parsed.ok) return;
const out = await postActiveRoute.handle(parsed.value, deps);
expect(out).toEqual({ ok: true, value: { active: false } });
expect(deps.store.current).toBeNull();
});
it('rejects when projectId is missing', () => {
const parsed = postActiveRoute.parse({ ...EMPTY_INPUT, body: {} });
expect(parsed.ok).toBe(false);
if (parsed.ok) return;
expect(parsed.error.code).toBe('BAD_REQUEST');
expect(parsed.error.message).toBe('projectId is required');
});
it('stores projectId + fileName + timestamp on success', async () => {
const deps = makeDeps(5_000);
const parsed = postActiveRoute.parse({
...EMPTY_INPUT,
body: { projectId: 'p1', fileName: 'index.html' },
});
expect(parsed.ok).toBe(true);
if (!parsed.ok) return;
const out = await postActiveRoute.handle(parsed.value, deps);
expect(out).toEqual({
ok: true,
value: { active: true, projectId: 'p1', fileName: 'index.html', ts: 5_000 },
});
expect(deps.store.current).toEqual({ projectId: 'p1', fileName: 'index.html', ts: 5_000 });
});
it('treats empty fileName as null', async () => {
const deps = makeDeps(7_000);
const parsed = postActiveRoute.parse({
...EMPTY_INPUT,
body: { projectId: 'p1', fileName: '' },
});
expect(parsed.ok).toBe(true);
if (!parsed.ok) return;
const out = await postActiveRoute.handle(parsed.value, deps);
expect(out.ok).toBe(true);
if (!out.ok) return;
expect(out.value).toMatchObject({ active: true, fileName: null });
});
});
describe('active context — GET /api/active', () => {
it('returns inactive when nothing is stored', async () => {
const deps = makeDeps();
const out = await getActiveRoute.handle(undefined, deps);
expect(out).toEqual({ ok: true, value: { active: false } });
});
it('returns inactive and clears when TTL has expired', async () => {
const deps = makeDeps(10_000 + ACTIVE_CONTEXT_TTL_MS);
deps.store.current = { projectId: 'p1', fileName: null, ts: 9_000 };
const out = await getActiveRoute.handle(undefined, deps);
expect(out).toEqual({ ok: true, value: { active: false } });
expect(deps.store.current).toBeNull();
});
it('returns active payload with project name + ageMs when fresh', async () => {
const deps = makeDeps(2_500);
deps.store.current = { projectId: 'p7', fileName: 'plan.md', ts: 2_000 };
const out = await getActiveRoute.handle(undefined, deps);
expect(out.ok).toBe(true);
if (!out.ok) return;
expect(out.value).toEqual({
active: true,
projectId: 'p7',
projectName: 'Project p7',
fileName: 'plan.md',
ts: 2_000,
ageMs: 500,
});
expect(deps.getProject).toHaveBeenCalledWith(deps.db, 'p7');
});
it('tolerates a missing project (projectName = null)', async () => {
const deps = makeDeps(3_000);
deps.getProject.mockReturnValue(null);
deps.store.current = { projectId: 'p9', fileName: null, ts: 2_500 };
const out = await getActiveRoute.handle(undefined, deps);
expect(out.ok).toBe(true);
if (!out.ok) return;
expect(out.value).toMatchObject({ active: true, projectName: null });
});
});