144 lines
4.8 KiB
JavaScript
144 lines
4.8 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|||
|
|
import { normalizeWalkthrough, parseModelJson, MAX_STOPS } from './schema.js';
|
||
|
|
|
||
|
|
const ALIASES = new Map([
|
||
|
|
['h1', 'working:src/a.ts:aaaa1111'],
|
||
|
|
['h2', 'working:src/a.ts:bbbb2222'],
|
||
|
|
['h3', 'working:src/b.ts:cccc3333'],
|
||
|
|
]);
|
||
|
|
|
||
|
|
const walkthrough = (chapters) => ({ title: 'Change', focus: 'why', chapters });
|
||
|
|
|
||
|
|
describe('normalizeWalkthrough', () => {
|
||
|
|
it('maps aliases to real hunk ids and assigns stable local ids', () => {
|
||
|
|
const result = normalizeWalkthrough(walkthrough([
|
||
|
|
{
|
||
|
|
title: 'Data',
|
||
|
|
icon: 'doc',
|
||
|
|
blurb: 'shape first',
|
||
|
|
stops: [
|
||
|
|
{ title: 'New field', hunks: ['h1', 'h2'], importance: 'critical', prose: 'Adds a field.' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
]), ALIASES);
|
||
|
|
|
||
|
|
expect(result.chapters[0].id).toBe('chapter-1');
|
||
|
|
expect(result.chapters[0].stops[0]).toMatchObject({
|
||
|
|
id: 'stop-1-1',
|
||
|
|
hunkIds: ['working:src/a.ts:aaaa1111', 'working:src/a.ts:bbbb2222'],
|
||
|
|
importance: 'critical',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('drops invented aliases instead of rendering a broken anchor', () => {
|
||
|
|
const result = normalizeWalkthrough(walkthrough([
|
||
|
|
{
|
||
|
|
title: 'Data',
|
||
|
|
icon: 'doc',
|
||
|
|
blurb: '',
|
||
|
|
stops: [
|
||
|
|
{ title: 'Mixed', hunks: ['h1', 'h99', 'nonsense'], importance: 'normal', prose: 'Something.' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
]), ALIASES);
|
||
|
|
|
||
|
|
expect(result.chapters[0].stops[0].hunkIds).toEqual(['working:src/a.ts:aaaa1111']);
|
||
|
|
expect(result.droppedAnchors).toBe(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('anchors each hunk to a single stop', () => {
|
||
|
|
const result = normalizeWalkthrough(walkthrough([
|
||
|
|
{
|
||
|
|
title: 'Data',
|
||
|
|
icon: 'doc',
|
||
|
|
blurb: '',
|
||
|
|
stops: [
|
||
|
|
{ title: 'First', hunks: ['h1'], importance: 'normal', prose: 'One.' },
|
||
|
|
{ title: 'Second', hunks: ['h1', 'h2'], importance: 'normal', prose: 'Two.' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
]), ALIASES);
|
||
|
|
|
||
|
|
expect(result.chapters[0].stops[0].hunkIds).toEqual(['working:src/a.ts:aaaa1111']);
|
||
|
|
expect(result.chapters[0].stops[1].hunkIds).toEqual(['working:src/a.ts:bbbb2222']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('discards stops left with no anchor or no prose', () => {
|
||
|
|
const result = normalizeWalkthrough(walkthrough([
|
||
|
|
{
|
||
|
|
title: 'Data',
|
||
|
|
icon: 'doc',
|
||
|
|
blurb: '',
|
||
|
|
stops: [
|
||
|
|
{ title: 'Ghost', hunks: ['h99'], importance: 'normal', prose: 'About nothing.' },
|
||
|
|
{ title: 'Silent', hunks: ['h1'], importance: 'normal', prose: ' ' },
|
||
|
|
{ title: 'Real', hunks: ['h2'], importance: 'normal', prose: 'Actual explanation.' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
]), ALIASES);
|
||
|
|
|
||
|
|
expect(result.chapters[0].stops.map((stop) => stop.title)).toEqual(['Real']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects a response whose stops all fall away', () => {
|
||
|
|
expect(() => normalizeWalkthrough(walkthrough([
|
||
|
|
{ title: 'Empty', icon: 'doc', blurb: '', stops: [{ title: 'Ghost', hunks: ['h99'], importance: 'normal', prose: 'x' }] },
|
||
|
|
]), ALIASES)).toThrow('no usable stops');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clamps chapter titles and falls back on unknown enums', () => {
|
||
|
|
const result = normalizeWalkthrough(walkthrough([
|
||
|
|
{
|
||
|
|
title: 'An extremely long chapter title that will not fit the column',
|
||
|
|
icon: 'rocket',
|
||
|
|
blurb: '',
|
||
|
|
stops: [{ title: 'A', hunks: ['h1'], importance: 'urgent', prose: 'Text.' }],
|
||
|
|
},
|
||
|
|
]), ALIASES);
|
||
|
|
|
||
|
|
expect(result.chapters[0].title.length).toBeLessThanOrEqual(24);
|
||
|
|
expect(result.chapters[0].icon).toBe('doc');
|
||
|
|
expect(result.chapters[0].stops[0].importance).toBe('normal');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('caps the total number of stops', () => {
|
||
|
|
const many = Array.from({ length: 30 }, (_, index) => ({
|
||
|
|
title: `Stop ${index}`,
|
||
|
|
hunks: [['h1', 'h2', 'h3'][index % 3]],
|
||
|
|
importance: 'normal',
|
||
|
|
prose: 'Text.',
|
||
|
|
}));
|
||
|
|
|
||
|
|
const result = normalizeWalkthrough(
|
||
|
|
walkthrough([{ title: 'All', icon: 'doc', blurb: '', stops: many }]),
|
||
|
|
ALIASES,
|
||
|
|
);
|
||
|
|
|
||
|
|
const total = result.chapters.reduce((sum, chapter) => sum + chapter.stops.length, 0);
|
||
|
|
expect(total).toBeLessThanOrEqual(MAX_STOPS);
|
||
|
|
// Only three aliases exist and each is used once, so the real cap here is
|
||
|
|
// the alias pool, not the stop limit.
|
||
|
|
expect(total).toBe(3);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('parseModelJson', () => {
|
||
|
|
it('parses a clean object', () => {
|
||
|
|
expect(parseModelJson('{"title":"x"}')).toEqual({ title: 'x' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('unwraps a fenced block', () => {
|
||
|
|
expect(parseModelJson('```json\n{"title":"x"}\n```')).toEqual({ title: 'x' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('recovers an object followed by stray prose', () => {
|
||
|
|
expect(parseModelJson('{"title":"x"}\n\nHope that helps!')).toEqual({ title: 'x' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('fails loudly on unusable output', () => {
|
||
|
|
expect(() => parseModelJson('')).toThrow('empty response');
|
||
|
|
expect(() => parseModelJson('no json at all')).toThrow('no JSON object');
|
||
|
|
expect(() => parseModelJson('{"broken":')).toThrow('not valid JSON');
|
||
|
|
});
|
||
|
|
});
|