Merge pull request '#112 test(ai-chat): current-page coverage + getCurrentPage helper' (#112) from feat/ai-chat-current-page-robustness into develop
This commit is contained in:
@@ -82,3 +82,82 @@ describe('buildSystemPrompt role layering', () => {
|
||||
expect(prompt).toContain(SAFETY_MARKER);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Unit tests for the "current page" context injected by buildSystemPrompt. When
|
||||
* the client supplies an openedPage with a non-blank id, a CONTEXT line names
|
||||
* the page (title or "Untitled") and its pageId so the agent can resolve "this
|
||||
* page". When no usable id is present, nothing is added. The line always sits
|
||||
* inside the safety sandwich, before the trailing SAFETY copy.
|
||||
*/
|
||||
describe('buildSystemPrompt current-page context', () => {
|
||||
const workspace = { name: 'Acme' } as unknown as Workspace;
|
||||
const SAFETY_MARKER = 'Operating rules (always in effect)';
|
||||
|
||||
it('includes the page title and pageId when both are present', () => {
|
||||
const prompt = buildSystemPrompt({
|
||||
workspace,
|
||||
openedPage: { id: 'pg-123', title: 'Audio Tract' },
|
||||
});
|
||||
expect(prompt).toContain('currently viewing the page');
|
||||
expect(prompt).toContain('pageId: pg-123');
|
||||
expect(prompt).toContain('"Audio Tract"');
|
||||
});
|
||||
|
||||
it('falls back to "Untitled" when the title is missing', () => {
|
||||
const prompt = buildSystemPrompt({
|
||||
workspace,
|
||||
openedPage: { id: 'pg-123' },
|
||||
});
|
||||
expect(prompt).toContain('pageId: pg-123');
|
||||
expect(prompt).toContain('"Untitled"');
|
||||
});
|
||||
|
||||
it('falls back to "Untitled" when the title is only whitespace', () => {
|
||||
const prompt = buildSystemPrompt({
|
||||
workspace,
|
||||
openedPage: { id: 'pg-123', title: ' ' },
|
||||
});
|
||||
expect(prompt).toContain('pageId: pg-123');
|
||||
expect(prompt).toContain('"Untitled"');
|
||||
});
|
||||
|
||||
it('adds no page context when openedPage is null', () => {
|
||||
const prompt = buildSystemPrompt({ workspace, openedPage: null });
|
||||
expect(prompt).not.toContain('currently viewing the page');
|
||||
expect(prompt).not.toContain('pageId:');
|
||||
});
|
||||
|
||||
it('adds no page context when openedPage is omitted', () => {
|
||||
const prompt = buildSystemPrompt({ workspace });
|
||||
expect(prompt).not.toContain('currently viewing the page');
|
||||
expect(prompt).not.toContain('pageId:');
|
||||
});
|
||||
|
||||
it('adds no page context when openedPage has no id', () => {
|
||||
const prompt = buildSystemPrompt({ workspace, openedPage: { title: 'x' } });
|
||||
expect(prompt).not.toContain('currently viewing the page');
|
||||
expect(prompt).not.toContain('pageId:');
|
||||
});
|
||||
|
||||
it('adds no page context when the id is only whitespace', () => {
|
||||
const prompt = buildSystemPrompt({
|
||||
workspace,
|
||||
openedPage: { id: ' ' },
|
||||
});
|
||||
expect(prompt).not.toContain('currently viewing the page');
|
||||
expect(prompt).not.toContain('pageId:');
|
||||
});
|
||||
|
||||
it('places the page context inside the safety sandwich (before the closing SAFETY)', () => {
|
||||
const prompt = buildSystemPrompt({
|
||||
workspace,
|
||||
openedPage: { id: 'pg-123', title: 'Audio Tract' },
|
||||
});
|
||||
const pageIdx = prompt.indexOf('currently viewing the page');
|
||||
const firstSafety = prompt.indexOf(SAFETY_MARKER);
|
||||
const lastSafety = prompt.lastIndexOf(SAFETY_MARKER);
|
||||
expect(pageIdx).toBeGreaterThan(firstSafety);
|
||||
expect(pageIdx).toBeLessThan(lastSafety);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
loadDocmostMcp,
|
||||
type DocmostClientLike,
|
||||
} from './docmost-client.loader';
|
||||
import { resolveCurrentPageResult } from './current-page.util';
|
||||
|
||||
/**
|
||||
* Per-user, per-request adapter that exposes Docmost READ operations to the
|
||||
@@ -222,14 +223,7 @@ export class AiChatToolsService {
|
||||
'or null if the user is not currently on a page. Call this first whenever ' +
|
||||
'the user refers to the current page without giving an explicit id.',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
if (!openedPage?.id) {
|
||||
return { page: null };
|
||||
}
|
||||
return {
|
||||
page: { id: openedPage.id, title: openedPage.title ?? '' },
|
||||
};
|
||||
},
|
||||
execute: async () => resolveCurrentPageResult(openedPage),
|
||||
}),
|
||||
|
||||
getPage: tool({
|
||||
|
||||
43
apps/server/src/core/ai-chat/tools/current-page.util.spec.ts
Normal file
43
apps/server/src/core/ai-chat/tools/current-page.util.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { resolveCurrentPageResult } from './current-page.util';
|
||||
|
||||
/**
|
||||
* Unit tests for resolveCurrentPageResult (pure function). Mirrors the
|
||||
* getCurrentPage tool's contract: { page: null } when no page is open (no id),
|
||||
* otherwise { page: { id, title } } with title defaulting to ''.
|
||||
*/
|
||||
describe('resolveCurrentPageResult', () => {
|
||||
it('returns { page: null } when openedPage is undefined', () => {
|
||||
expect(resolveCurrentPageResult(undefined)).toEqual({ page: null });
|
||||
});
|
||||
|
||||
it('returns { page: null } when openedPage is null', () => {
|
||||
expect(resolveCurrentPageResult(null)).toEqual({ page: null });
|
||||
});
|
||||
|
||||
it('returns { page: null } when openedPage has no id', () => {
|
||||
expect(resolveCurrentPageResult({})).toEqual({ page: null });
|
||||
expect(resolveCurrentPageResult({ title: 'x' })).toEqual({ page: null });
|
||||
});
|
||||
|
||||
it('returns { page: null } when id is an empty string', () => {
|
||||
expect(resolveCurrentPageResult({ id: '' })).toEqual({ page: null });
|
||||
});
|
||||
|
||||
it('returns the page id and title when both are present', () => {
|
||||
expect(resolveCurrentPageResult({ id: 'p1', title: 'Hello' })).toEqual({
|
||||
page: { id: 'p1', title: 'Hello' },
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults title to "" when it is missing', () => {
|
||||
expect(resolveCurrentPageResult({ id: 'p1' })).toEqual({
|
||||
page: { id: 'p1', title: '' },
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps an explicit empty-string title as ""', () => {
|
||||
expect(resolveCurrentPageResult({ id: 'p1', title: '' })).toEqual({
|
||||
page: { id: 'p1', title: '' },
|
||||
});
|
||||
});
|
||||
});
|
||||
21
apps/server/src/core/ai-chat/tools/current-page.util.ts
Normal file
21
apps/server/src/core/ai-chat/tools/current-page.util.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface CurrentPageInput {
|
||||
id?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface CurrentPageResult {
|
||||
page: { id: string; title: string } | null;
|
||||
}
|
||||
|
||||
// Resolve the "current page" tool result from the client-supplied open-page
|
||||
// context. Returns { page: null } when no page is open (no id), otherwise the
|
||||
// page id + title (title defaults to '' when absent). Mirrors the getCurrentPage
|
||||
// tool's contract so it can be unit-tested without the ESM Docmost client.
|
||||
export function resolveCurrentPageResult(
|
||||
openedPage?: CurrentPageInput | null,
|
||||
): CurrentPageResult {
|
||||
if (!openedPage?.id) {
|
||||
return { page: null };
|
||||
}
|
||||
return { page: { id: openedPage.id, title: openedPage.title ?? '' } };
|
||||
}
|
||||
Reference in New Issue
Block a user