forked from yvv/decision
128 lines
6.4 KiB
TypeScript
128 lines
6.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { Proposal, Unit } from '../../src/lib/types';
|
|
import { articles, deepSections, excerpts, level } from '../../src/lib/versions';
|
|
|
|
function prop(id: string, unitId: string, over: Partial<Proposal> = {}): Proposal {
|
|
return {
|
|
id, unitId, kind: 'text', inertiaTarget: null, title: unitId, body: `texte ${id}`, status: 'open', origin: 'seed',
|
|
label: null, author: null, mine: false, revision: 1, createdAt: '', updatedAt: '', submittedAt: null, enteredAt: null,
|
|
confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, tallyPending: { for: 0, against: 0 }, tallySimulated: { for: 0, against: 0 },
|
|
evaluation: { cast: 0, required: 50, reached: false, missing: 50, requiredRatio: 1, wellRated: false },
|
|
votedAtInertia: 3, blocked: false, votable: true, objections: 0, myVote: null, myVoteState: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function unit(id: string, proposals: Proposal[]): Unit {
|
|
return { id, code: id, section: null, position: 0, inertia: 3, inertiaChanges: [], inForce: null, proposals };
|
|
}
|
|
|
|
/** The reader's list: every open proposal, unit by unit, in document order. */
|
|
function items(...units: Unit[]) {
|
|
return units.flatMap((u) => u.proposals.map((proposal) => ({ unit: u, proposal })));
|
|
}
|
|
|
|
const tally = (f: number, against = 0) => ({ tally: { for: f, against } });
|
|
|
|
function at<T>(xs: readonly T[], i: number): T {
|
|
const x = xs[i];
|
|
if (x === undefined) throw new Error(`nothing at ${i}`);
|
|
return x;
|
|
}
|
|
|
|
describe('the versions of an article', () => {
|
|
it('one line per article, in document order, its versions inside', () => {
|
|
const e2 = unit('E2', [prop('a', 'E2')]);
|
|
const e3 = unit('E3', [prop('b1', 'E3', { label: '1.0.0' }), prop('b2', 'E3', { label: '0.4.0-draft' })]);
|
|
const e4 = unit('E4', [prop('c', 'E4')]);
|
|
const list = articles(items(e2, e3, e4));
|
|
expect(list.map((a) => a.unit.id)).toEqual(['E2', 'E3', 'E4']);
|
|
expect(at(list, 1).versions.map((p) => p.id)).toEqual(['b1', 'b2']);
|
|
});
|
|
|
|
it('the best version first: the highest water, then more for, then fewer against', () => {
|
|
const u = unit('E3', [
|
|
prop('low', 'E3', tally(3)),
|
|
prop('high', 'E3', tally(20, 4)),
|
|
prop('same', 'E3', tally(20, 1)),
|
|
]);
|
|
const a = at(articles(items(u)), 0);
|
|
expect(a.versions.map((p) => p.id)).toEqual(['same', 'high', 'low']);
|
|
expect(a.leads).toBe(true);
|
|
});
|
|
|
|
it('equal versions keep their order, and none is said to lead', () => {
|
|
const u = unit('E3', [prop('first', 'E3'), prop('second', 'E3')]);
|
|
const a = at(articles(items(u)), 0);
|
|
expect(a.versions.map((p) => p.id)).toEqual(['first', 'second']);
|
|
expect(a.leads).toBe(false);
|
|
});
|
|
|
|
it('a blocked version never comes first: it cannot enter into force', () => {
|
|
const u = unit('E3', [prop('blocked', 'E3', { ...tally(40), blocked: true }), prop('free', 'E3', tally(2))]);
|
|
expect(at(at(articles(items(u)), 0).versions, 0).id).toBe('free');
|
|
});
|
|
|
|
it('a filter keeps an article when one of its versions passes, with all its versions', () => {
|
|
const wellRated = { cast: 9, required: 50, reached: false, missing: 41, requiredRatio: 1, wellRated: true };
|
|
const e3 = unit('E3', [prop('theirs', 'E3'), prop('mine', 'E3', { mine: true, evaluation: wellRated })]);
|
|
const e4 = unit('E4', [prop('other', 'E4')]);
|
|
for (const filter of ['mine', 'wellrated'] as const) {
|
|
const list = articles(items(e3, e4), filter);
|
|
expect(list.map((a) => a.unit.id), filter).toEqual(['E3']);
|
|
expect(at(list, 0).versions).toHaveLength(2);
|
|
}
|
|
expect(articles(items(e3, e4), 'all')).toHaveLength(2);
|
|
});
|
|
|
|
it('the water level is the share of the threshold reached', () => {
|
|
expect(level(prop('x', 'E1', tally(25)))).toBe(0.5);
|
|
expect(level(prop('x', 'E1', tally(80)))).toBe(1);
|
|
expect(level(prop('x', 'E1', { evaluation: null }))).toBe(0);
|
|
});
|
|
|
|
it('sibling versions are told apart where they part ways', () => {
|
|
const a = 'Je me suis assuré de connaître suffisamment la personne qui gère cette clé publique. Connaître suffisamment ne signifie pas avoir vu.';
|
|
const b = 'Je me suis assuré de connaître suffisamment la personne qui gère cette clé publique : je sais la recontacter.';
|
|
expect(excerpts([a, b])).toEqual([
|
|
'… gère cette clé publique. Connaître suffisamment ne signifie pas avoir vu.',
|
|
'… gère cette clé publique : je sais la recontacter.',
|
|
]);
|
|
// A short common start reads well as it is.
|
|
expect(excerpts(['Je certifie une personne.', 'Je certifie deux personnes.'])).toEqual([
|
|
'Je certifie une personne.',
|
|
'Je certifie deux personnes.',
|
|
]);
|
|
expect(excerpts(['Seule.'])).toEqual(['Seule.']);
|
|
});
|
|
});
|
|
|
|
describe('the open proposals by section (lot U)', () => {
|
|
const inSection = (u: Unit, section: string | null): Unit => ({ ...u, section });
|
|
it('each section holds its articles in document order, and its order when it closes there', () => {
|
|
const t1 = inSection(unit('T1', [prop('t1', 'T1')]), 'technique');
|
|
const c1 = inSection(unit('C1', [prop('c1', 'C1')]), 'conclusion');
|
|
const t2 = inSection(unit('T2', [prop('t2', 'T2')]), 'technique');
|
|
const order = { ...inSection(unit('O', []), 'technique'), kind: 'order' as const };
|
|
const list = deepSections(articles(items(t1, t2, c1)), [order], ['introduction', 'technique', 'conclusion']);
|
|
expect(list.map((s) => s.key)).toEqual(['technique', 'conclusion']);
|
|
expect(at(list, 0).articles.map((a) => a.unit.id)).toEqual(['T1', 'T2']);
|
|
expect(at(list, 0).order?.id).toBe('O');
|
|
expect(at(list, 1).order).toBeUndefined();
|
|
});
|
|
it('a section with only its order to show is kept, at its place in the document', () => {
|
|
const c1 = inSection(unit('C1', [prop('c1', 'C1')]), 'conclusion');
|
|
const order = { ...inSection(unit('O', []), 'introduction'), kind: 'order' as const };
|
|
const list = deepSections(articles(items(c1)), [order], ['introduction', 'conclusion']);
|
|
expect(list.map((s) => [s.key, s.articles.length, s.order?.id ?? null])).toEqual([
|
|
['introduction', 0, 'O'],
|
|
['conclusion', 1, null],
|
|
]);
|
|
});
|
|
it('the articles without a section come after the sections the document lists', () => {
|
|
const loose = unit('N1', [prop('n1', 'N1')]);
|
|
const c1 = inSection(unit('C1', [prop('c1', 'C1')]), 'conclusion');
|
|
expect(deepSections(articles(items(loose, c1)), [], ['conclusion']).map((s) => s.key)).toEqual(['conclusion', null]);
|
|
});
|
|
});
|