192 lines
9.0 KiB
JavaScript
192 lines
9.0 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createContextObligatoryRuntime } from './runtime.js';
|
|
|
|
const json = (body) => new Response(JSON.stringify(body), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
describe('context obligatory runtime', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('injects pinned text in chronological order after compaction and records the summary cursor', async () => {
|
|
const requests = [];
|
|
let sessionReads = 0;
|
|
vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
|
|
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
|
|
if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
|
|
if (url.pathname === '/session/ses_1') {
|
|
sessionReads += 1;
|
|
return json({
|
|
id: 'ses_1',
|
|
metadata: { openchamber: { context_obligatory_messages: [
|
|
{ id: 'msg_2', createdAt: 20, role: 'assistant' },
|
|
{ id: 'msg_1', createdAt: 10, role: 'user' },
|
|
] } },
|
|
});
|
|
}
|
|
if (url.pathname === '/session/ses_1/message') return json([
|
|
{ info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
|
|
{ info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
|
|
]);
|
|
if (url.pathname === '/session/ses_1/message/msg_1') return json({ parts: [{ type: 'text', text: 'First' }] });
|
|
if (url.pathname === '/session/ses_1/message/msg_2') return json({ parts: [{ type: 'text', text: 'Second' }] });
|
|
if (url.pathname === '/session/ses_1/prompt_async') return json({});
|
|
throw new Error(`Unexpected ${url.pathname}`);
|
|
}));
|
|
const runtime = createContextObligatoryRuntime({
|
|
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
});
|
|
|
|
await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
|
|
const prompt = requests.find((request) => request.path.endsWith('/prompt_async'));
|
|
const payload = JSON.parse(prompt.body);
|
|
expect(payload).toMatchObject({
|
|
model: { providerID: 'provider', modelID: 'model' },
|
|
agent: 'build',
|
|
parts: [{ type: 'text', synthetic: true }],
|
|
});
|
|
expect(payload.parts[0].text.indexOf('First')).toBeLessThan(payload.parts[0].text.indexOf('Second'));
|
|
expect(payload.parts[0].text).toContain('continuing the pre-compaction work');
|
|
expect(payload.parts[0].text).toContain('use it silently as background context');
|
|
expect(payload.parts[0].text).toContain('Only if no tasks or next steps remain');
|
|
expect(payload.parts[0].text).toContain('no more than one short paragraph');
|
|
const patch = requests.find((request) => request.method === 'PATCH');
|
|
expect(JSON.parse(patch.body).metadata.openchamber.context_obligatory_last_compaction_message_id).toBe('msg_summary');
|
|
expect(sessionReads).toBe(2);
|
|
runtime.stop();
|
|
});
|
|
|
|
|
|
it('restores project knowledge after compaction even with nothing pinned', async () => {
|
|
// Pinned messages are already in the conversation until compaction removes
|
|
// them; project knowledge was never there at all, so a session with no
|
|
// pinned messages still has something to get back.
|
|
const requests = [];
|
|
vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
|
|
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
|
|
if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
|
|
if (url.pathname === '/session/ses_1') return json({
|
|
id: 'ses_1',
|
|
metadata: { openchamber: { knowledge_context_delivered: 'sig-before-compaction' } },
|
|
});
|
|
if (url.pathname === '/session/ses_1/message') return json([
|
|
{ info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
|
|
{ info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
|
|
]);
|
|
if (url.pathname === '/session/ses_1/prompt_async') return json({});
|
|
throw new Error(`Unexpected ${url.pathname}`);
|
|
}));
|
|
const resolvePending = vi.fn(async () => ({
|
|
text: '## Pinned notes\n\n- Remember this.',
|
|
signature: 'sig-1',
|
|
}));
|
|
const runtime = createContextObligatoryRuntime({
|
|
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
sessionKnowledgeRuntime: {
|
|
metadataKey: 'knowledge_context_delivered',
|
|
readPins: () => ({ notes: ['n1'], plans: [] }),
|
|
resolvePending,
|
|
},
|
|
});
|
|
|
|
await runtime.processPayload({
|
|
type: 'session.compacted',
|
|
properties: { sessionID: 'ses_1', directory: '/work/project' },
|
|
});
|
|
|
|
expect(resolvePending).toHaveBeenCalledWith(
|
|
'/work/project',
|
|
'',
|
|
{ notes: ['n1'], plans: [] },
|
|
);
|
|
const prompt = requests.find((request) => request.path.endsWith('/prompt_async'));
|
|
expect(JSON.parse(prompt.body).parts[0].text).toContain('Remember this.');
|
|
const patch = requests.find((request) => request.method === 'PATCH');
|
|
// Recorded with the cursor, so the next ordinary send does not repeat it.
|
|
expect(JSON.parse(patch.body).metadata.openchamber.knowledge_context_delivered).toBe('sig-1');
|
|
});
|
|
|
|
it('sends pinned messages and project knowledge as one message', async () => {
|
|
const requests = [];
|
|
vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
|
|
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
|
|
if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
|
|
if (url.pathname === '/session/ses_1') return json({
|
|
id: 'ses_1',
|
|
metadata: { openchamber: { context_obligatory_messages: [{ id: 'msg_1', createdAt: 10, role: 'user' }] } },
|
|
});
|
|
if (url.pathname === '/session/ses_1/message') return json([
|
|
{ info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
|
|
{ info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
|
|
]);
|
|
if (url.pathname === '/session/ses_1/message/msg_1') return json({ parts: [{ type: 'text', text: 'Pinned message' }] });
|
|
if (url.pathname === '/session/ses_1/prompt_async') return json({});
|
|
throw new Error(`Unexpected ${url.pathname}`);
|
|
}));
|
|
const runtime = createContextObligatoryRuntime({
|
|
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
sessionKnowledgeRuntime: {
|
|
metadataKey: 'knowledge_context_delivered',
|
|
readPins: () => ({ notes: ['n1'], plans: [] }),
|
|
resolvePending: async () => ({ text: 'Pinned notes block', signature: 'sig-1' }),
|
|
},
|
|
});
|
|
|
|
await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
|
|
// One turn, not two: back-to-back synthetic messages read as the agent
|
|
// being interrupted twice.
|
|
const prompts = requests.filter((request) => request.path.endsWith('/prompt_async'));
|
|
expect(prompts).toHaveLength(1);
|
|
const text = JSON.parse(prompts[0].body).parts[0].text;
|
|
expect(text).toContain('Pinned notes block');
|
|
expect(text).toContain('Pinned message');
|
|
});
|
|
|
|
it('does nothing when the session already carries the knowledge and has no pins', async () => {
|
|
const requests = [];
|
|
vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
|
|
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
requests.push({ path: url.pathname, method: init.method ?? 'GET' });
|
|
if (url.pathname === '/session/ses_1') return json({ id: 'ses_1', metadata: {} });
|
|
throw new Error(`Unexpected ${url.pathname}`);
|
|
}));
|
|
const runtime = createContextObligatoryRuntime({
|
|
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
sessionKnowledgeRuntime: {
|
|
metadataKey: 'knowledge_context_delivered',
|
|
readPins: () => ({ notes: [], plans: [] }),
|
|
resolvePending: async () => ({ text: '', signature: 'sig-1' }),
|
|
},
|
|
});
|
|
|
|
await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
|
|
expect(requests.some((request) => request.path.endsWith('/prompt_async'))).toBe(false);
|
|
});
|
|
|
|
it('ignores ordinary idle events without making requests', async () => {
|
|
const fetchImpl = vi.fn();
|
|
vi.stubGlobal('fetch', fetchImpl);
|
|
const runtime = createContextObligatoryRuntime({
|
|
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
});
|
|
await runtime.processPayload({ type: 'session.status', properties: { sessionID: 'ses_1', status: { type: 'idle' } } });
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
runtime.stop();
|
|
});
|
|
});
|