Files
TREK/server/tests/unit/nest/assistant/assistant-memory.test.ts
T
Maurice d1b8cb20a3 feat(assistant): found places open the place form, notes kept on its own, city walk stops checked off
- Found places: the strip shows only when the answer names one of them, the
  named ones first. A click on a card, or "Add to trip", opens the planner's
  place form filled in with the place on the day in focus, its Google details
  (photos, hours, description) loading beside it; "+ Day" and the day grid
  are gone.
- Trip notes: remember_for_trip runs at once without a card. The assistant
  keeps lasting facts the user mentions (who travels, needs, likes, budget,
  pace) on its own and offers the unsure ones as a follow-up; the chat shows
  "Noted for this trip: ..." with the notes a tap away, and reads them again
  so the dialog never saves over the new note.
- City walks: set_city_walk_visit checks a station off on its day (visited,
  skipped, or the mark taken back), with day_edit as in the day plan, a
  change card naming the station and its walk, and an undo that restores the
  status before while the station is as the change left it.
- Card heads are a step smaller (32px tile, smaller title and count), and the
  dot beside "Working on it" in the header is gone.
2026-10-23 18:21:54 +02:00

91 lines
4.4 KiB
TypeScript

/**
* The trip notes the assistant keeps: a note goes below what is there, the
* same note is not added twice, notes that would run past the limit are
* refused whole, and other trips' notes stay as they are.
*/
import type { AgentToolPort } from '../../../../src/nest/assistant/assistant-agent';
import {
noteOf,
REMEMBER_SPEC,
rememberForTrip,
rememberingPort,
} from '../../../../src/nest/assistant/assistant-memory';
import type { SettingsService } from '../../../../src/nest/settings/settings.service';
import { describe, expect, it, vi } from 'vitest';
function settingsWith(prefs: Record<string, unknown>) {
const upsertSetting = vi.fn(async () => undefined);
const settings = { getUserSettings: vi.fn(async () => prefs), upsertSetting } as unknown as SettingsService;
return { settings, upsertSetting };
}
describe('the trip notes', () => {
it('ASTMEM-001: a note goes below the trip notes there are, and other trips keep theirs', async () => {
const { settings, upsertSetting } = settingsWith({ assistant_memory: { '4': 'Vegetarian', '9': 'Other trip' } });
const run = await rememberForTrip(settings, 1, 4, { note: ' We travel as two. ' });
expect(run).toEqual({ ok: true, value: { notes: 'Vegetarian\nWe travel as two.' } });
expect(upsertSetting).toHaveBeenCalledWith(1, 'assistant_memory', {
'4': 'Vegetarian\nWe travel as two.',
'9': 'Other trip',
});
});
it('ASTMEM-002: the first note starts them, and a note kept already is not added again', async () => {
const first = settingsWith({});
expect(await rememberForTrip(first.settings, 1, 4, { note: 'No museums' })).toEqual({
ok: true,
value: { notes: 'No museums' },
});
expect(first.upsertSetting).toHaveBeenCalledWith(1, 'assistant_memory', { '4': 'No museums' });
const again = settingsWith({ assistant_memory: { '4': 'No museums' } });
expect((await rememberForTrip(again.settings, 1, 4, { note: 'No museums' })).ok).toBe(true);
expect(again.upsertSetting).not.toHaveBeenCalled();
});
it('ASTMEM-003: nothing to keep, or no room left, is refused and writes nothing', async () => {
const empty = settingsWith({});
expect(await rememberForTrip(empty.settings, 1, 4, { note: ' ' })).toMatchObject({ ok: false });
const full = settingsWith({ assistant_memory: { '4': 'x'.repeat(1_990) } });
expect(await rememberForTrip(full.settings, 1, 4, { note: 'One more thing to keep' })).toMatchObject({
ok: false,
});
expect(empty.upsertSetting).not.toHaveBeenCalled();
expect(full.upsertSetting).not.toHaveBeenCalled();
});
it('ASTMEM-004: the note is read trimmed and capped, and the model is offered one short sentence', () => {
expect(noteOf({ note: 42 })).toBeNull();
expect(noteOf({ note: 'y'.repeat(600) })).toHaveLength(500);
expect(REMEMBER_SPEC.parameters).toMatchObject({ required: ['note'], additionalProperties: false });
});
it('ASTMEM-005: in a trip turn the note is kept at once as a read; other tools and a journey keep the port as it is', async () => {
const port: AgentToolPort = {
specs: () => [REMEMBER_SPEC],
kindOf: (name) => (name === 'remember_for_trip' || name === 'create_place' ? 'write' : null),
pinArgs: (_name, args) => ({ args: { ...args, tripId: 4 } }),
call: vi.fn(async () => ({ ok: true, value: 'port' })),
};
const remember = vi.fn(async () => ({ ok: true, value: { notes: 'Vegetarian' } }));
const kept = rememberingPort(port, remember);
expect(kept.kindOf('remember_for_trip')).toBe('read');
expect(kept.kindOf('create_place')).toBe('write');
expect(kept.kindOf('nope')).toBeNull();
expect(kept.pinArgs('remember_for_trip', { note: 'x' })).toEqual({ args: { note: 'x' } });
expect(kept.pinArgs('create_place', {})).toEqual({ args: { tripId: 4 } });
expect(await kept.call('remember_for_trip', { note: 'Vegetarian' })).toEqual({
ok: true,
value: { notes: 'Vegetarian' },
});
expect(await kept.call('create_place', {})).toEqual({ ok: true, value: 'port' });
expect(kept.specs()).toEqual([REMEMBER_SPEC]);
expect(remember).toHaveBeenCalledTimes(1);
expect(rememberingPort(port, null)).toBe(port);
// A session that does not hold the tool (a journey's) never gains it.
const journey = rememberingPort({ ...port, kindOf: () => null }, remember);
expect(journey.kindOf('remember_for_trip')).toBeNull();
});
});