forked from yvv/decision
Fusion du lot F : objections redit / contredit, coquilles validées par leur auteur, messagerie, délai de 3 jours
Conflits avec B2 : docs.rs garde shown_tally / shown_evaluation et votable ; proposals.rs sert les totaux montrés et « votable » ; CONTROLES réunit C24 (« faire une objection », le panier par son adresse), C28–C33 et les deux passages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { clearScope, counts, fill, lines, sections, waiting } from '../../src/lib/basket';
|
||||
import { clearScope, counts, fill, lines, sections, votableIds, waiting } from '../../src/lib/basket';
|
||||
import type { Proposal, Unit } from '../../src/lib/types';
|
||||
|
||||
function prop(id: string, unitId: string, over: Partial<Proposal> = {}): Proposal {
|
||||
@@ -7,7 +7,7 @@ function prop(id: string, unitId: string, over: Partial<Proposal> = {}): Proposa
|
||||
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 }, evaluation: null, votedAtInertia: 3,
|
||||
blocked: false, signals: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
|
||||
blocked: false, votable: true, objections: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
|
||||
...over,
|
||||
};
|
||||
}
|
||||
@@ -72,4 +72,26 @@ describe('the basket of a vote', () => {
|
||||
]);
|
||||
expect(counts({ i1: 'for', e2: 'none', e4: 'against', e5: 'for' })).toEqual({ for: 2, against: 1, none: 1, total: 4 });
|
||||
});
|
||||
|
||||
it('a version held back by an objection is shown apart: no grouped gesture fills it, nothing is sent for it', () => {
|
||||
// E5 has the text in force and a rival held back; E6 is a new article, held back.
|
||||
const e5 = unit('E5', 'technique', prop('e5', 'E5', { status: 'in_force' }), [
|
||||
prop('e5r', 'E5', { blocked: true, votable: false }),
|
||||
]);
|
||||
const e6 = unit('E6', 'technique', null, [prop('e6', 'E6', { blocked: true, votable: false })]);
|
||||
const s = sections([e5, e6]);
|
||||
const [slotE5, slotE6] = s[0]?.slots ?? [];
|
||||
expect(slotE5?.versions.map((p) => p.id)).toEqual(['e5']);
|
||||
expect(slotE5?.held.map((p) => p.id)).toEqual(['e5r']);
|
||||
// A held article stays in sight, with nothing to choose.
|
||||
expect(slotE6?.versions).toEqual([]);
|
||||
expect(slotE6?.held.map((p) => p.id)).toEqual(['e6']);
|
||||
const all = s.flatMap((x) => x.slots);
|
||||
// The held rival does not compete now: the text in force is the only choice.
|
||||
expect(fill(all, 'for', {})).toEqual({ e5: 'for' });
|
||||
expect(waiting(all, {})).toEqual([]);
|
||||
// A choice kept from before the objection is not sent.
|
||||
expect(lines({ e5: 'for', e5r: 'for' }, votableIds(s))).toEqual([{ proposalId: 'e5', choice: 'for' }]);
|
||||
expect(clearScope(all, { e5: 'for', e5r: 'against', i1: 'for' })).toEqual({ i1: 'for' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { condense } from '../../src/lib/diff';
|
||||
import type { DiffPart } from '../../src/lib/types';
|
||||
|
||||
const text = (parts: DiffPart[]) => parts.map((p) => (p.op === 'same' ? p.text : `[${p.op === 'added' ? '+' : '-'}${p.text}]`)).join('');
|
||||
|
||||
describe('a typo fix, told around its change', () => {
|
||||
it('keeps a few words on each side of a change, cut between words', () => {
|
||||
const parts: DiffPart[] = [
|
||||
{ op: 'same', text: "Raison d'être\nDerrière une clé il n'y a qu'un seul être humain vivant. C" },
|
||||
{ op: 'removed', text: "'" },
|
||||
{ op: 'added', text: '’' },
|
||||
{ op: 'same', text: "est le seul et unique propos de la certification, qui donne à notre toile fiduciaire sa valeur." },
|
||||
];
|
||||
expect(text(condense(parts, 24))).toBe("… être humain vivant. C[-'][+’]est le seul et unique …");
|
||||
});
|
||||
|
||||
it('two changes far apart: the middle is cut; close ones stay together', () => {
|
||||
const far: DiffPart[] = [
|
||||
{ op: 'same', text: 'Je m' },
|
||||
{ op: 'removed', text: "'" },
|
||||
{ op: 'added', text: '’' },
|
||||
{ op: 'same', text: 'engage à vérifier la clé publique avec la personne, en sa présence, avant toute certification de son compte, et à n' },
|
||||
{ op: 'removed', text: "'" },
|
||||
{ op: 'added', text: '’' },
|
||||
{ op: 'same', text: 'en rien faire sinon.' },
|
||||
];
|
||||
expect(text(condense(far, 20))).toBe("Je m[-'][+’]engage à vérifier la … son compte, et à n[-'][+’]en rien faire sinon.");
|
||||
const near: DiffPart[] = [
|
||||
{ op: 'same', text: 'Je m' },
|
||||
{ op: 'removed', text: "'" },
|
||||
{ op: 'added', text: '’' },
|
||||
{ op: 'same', text: 'engage à n' },
|
||||
{ op: 'removed', text: "'" },
|
||||
{ op: 'added', text: '’' },
|
||||
{ op: 'same', text: 'utiliser qu' },
|
||||
];
|
||||
expect(condense(near, 20)).toEqual(near);
|
||||
});
|
||||
|
||||
it('nothing changed: the text as it is', () => {
|
||||
const same: DiffPart[] = [{ op: 'same', text: 'Un texte.' }];
|
||||
expect(condense(same)).toEqual(same);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { answerPanel, holding } from '../../src/lib/objections';
|
||||
import type { Objection, VersionRef } from '../../src/lib/types';
|
||||
|
||||
const ref = (id: string, over: Partial<VersionRef> = {}): VersionRef => ({
|
||||
id, title: id, status: 'open', unitId: `u-${id}`, code: id.toUpperCase(), author: null, authorId: null, ...over,
|
||||
});
|
||||
|
||||
function objection(over: Partial<Objection> = {}): Objection {
|
||||
return {
|
||||
id: 'o1', kind: 'redit', reason: 'Ces deux textes disent la même chose.', objector: 'Yvv', objectorInvited: false,
|
||||
mine: false, at: '', phase: 'author', cycle: 1, dueAt: null, answer: null, answerId: null, answeredAt: null,
|
||||
lift: null, liftedAt: null, heldVersion: ref('k1', { author: 'Chiara', authorId: 'dev:chiara' }),
|
||||
other: ref('e2', { status: 'in_force' }), iAnswer: true, canLift: false, canReiterate: false, held: true, ...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe('answering an objection', () => {
|
||||
it('a repetition: a proposal linked to it, on the article repeated, from its text', () => {
|
||||
expect(answerPanel(objection(), 'dev:chiara')).toEqual({ tab: 'propose', unitId: 'u-e2', proposalId: 'e2', objectionId: 'o1' });
|
||||
});
|
||||
|
||||
it('a contradiction: one rewords one’s own version in place', () => {
|
||||
expect(answerPanel(objection({ kind: 'contredit' }), 'dev:chiara')).toEqual({ tab: 'details', unitId: 'u-k1', proposalId: 'k1', edit: true });
|
||||
});
|
||||
|
||||
it('a contradiction on a seeded version: a linked proposal on its article', () => {
|
||||
const o = objection({ kind: 'contredit', heldVersion: ref('k1') });
|
||||
expect(answerPanel(o, 'dev:chiara')).toEqual({ tab: 'propose', unitId: 'u-k1', proposalId: 'k1', objectionId: 'o1' });
|
||||
});
|
||||
|
||||
it('nothing held back, nothing to answer; a lifted objection holds nothing', () => {
|
||||
expect(answerPanel(objection({ heldVersion: null }), 'dev:chiara')).toBeNull();
|
||||
const list = [objection(), objection({ id: 'o2', phase: 'lifted' }), objection({ id: 'o3', held: false })];
|
||||
expect(holding(list).map((o) => o.id)).toEqual(['o1']);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ function prop(id: string, unitId: string, over: Partial<Proposal> = {}): Proposa
|
||||
label: null, author: null, mine: false, revision: 1, createdAt: '', updatedAt: '', submittedAt: null, enteredAt: null,
|
||||
confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 },
|
||||
evaluation: { cast: 0, required: 50, reached: false, missing: 50, requiredRatio: 1, wellRated: false },
|
||||
votedAtInertia: 3, blocked: false, signals: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
|
||||
votedAtInertia: 3, blocked: false, votable: true, objections: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user