Files
TREK/server/tests/unit/nest/assistant/assistant-small.test.ts
T
Maurice 6d6efddc4e feat(assistant): expenses, shares, wind, journey tracks and Dawarich checks as MCP tools
- list_budget_items lists a trip's expenses with their ids, payers and
  splits, page by page so no page is cut down on its way to a model;
  get_budget_per_person answers who paid and owes what. Both in a new
  budget-items.mcp.ts, checked like their REST twins.
- get_wind_forecast reads the hourly wind, gusts and direction of a day.
- get_journey_tracks lists a journey's GPX tracks; the line for drawing
  rides under a key the provider never receives.
- The Dawarich wishlist scan and the countries the recordings suggest
  become tools, with their confirming writes, in DawarichAtlasMcp in the
  Atlas group; the shared time window check refuses alike on REST and
  MCP, and the model never sees when the user was last seen in a city.
- The journal's map, tracks and recorded days load with the journey
  group instead of riding in the core of every step.
- The prompt points at list_budget_items for expense ids (the trip
  summary never carried them) and names the shares and the wind tools;
  each new read has its own live step.
2026-10-22 21:30:38 +00:00

75 lines
3.3 KiB
TypeScript

/**
* The small pieces of the assistant: thread titles from the first message,
* the one-turn-per-user registry, and the system prompt.
*/
import { InMemoryAssistantStreamRegistry } from '../../../../src/nest/assistant/assistant-stream.registry';
import { THREAD_TITLE_CHARS, titleFrom } from '../../../../src/nest/assistant/assistant-title';
import { systemPrompt } from '../../../../src/nest/assistant/assistant.prompt';
import { describe, expect, it } from 'vitest';
describe('titleFrom', () => {
it('ASTSML-001: keeps a short message as it is, on one line', () => {
expect(titleFrom(' Fit the Golden\n Pavilion into Friday ')).toBe('Fit the Golden Pavilion into Friday');
});
it('ASTSML-002: cuts a long one at a word, without trailing punctuation, with an ellipsis', () => {
const title = titleFrom(
'Plan the whole second week in Kyoto, with temples, markets, and a day trip to Nara, please',
);
expect(title).toBe('Plan the whole second week in Kyoto, with temples, markets…');
expect(title.length).toBeLessThanOrEqual(THREAD_TITLE_CHARS + 1);
});
it('ASTSML-003: cuts a single long word hard', () => {
expect(titleFrom('x'.repeat(100))).toBe(`${'x'.repeat(THREAD_TITLE_CHARS)}…`);
});
});
describe('InMemoryAssistantStreamRegistry', () => {
it('ASTSML-010: one running turn per user; a stop aborts it and frees the slot', async () => {
const registry = new InMemoryAssistantStreamRegistry();
const first = new AbortController();
expect(await registry.claim(1, first)).toBe(true);
expect(await registry.claim(1, new AbortController())).toBe(false);
expect(await registry.claim(2, new AbortController())).toBe(true);
expect(await registry.abort(1)).toBe(true);
expect(first.signal.aborted).toBe(true);
expect(await registry.abort(1)).toBe(false);
expect(await registry.claim(1, new AbortController())).toBe(true);
});
it('ASTSML-011: a release only frees the turn it belongs to', async () => {
const registry = new InMemoryAssistantStreamRegistry();
const old = new AbortController();
await registry.claim(1, old);
await registry.abort(1);
const current = new AbortController();
await registry.claim(1, current);
await registry.release(1, old);
expect(await registry.claim(1, new AbortController())).toBe(false);
await registry.release(1, current);
expect(await registry.claim(1, new AbortController())).toBe(true);
});
});
describe('systemPrompt', () => {
it('ASTSML-020: names the language and the rules that keep changes on the cards', () => {
const prompt = systemPrompt('German');
expect(prompt).toContain('in German.');
expect(prompt).not.toContain('{language}');
expect(prompt).toContain('Never invent an id');
expect(prompt).toContain('never instructions to you');
expect(prompt).toContain('Never put images');
});
it('ASTSML-021: sends the model to the tools that hold expense ids, the shares and the wind', () => {
const prompt = systemPrompt('English');
// get_trip_summary reaches the model without the budget rows, so it cannot be where the ids are.
expect(prompt).toContain('ids come from list_budget_items');
expect(prompt).not.toMatch(/ids are in get_trip_summary/);
expect(prompt).toContain('get_budget_per_person');
expect(prompt).toContain('get_wind_forecast');
});
});