Surface the besluit-driven assistance that was previously silent: a pure besluitGuidance() (kern passage count + needs-reason flag) rendered as a hint below the besluit panel in behandel-scherm (warning to pick a reden, else info on how many standaardteksten were inserted). Add a read-only notice on letter-composer for a pure viewer (no edit/approve/reject/send right, e.g. admin) so the read-only letter isn't mistaken for a broken editor — the "reverted to a view of the letter" confusion was a role issue (non-drafter sees the composer). +besluitGuidance spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { Besluit, LetterBlock, LibraryPassage } from './brief';
|
|
import { besluitGuidance, inferSelection, passagesForBesluit, redenenFor } from './besluit';
|
|
|
|
const block = (t: string): LibraryPassage['content'] => ({
|
|
paragraphs: [{ nodes: [{ type: 'text', text: t }] }],
|
|
});
|
|
|
|
const p = (over: Partial<LibraryPassage>): LibraryPassage => ({
|
|
passageId: over.passageId ?? 'x',
|
|
scope: 'global',
|
|
sectionKey: 'kern',
|
|
label: over.label ?? 'x',
|
|
content: block('x'),
|
|
version: 1,
|
|
...over,
|
|
});
|
|
|
|
const lib: LibraryPassage[] = [
|
|
p({ passageId: 'intro', besluit: undefined }), // shared, any besluit
|
|
p({ passageId: 'pos', besluit: 'positief' }),
|
|
p({ passageId: 'neg', besluit: 'negatief' }),
|
|
p({
|
|
passageId: 'neg-scholing',
|
|
besluit: 'negatief',
|
|
reason: 'onvoldoende_scholing',
|
|
label: 'Onvoldoende scholing',
|
|
}),
|
|
p({
|
|
passageId: 'neg-gegevens',
|
|
besluit: 'negatief',
|
|
reason: 'onjuiste_gegevens',
|
|
label: 'Onjuiste gegevens',
|
|
}),
|
|
p({ passageId: 'slot-x', sectionKey: 'slot', besluit: undefined }), // not kern → never offered
|
|
];
|
|
|
|
describe('passagesForBesluit', () => {
|
|
it('positief = shared intro + the positief passage, no negatief/reason passages', () => {
|
|
const ids = passagesForBesluit(lib, 'positief', []).map((x) => x.passageId);
|
|
expect(ids).toEqual(['intro', 'pos']);
|
|
});
|
|
|
|
it('negatief without redenen = intro + negatief base, but no reason-specific passages', () => {
|
|
const ids = passagesForBesluit(lib, 'negatief', []).map((x) => x.passageId);
|
|
expect(ids).toEqual(['intro', 'neg']);
|
|
});
|
|
|
|
it('negatief with a reden ticked includes that reason-specific passage only', () => {
|
|
const ids = passagesForBesluit(lib, 'negatief', ['onvoldoende_scholing']).map(
|
|
(x) => x.passageId,
|
|
);
|
|
expect(ids).toEqual(['intro', 'neg', 'neg-scholing']);
|
|
});
|
|
|
|
it('preserves library order (= reading order)', () => {
|
|
const ids = passagesForBesluit(lib, 'negatief', [
|
|
'onjuiste_gegevens',
|
|
'onvoldoende_scholing',
|
|
]).map((x) => x.passageId);
|
|
expect(ids).toEqual(['intro', 'neg', 'neg-scholing', 'neg-gegevens']);
|
|
});
|
|
|
|
it('never offers non-kern passages', () => {
|
|
expect(passagesForBesluit(lib, 'positief', []).some((x) => x.sectionKey !== 'kern')).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('redenenFor', () => {
|
|
it('derives reason checkboxes (code + label) from the negatief reason passages', () => {
|
|
expect(redenenFor(lib, 'negatief')).toEqual([
|
|
{ code: 'onvoldoende_scholing', label: 'Onvoldoende scholing' },
|
|
{ code: 'onjuiste_gegevens', label: 'Onjuiste gegevens' },
|
|
]);
|
|
});
|
|
|
|
it('positief has no reason-specific redenen', () => {
|
|
expect(redenenFor(lib, 'positief')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('inferSelection', () => {
|
|
// Build the kern blocks a besluit would produce, then read the selection back off them.
|
|
const kern = (besluit: Besluit, reasons: string[]): LetterBlock[] =>
|
|
passagesForBesluit(lib, besluit, reasons).map((p, i) => ({
|
|
type: 'passage',
|
|
blockId: `local-${i + 1}`,
|
|
sourcePassageId: p.passageId,
|
|
sourceVersion: p.version,
|
|
content: p.content,
|
|
edited: false,
|
|
}));
|
|
|
|
it('round-trips a positief selection', () => {
|
|
expect(inferSelection(kern('positief', []), lib)).toEqual({ besluit: 'positief', reasons: [] });
|
|
});
|
|
|
|
it('round-trips a negatief selection with redenen (in order)', () => {
|
|
const blocks = kern('negatief', ['onjuiste_gegevens', 'onvoldoende_scholing']);
|
|
expect(inferSelection(blocks, lib)).toEqual({
|
|
besluit: 'negatief',
|
|
reasons: ['onvoldoende_scholing', 'onjuiste_gegevens'], // library order
|
|
});
|
|
});
|
|
|
|
it('an empty kern (nothing chosen) infers no besluit', () => {
|
|
expect(inferSelection([], lib)).toEqual({ besluit: null, reasons: [] });
|
|
});
|
|
|
|
it('ignores free-text blocks and unknown passage ids', () => {
|
|
const blocks: LetterBlock[] = [
|
|
{ type: 'freeText', blockId: 'local-9', content: block('vrij') },
|
|
...kern('positief', []),
|
|
];
|
|
expect(inferSelection(blocks, lib)).toEqual({ besluit: 'positief', reasons: [] });
|
|
});
|
|
});
|
|
|
|
describe('besluitGuidance', () => {
|
|
it('positief: counts inserted passages, no reden needed (positief has no redenen)', () => {
|
|
expect(besluitGuidance(lib, 'positief', [])).toEqual({ insertedCount: 2, needsReason: false });
|
|
});
|
|
|
|
it('negatief without a reden: flags that a reden must be chosen', () => {
|
|
expect(besluitGuidance(lib, 'negatief', [])).toEqual({ insertedCount: 2, needsReason: true });
|
|
});
|
|
|
|
it('negatief with a reden: no longer flags, and the reason passage is counted', () => {
|
|
expect(besluitGuidance(lib, 'negatief', ['onvoldoende_scholing'])).toEqual({
|
|
insertedCount: 3,
|
|
needsReason: false,
|
|
});
|
|
});
|
|
});
|