Files
openmaic/OpenMAIC/packages/@openmaic/storage/test/runtime-reference-server.test.ts
2026-08-16 14:58:47 +08:00

851 lines
31 KiB
TypeScript

import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http';
import { RUNTIME_DSL_VERSION } from '@openmaic/dsl';
import { IDBFactory } from 'fake-indexeddb';
import { describe, expect, test, vi } from 'vitest';
import { BrowserRuntimeStore } from '../src/runtime/browser.js';
import { HttpRuntimeStore } from '../src/runtime/http.js';
import type { RuntimePayloadValidator, RuntimeStore } from '../src/runtime/types.js';
import { createRuntimeHttpHandler } from '../src/server/index.js';
import {
createReferenceRuntimeServer,
type ConnectableQueryable,
} from '../src/server/reference.js';
import { makeRecordInit, makeSession, runRuntimeStoreContract } from './runtime-contract.js';
const BASE_URL = 'http://runtime-reference.invalid';
function handlerFetch(
handler: RequestListener,
authorizationFor: (request: Request) => Promise<string | undefined>,
): typeof globalThis.fetch {
return async (input, init) => {
const request = new Request(input, init);
const url = new URL(request.url);
const authorization = await authorizationFor(request);
const body = await request.text();
const headers = Object.fromEntries(request.headers.entries());
if (authorization !== undefined) headers.authorization = authorization;
const fakeRequest = {
method: request.method,
url: `${url.pathname}${url.search}`,
headers,
async *[Symbol.asyncIterator]() {
if (body !== '') yield Buffer.from(body);
},
} as unknown as IncomingMessage;
return new Promise<Response>((resolve, reject) => {
let status = 200;
let responseHeaders: Record<string, string> = {};
let responseBody: string | undefined;
let headersSent = false;
const fakeResponse = {
get headersSent() {
return headersSent;
},
writeHead(nextStatus: number, nextHeaders?: Record<string, string>) {
status = nextStatus;
responseHeaders = nextHeaders ?? {};
headersSent = true;
return this;
},
end(chunk?: string | Buffer) {
responseBody = chunk === undefined ? undefined : chunk.toString();
resolve(
new Response(status === 204 ? null : responseBody, {
status,
headers: responseHeaders,
}),
);
return this;
},
destroy(error?: Error) {
reject(error ?? new Error('response destroyed'));
return this;
},
} as unknown as ServerResponse;
try {
handler(fakeRequest, fakeResponse);
} catch (error) {
reject(error);
}
});
};
}
function bearerLearner(req: IncomingMessage): { learnerKey: string } | undefined {
const authorization = req.headers.authorization;
if (typeof authorization !== 'string' || !authorization.startsWith('Bearer ')) return undefined;
const learnerKey = authorization.slice('Bearer '.length);
return learnerKey === '' ? undefined : { learnerKey };
}
async function contractCredential(request: Request, store: RuntimeStore): Promise<string> {
const url = new URL(request.url);
const path = url.pathname;
if (request.method === 'POST' && path === '/runtime/sessions') {
const body = (await request.clone().json()) as { learnerKey: string };
return `Bearer ${body.learnerKey || 'invalid-envelope'}`;
}
const listedLearner = path.match(/^\/runtime\/stages\/[^/]+\/learners\/([^/]+)/)?.[1];
if (listedLearner !== undefined) return `Bearer ${decodeURIComponent(listedLearner)}`;
const sessionId = path.match(/^\/runtime\/sessions\/([^/]+)/)?.[1];
if (sessionId !== undefined) {
const session = await store.getSession(decodeURIComponent(sessionId));
if (session !== undefined) return `Bearer ${session.learnerKey}`;
}
return 'Bearer contract-operator';
}
runRuntimeStoreContract('reference HTTP handler', () => {
const backingStore = new BrowserRuntimeStore({ indexedDB: new IDBFactory() });
const handler = createRuntimeHttpHandler(backingStore, {
authenticate: async (req) => bearerLearner(req),
authorizeMerge: async () => true,
// The shared contract includes deleteAllRuntime, so this explicitly grants
// its test-only operator principal access to the admin route.
authorizeAdmin: async () => true,
});
return new HttpRuntimeStore({
baseUrl: BASE_URL,
fetch: handlerFetch(handler, (request) => contractCredential(request, backingStore)),
});
});
describe('reference HTTP handler records-route existence concealment', () => {
function makeHarness() {
const backingStore = new BrowserRuntimeStore({ indexedDB: new IDBFactory() });
const handler = createRuntimeHttpHandler(backingStore, {
authenticate: async (req) => bearerLearner(req),
});
const fetchAs = (authorization: string) => handlerFetch(handler, async () => authorization);
return { backingStore, fetchAs };
}
test('absent and foreign sessions answer the records route identically', async () => {
const { backingStore, fetchAs } = makeHarness();
await backingStore.createSession(
makeSession({ id: 'victim-session', learnerKey: 'learner-victim' }),
);
const absent = await fetchAs('Bearer learner-probe')(
`${BASE_URL}/runtime/sessions/no-such-session/records`,
);
const foreign = await fetchAs('Bearer learner-probe')(
`${BASE_URL}/runtime/sessions/victim-session/records`,
);
expect(absent.status).toBe(404);
expect(foreign.status).toBe(404);
expect(((await absent.json()) as { error: { code: string } }).error.code).toBe(
((await foreign.json()) as { error: { code: string } }).error.code,
);
});
test('the client restores empty-list semantics for the concealed 404', async () => {
const { fetchAs } = makeHarness();
const client = new HttpRuntimeStore({
baseUrl: BASE_URL,
fetch: fetchAs('Bearer learner-probe'),
});
await expect(client.listRecords('no-such-session')).resolves.toEqual([]);
});
});
describe('reference HTTP handler DELETE /runtime authorization', () => {
function makeHarness() {
const store = new BrowserRuntimeStore({ indexedDB: new IDBFactory() });
const handler = createRuntimeHttpHandler(store, {
authenticate: async (req) => bearerLearner(req),
authorizeAdmin: async (principal) => principal.learnerKey === 'admin',
});
const request = (authorization?: string) =>
handlerFetch(handler, async () => authorization)(`${BASE_URL}/runtime`, {
method: 'DELETE',
});
return { request, store };
}
test('denies a learner credential with 403', async () => {
const { request } = makeHarness();
const response = await request('Bearer learner-1');
expect(response.status).toBe(403);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'FORBIDDEN_ADMIN' },
});
});
test('denies a missing credential with 401', async () => {
const { request } = makeHarness();
const response = await request();
expect(response.status).toBe(401);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'UNAUTHENTICATED' },
});
});
test('allows an admin credential and empties the runtime store', async () => {
const { request, store } = makeHarness();
await store.createSession(makeSession({ id: 'stage-1-session' }));
await store.appendRecord(makeRecordInit('stage-1-session'));
await store.createSession(makeSession({ id: 'stage-2-session', stageId: 'stage-2' }));
const response = await request('Bearer admin');
expect(response.status).toBe(204);
expect(await store.getSession('stage-1-session')).toBeUndefined();
expect(await store.getSession('stage-2-session')).toBeUndefined();
expect(await store.listRecords('stage-1-session')).toEqual([]);
});
});
describe('reference HTTP handler principal capabilities', () => {
test('allows admin-only and merge-only principals without a fabricated learnerKey', async () => {
const store = new BrowserRuntimeStore({ indexedDB: new IDBFactory() });
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({}),
authorizeAdmin: async () => true,
authorizeMerge: async () => true,
});
const request = handlerFetch(handler, async () => 'Bearer capability-only');
const adminResponse = await request(`${BASE_URL}/runtime`, { method: 'DELETE' });
expect(adminResponse.status).toBe(204);
const mergeResponse = await request(`${BASE_URL}/runtime/learners/merge`, {
method: 'POST',
body: JSON.stringify({ fromLearnerKey: 'learner-a', toLearnerKey: 'learner-b' }),
});
expect(mergeResponse.status).toBe(200);
});
test('reference factory stays unbound and applies authorization overrides', async () => {
const statements: string[] = [];
const query = async (text: string) => {
statements.push(text);
return { rows: [] };
};
const pool = {
query,
connect: async () => ({ query, release: () => undefined }),
} as unknown as ConnectableQueryable;
const server = await createReferenceRuntimeServer(pool, {
authenticate: async () => ({}),
authorizeAdmin: async () => true,
authorizeMerge: async () => true,
payloadValidators: {},
maxBodyBytes: 64,
});
const handler = server.listeners('request')[0] as RequestListener;
expect(server.listening).toBe(false);
const response = await handlerFetch(handler, async () => 'Bearer capability-only')(
`${BASE_URL}/runtime`,
{ method: 'DELETE' },
);
expect(response.status).toBe(204);
expect(statements).toContain('DELETE FROM runtime_sessions');
const oversized = await handlerFetch(handler, async () => 'Bearer capability-only')(
`${BASE_URL}/runtime/learners/merge`,
{
method: 'POST',
body: JSON.stringify({ padding: 'x'.repeat(100) }),
},
);
expect(oversized.status).toBe(413);
});
test('returns 403 FORBIDDEN_LEARNER on learner routes without learnerKey', async () => {
const store = new BrowserRuntimeStore({ indexedDB: new IDBFactory() });
const handler = createRuntimeHttpHandler(store, { authenticate: async () => ({}) });
const response = await handlerFetch(
handler,
async () => 'Bearer admin-only',
)(`${BASE_URL}/runtime/stages/stage-1/learners/learner-a/sessions`);
expect(response.status).toBe(403);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'FORBIDDEN_LEARNER' },
});
});
});
describe('reference HTTP handler validation boundary', () => {
test('rejects an oversized request body with 413 and accepts an under-limit body', async () => {
const store = new BrowserRuntimeStore({ indexedDB: new IDBFactory() });
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: 'anon:device-1' }),
maxBodyBytes: 256,
});
const request = handlerFetch(handler, async () => 'Bearer anon:device-1');
const oversized = await request(`${BASE_URL}/runtime/sessions`, {
method: 'POST',
body: JSON.stringify({ padding: 'x'.repeat(300) }),
});
expect(oversized.status).toBe(413);
await expect(oversized.json()).resolves.toMatchObject({
error: { code: 'PAYLOAD_TOO_LARGE' },
});
const accepted = await request(`${BASE_URL}/runtime/sessions`, {
method: 'POST',
body: JSON.stringify(makeSession({ id: 'under-limit' })),
});
expect(accepted.status).toBe(201);
});
test.each([
['session id with NUL', makeSession({ id: 'bad\u0000session' })],
['session stageId with a lone surrogate', makeSession({ stageId: 'bad\ud800stage' })],
])('rejects a non-JSON-domain %s before calling the store', async (_label, init) => {
let createCalled = false;
const store = {
getSession: async () => undefined,
createSession: async () => {
createCalled = true;
throw new Error('must not be called');
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: 'anon:device-1' }),
});
const response = await handlerFetch(handler, async () => 'Bearer anon:device-1')(
`${BASE_URL}/runtime/sessions`,
{ method: 'POST', body: JSON.stringify(init) },
);
expect(response.status).toBe(400);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'VALIDATION_FAILED' },
});
expect(createCalled).toBe(false);
});
test.each([
['record id with NUL', { id: 'bad\u0000record' }],
['record sceneId with a lone surrogate', { sceneId: 'bad\ud800scene' }],
])('rejects a non-JSON-domain %s before calling the store', async (_label, overrides) => {
const session = { ...makeSession(), runtimeDslVersion: RUNTIME_DSL_VERSION };
let appendCalled = false;
const store = {
getSession: async () => session,
appendRecord: async () => {
appendCalled = true;
throw new Error('must not be called');
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: session.learnerKey }),
});
const response = await handlerFetch(handler, async () => `Bearer ${session.learnerKey}`)(
`${BASE_URL}/runtime/sessions/${session.id}/records`,
{
method: 'POST',
body: JSON.stringify(makeRecordInit(session.id, overrides)),
},
);
expect(response.status).toBe(400);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'VALIDATION_FAILED' },
});
expect(appendCalled).toBe(false);
});
test('uses the same whole-table payload validator replacement as the injected store', async () => {
const payloadValidators: Record<string, RuntimePayloadValidator> = {
chat: (payload) =>
typeof payload === 'object' && payload !== null && 'custom' in payload
? { valid: true }
: { valid: false, errors: [{ path: '/payload', message: 'expected custom payload' }] },
};
const store = new BrowserRuntimeStore({
indexedDB: new IDBFactory(),
payloadValidators,
});
await store.createSession(makeSession());
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: 'anon:device-1' }),
payloadValidators,
});
const response = await handlerFetch(handler, async () => 'Bearer anon:device-1')(
`${BASE_URL}/runtime/sessions/sess-1/records`,
{
method: 'POST',
body: JSON.stringify(makeRecordInit('sess-1', { payload: { custom: true } })),
},
);
expect(response.status).toBe(201);
});
});
describe('reference HTTP handler error disclosure', () => {
test('returns a generic INTERNAL_ERROR and logs the underlying store error server-side', async () => {
const secret = 'postgres password=do-not-reflect';
const underlying = new Error(secret);
const store = {
getSession: async () => {
throw underlying;
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: 'learner-a' }),
});
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
try {
const response = await handlerFetch(
handler,
async () => 'Bearer learner-a',
)(`${BASE_URL}/runtime/sessions/session-a`);
const text = await response.text();
expect(response.status).toBe(500);
expect(text).not.toContain(secret);
expect(JSON.parse(text)).toEqual({
error: {
code: 'INTERNAL_ERROR',
message: '@openmaic/storage: internal server error',
},
});
expect(consoleError).toHaveBeenCalledWith(
'@openmaic/storage: Runtime HTTP handler internal error',
underlying,
);
} finally {
consoleError.mockRestore();
}
});
});
describe('reference HTTP handler future-version semantics', () => {
const futureSession = {
...makeSession({ id: 'future-session', learnerKey: 'learner-a', stageId: 'stage-1' }),
runtimeDslVersion: '999.0.0',
};
test('passes future-stamped session and record reads through unchanged', async () => {
const record = { ...makeRecordInit(futureSession.id), seq: 0 };
const store = {
getSession: async () => futureSession,
listRecords: async () => [record],
listSessions: async () => [futureSession],
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: futureSession.learnerKey }),
});
const request = handlerFetch(handler, async () => `Bearer ${futureSession.learnerKey}`);
const sessionResponse = await request(`${BASE_URL}/runtime/sessions/${futureSession.id}`);
expect(sessionResponse.status).toBe(200);
await expect(sessionResponse.json()).resolves.toEqual(futureSession);
const recordsResponse = await request(
`${BASE_URL}/runtime/sessions/${futureSession.id}/records`,
);
expect(recordsResponse.status).toBe(200);
await expect(recordsResponse.json()).resolves.toEqual([record]);
const sessionsResponse = await request(
`${BASE_URL}/runtime/stages/${futureSession.stageId}/learners/${futureSession.learnerKey}/sessions`,
);
expect(sessionsResponse.status).toBe(200);
await expect(sessionsResponse.json()).resolves.toEqual([futureSession]);
});
test.each([
['session', `/runtime/sessions/${futureSession.id}`, 'deleteSession'],
[
'learner partition',
`/runtime/stages/${futureSession.stageId}/learners/${futureSession.learnerKey}`,
'deleteLearnerRuntime',
],
['stage', `/runtime/stages/${futureSession.stageId}`, 'deleteStageRuntime'],
['all runtime', '/runtime', 'deleteAllRuntime'],
] as const)('keeps the %s delete version-independent', async (_label, path, methodName) => {
const calls: string[] = [];
const store = {
getSession: async () => futureSession,
deleteSession: async () => calls.push('deleteSession'),
deleteLearnerRuntime: async () => calls.push('deleteLearnerRuntime'),
deleteStageRuntime: async () => calls.push('deleteStageRuntime'),
deleteAllRuntime: async () => calls.push('deleteAllRuntime'),
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: futureSession.learnerKey }),
authorizeAdmin: async () => true,
});
const response = await handlerFetch(handler, async () => `Bearer ${futureSession.learnerKey}`)(
`${BASE_URL}${path}`,
{ method: 'DELETE' },
);
expect(response.status).toBe(204);
expect(calls).toEqual([methodName]);
});
test.each([
{
operation: 'appendRecord',
invoke: (request: typeof globalThis.fetch) =>
request(`${BASE_URL}/runtime/sessions/${futureSession.id}/records`, {
method: 'POST',
body: JSON.stringify(makeRecordInit(futureSession.id)),
}),
},
{
operation: 'setSessionStatus',
invoke: (request: typeof globalThis.fetch) =>
request(`${BASE_URL}/runtime/sessions/${futureSession.id}/status`, {
method: 'PATCH',
body: JSON.stringify({ status: 'completed', updatedAt: '2026-01-01T00:02:00.000Z' }),
}),
},
])('$operation keeps the 409 FUTURE_VERSION write guard', async ({ operation, invoke }) => {
const called: string[] = [];
const store = {
getSession: async () => futureSession,
appendRecord: async () => called.push('appendRecord'),
setSessionStatus: async () => {
called.push('setSessionStatus');
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: futureSession.learnerKey }),
});
const response = await invoke(
handlerFetch(handler, async () => `Bearer ${futureSession.learnerKey}`),
);
expect(response.status).toBe(409);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'FUTURE_VERSION' },
});
expect(called).not.toContain(operation);
});
test('mergeLearner returns 409 only after a structured future-session re-fetch', async () => {
const storeError = new Error(
`@openmaic/storage: session ${JSON.stringify(futureSession.id)} was written at runtime DSL ` +
`version ${JSON.stringify(futureSession.runtimeDslVersion)}, newer than this client's ` +
RUNTIME_DSL_VERSION,
);
const store = {
getSession: async () => futureSession,
mergeLearner: async () => {
throw storeError;
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: futureSession.learnerKey }),
authorizeMerge: async () => true,
});
const response = await handlerFetch(handler, async () => `Bearer ${futureSession.learnerKey}`)(
`${BASE_URL}/runtime/learners/merge`,
{
method: 'POST',
body: JSON.stringify({
fromLearnerKey: futureSession.learnerKey,
toLearnerKey: 'learner-b',
}),
},
);
expect(response.status).toBe(409);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'FUTURE_VERSION' },
});
});
});
describe('reference HTTP handler concurrent session-write classification', () => {
const operations = [
{
name: 'appendRecord',
invoke: (request: typeof globalThis.fetch, sessionId: string) =>
request(`${BASE_URL}/runtime/sessions/${sessionId}/records`, {
method: 'POST',
body: JSON.stringify(makeRecordInit(sessionId)),
}),
},
{
name: 'setSessionStatus',
invoke: (request: typeof globalThis.fetch, sessionId: string) =>
request(`${BASE_URL}/runtime/sessions/${sessionId}/status`, {
method: 'PATCH',
body: JSON.stringify({ status: 'completed', updatedAt: '2026-01-01T00:02:00.000Z' }),
}),
},
];
test.each(operations)(
'$name returns 404 when the session is deleted before the write',
async ({ name, invoke }) => {
const session = {
...makeSession({ id: `race-${name}` }),
runtimeDslVersion: RUNTIME_DSL_VERSION,
};
const underlying = new Error('session does not exist');
let reads = 0;
const store = {
getSession: async () => (++reads === 1 ? session : undefined),
appendRecord: async () => {
throw underlying;
},
setSessionStatus: async () => {
throw underlying;
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: session.learnerKey }),
});
const response = await invoke(
handlerFetch(handler, async () => `Bearer ${session.learnerKey}`),
session.id,
);
expect(response.status).toBe(404);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'SESSION_NOT_FOUND' },
});
expect(reads).toBe(2);
},
);
test.each(operations)(
'$name returns 400 with the current status when the session completes before the write',
async ({ name, invoke }) => {
const session = {
...makeSession({ id: `race-${name}` }),
runtimeDslVersion: RUNTIME_DSL_VERSION,
};
const completed = { ...session, status: 'completed' as const };
const underlying = new Error('session not active');
let reads = 0;
const store = {
getSession: async () => (++reads === 1 ? session : completed),
appendRecord: async () => {
throw underlying;
},
setSessionStatus: async () => {
throw underlying;
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: session.learnerKey }),
});
const response = await invoke(
handlerFetch(handler, async () => `Bearer ${session.learnerKey}`),
session.id,
);
const body = (await response.json()) as { error: { code: string; message: string } };
expect(response.status).toBe(400);
expect(body.error.code).toBe('VALIDATION_FAILED');
expect(body.error.message).toContain("current status is 'completed'");
expect(reads).toBe(2);
},
);
test.each(operations)(
'$name preserves 500 for an unclassified store failure',
async ({ name, invoke }) => {
const session = {
...makeSession({ id: `race-${name}` }),
runtimeDslVersion: RUNTIME_DSL_VERSION,
};
const underlying = new Error('unexpected write failure');
let reads = 0;
const store = {
getSession: async () => {
reads += 1;
return session;
},
appendRecord: async () => {
throw underlying;
},
setSessionStatus: async () => {
throw underlying;
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: session.learnerKey }),
});
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
try {
const response = await invoke(
handlerFetch(handler, async () => `Bearer ${session.learnerKey}`),
session.id,
);
expect(response.status).toBe(500);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'INTERNAL_ERROR' },
});
expect(reads).toBe(2);
expect(consoleError).toHaveBeenCalledWith(
'@openmaic/storage: Runtime HTTP handler internal error',
underlying,
);
} finally {
consoleError.mockRestore();
}
},
);
});
describe('reference HTTP handler cross-learner rejection matrix', () => {
function makeCrossLearnerHarness() {
const store = new BrowserRuntimeStore({ indexedDB: new IDBFactory() });
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: 'learner-a' }),
authorizeMerge: async (principal, fromKey) => principal.learnerKey === fromKey,
});
const request = handlerFetch(handler, async () => 'Bearer learner-a');
return { store, request };
}
const cases: {
route: string;
expectedStatus: 403 | 404;
expectedCode: 'FORBIDDEN_LEARNER' | 'SESSION_NOT_FOUND';
invoke(request: typeof globalThis.fetch): Promise<Response>;
}[] = [
{
route: 'get session',
expectedStatus: 404,
expectedCode: 'SESSION_NOT_FOUND',
invoke: (request) => request(`${BASE_URL}/runtime/sessions/session-b`),
},
{
route: 'list records',
expectedStatus: 404,
expectedCode: 'SESSION_NOT_FOUND',
invoke: (request) => request(`${BASE_URL}/runtime/sessions/session-b/records`),
},
{
route: 'append record',
expectedStatus: 404,
expectedCode: 'SESSION_NOT_FOUND',
invoke: (request) =>
request(`${BASE_URL}/runtime/sessions/session-b/records`, {
method: 'POST',
body: JSON.stringify(makeRecordInit('session-b')),
}),
},
{
route: 'set session status',
expectedStatus: 404,
expectedCode: 'SESSION_NOT_FOUND',
invoke: (request) =>
request(`${BASE_URL}/runtime/sessions/session-b/status`, {
method: 'PATCH',
body: JSON.stringify({ status: 'completed', updatedAt: '2026-01-01T00:02:00.000Z' }),
}),
},
{
route: 'delete session',
expectedStatus: 404,
expectedCode: 'SESSION_NOT_FOUND',
invoke: (request) =>
request(`${BASE_URL}/runtime/sessions/session-b`, {
method: 'DELETE',
}),
},
{
route: 'list stage learner sessions',
expectedStatus: 403,
expectedCode: 'FORBIDDEN_LEARNER',
invoke: (request) =>
request(`${BASE_URL}/runtime/stages/stage-1/learners/learner-b/sessions`),
},
{
route: 'delete stage learner runtime',
expectedStatus: 403,
expectedCode: 'FORBIDDEN_LEARNER',
invoke: (request) =>
request(`${BASE_URL}/runtime/stages/stage-1/learners/learner-b`, {
method: 'DELETE',
}),
},
{
route: 'merge learner',
expectedStatus: 403,
expectedCode: 'FORBIDDEN_LEARNER',
invoke: (request) =>
request(`${BASE_URL}/runtime/learners/merge`, {
method: 'POST',
body: JSON.stringify({ fromLearnerKey: 'learner-b', toLearnerKey: 'learner-a' }),
}),
},
];
test.each(cases)('$route returns $expectedStatus $expectedCode', async (testCase) => {
const { store, request } = makeCrossLearnerHarness();
await store.createSession(
makeSession({ id: 'session-b', learnerKey: 'learner-b', stageId: 'stage-1' }),
);
await store.appendRecord(makeRecordInit('session-b'));
const response = await testCase.invoke(request);
expect(response.status).toBe(testCase.expectedStatus);
await expect(response.json()).resolves.toMatchObject({
error: { code: testCase.expectedCode },
});
});
});
describe('reference HTTP handler ownership ordering', () => {
test('returns concealed 404 before classifying another learner future-version session', async () => {
const futureSession = {
...makeSession({ id: 'future-b', learnerKey: 'learner-b' }),
runtimeDslVersion: '999.0.0',
};
const store = { getSession: async () => futureSession } as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: 'learner-a' }),
});
const response = await handlerFetch(
handler,
async () => 'Bearer learner-a',
)(`${BASE_URL}/runtime/sessions/future-b`);
expect(response.status).toBe(404);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'SESSION_NOT_FOUND' },
});
});
test('re-checks ownership immediately before delete and rejects a concurrent merge', async () => {
const owned = { ...makeSession({ id: 'moving' }), runtimeDslVersion: RUNTIME_DSL_VERSION };
const moved = { ...owned, learnerKey: 'learner-b' };
let reads = 0;
let deleted = false;
const store = {
getSession: async () => (++reads === 1 ? owned : moved),
deleteSession: async () => {
deleted = true;
},
} as unknown as RuntimeStore;
const handler = createRuntimeHttpHandler(store, {
authenticate: async () => ({ learnerKey: owned.learnerKey }),
});
const response = await handlerFetch(handler, async () => `Bearer ${owned.learnerKey}`)(
`${BASE_URL}/runtime/sessions/moving`,
{ method: 'DELETE' },
);
expect(response.status).toBe(404);
expect(reads).toBe(2);
expect(deleted).toBe(false);
});
});