Repousser un projet en bloc, sauf ce dont on ne décide pas
Glisser la barre résumé décale toutes les phases du même nombre de jours, écarts conservés : un projet engagé glisse souvent, et le faire phase par phase les déformait. Restent en place ce qui est terminé — on ne réécrit pas le passé — et les phases à date imposée, nouveau booléen `fixed` pour ce qui est délégué ou tenu du dehors. D'où le format en version 6 : sans l'incrément, un binaire antérieur effacerait ces dates imposées à la première sauvegarde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,12 +12,14 @@ import {
|
||||
bornesPlanning,
|
||||
bornesProjet,
|
||||
bornesReference,
|
||||
ancreDecalage,
|
||||
cleTag,
|
||||
clotureEcraseDesStatuts,
|
||||
creerPhase,
|
||||
creerProjet,
|
||||
dateValide,
|
||||
decalerHorizon,
|
||||
decalerProjet,
|
||||
definirHorizon,
|
||||
deplacerPhase,
|
||||
dernierJourDuMois,
|
||||
@@ -42,6 +44,7 @@ import {
|
||||
modifierPhase,
|
||||
moisValide,
|
||||
normaliserTags,
|
||||
phasesDecalables,
|
||||
planningVide,
|
||||
pourEcriture,
|
||||
premierJourDuMois,
|
||||
@@ -368,6 +371,24 @@ describe('validerPlanning', () => {
|
||||
assert.equal(resultat.projects[0].notes, '');
|
||||
});
|
||||
|
||||
test('une phase d un fichier en version 5 n a pas de date imposée', () => {
|
||||
// Rien dans les données ne permettrait de deviner laquelle l'était : la
|
||||
// migration ne devine pas, elle met tout à `false`.
|
||||
const resultat = validerPlanning({ version: 5, projects: [projet({ phases: [phase()] })] });
|
||||
assert.equal(resultat.projects[0].phases[0].fixed, false);
|
||||
});
|
||||
|
||||
test('conserve une date imposée, et l écrit dans le fichier', () => {
|
||||
const resultat = validerPlanning({
|
||||
projects: [projet({ phases: [phase({ fixed: true })] })],
|
||||
});
|
||||
assert.equal(resultat.projects[0].phases[0].fixed, true);
|
||||
|
||||
const relu = JSON.parse(JSON.stringify(pourEcriture(resultat)));
|
||||
assert.equal(relu.version, VERSION_FORMAT);
|
||||
assert.equal(relu.projects[0].phases[0].fixed, true);
|
||||
});
|
||||
|
||||
test('conserve les notes d un projet', () => {
|
||||
const resultat = validerPlanning({
|
||||
projects: [projet({ notes: '## Contexte\n\n- [ ] relancer' })],
|
||||
@@ -901,6 +922,156 @@ describe('modifications', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- décalage d'un projet ---------------------------------------------------
|
||||
|
||||
describe('décaler un projet', () => {
|
||||
const enCours = projet({
|
||||
phases: [
|
||||
phase({ id: 'cadrage', start: '2026-08-03', end: '2026-08-21', status: 'done' }),
|
||||
phase({ id: 'chantier', start: '2026-08-24', end: '2026-10-02', status: 'doing' }),
|
||||
phase({
|
||||
id: 'livraison',
|
||||
start: '2026-10-12',
|
||||
end: '2026-10-12',
|
||||
status: 'todo',
|
||||
milestone: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const avecImposee = projet({
|
||||
phases: [
|
||||
phase({ id: 'chantier', start: '2026-08-24', end: '2026-10-02', status: 'doing' }),
|
||||
phase({
|
||||
id: 'audit-externe',
|
||||
start: '2026-09-07',
|
||||
end: '2026-09-18',
|
||||
status: 'todo',
|
||||
fixed: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
test('emporte tout ce qui n est pas terminé, et rien d autre', () => {
|
||||
assert.deepEqual(
|
||||
phasesDecalables(enCours).map((p) => p.id),
|
||||
['chantier', 'livraison']
|
||||
);
|
||||
});
|
||||
|
||||
test('n emporte pas une phase à date imposée, quel que soit son statut', () => {
|
||||
assert.deepEqual(
|
||||
phasesDecalables(avecImposee).map((p) => p.id),
|
||||
['chantier']
|
||||
);
|
||||
|
||||
// Le statut ne rachète rien : c'est bien la date qui est tenue, pas le
|
||||
// travail qui serait fini.
|
||||
const enCoursEtImposee = projet({
|
||||
phases: [phase({ status: 'doing', fixed: true })],
|
||||
});
|
||||
assert.deepEqual(phasesDecalables(enCoursEtImposee), []);
|
||||
});
|
||||
|
||||
test('une phase à date imposée reste en place quand le projet est repoussé', () => {
|
||||
const repousse = decalerProjet(avecImposee, 30);
|
||||
const audit = repousse.phases.find((p) => p.id === 'audit-externe');
|
||||
const chantier = repousse.phases.find((p) => p.id === 'chantier');
|
||||
|
||||
assert.deepEqual(
|
||||
{ start: audit.start, end: audit.end },
|
||||
{ start: '2026-09-07', end: '2026-09-18' }
|
||||
);
|
||||
assert.equal(chantier.start, '2026-09-23');
|
||||
// Le chantier passe désormais par-dessus l'audit : c'est l'information que
|
||||
// le geste doit faire apparaître, pas un cas à empêcher.
|
||||
assert.ok(ecartJours(chantier.start, audit.end) < 0);
|
||||
});
|
||||
|
||||
test('un projet dont tout est imposé ou terminé n offre aucune prise', () => {
|
||||
const tenu = projet({
|
||||
phases: [
|
||||
phase({ id: 'a', status: 'done' }),
|
||||
phase({ id: 'b', start: '2026-09-01', end: '2026-09-10', fixed: true }),
|
||||
],
|
||||
});
|
||||
assert.equal(ancreDecalage(tenu), null);
|
||||
assert.deepEqual(decalerProjet(tenu, 10).phases, tenu.phases);
|
||||
});
|
||||
|
||||
test('s ancre sur la première phase qui bouge, pas sur le début du projet', () => {
|
||||
// Le projet commence le 3 août, mais ce début est clos : c'est le 24 qui
|
||||
// suit le pointeur.
|
||||
assert.equal(ancreDecalage(enCours), '2026-08-24');
|
||||
assert.equal(ancreDecalage(projet({ phases: [phase()] })), '2026-08-01');
|
||||
});
|
||||
|
||||
test('un projet dont tout est terminé n a pas d ancre', () => {
|
||||
const clos = projet({ phases: [phase({ status: 'done' })] });
|
||||
assert.equal(ancreDecalage(clos), null);
|
||||
assert.equal(ancreDecalage(projet()), null, 'ni un projet sans phase');
|
||||
});
|
||||
|
||||
test('applique le même delta partout : les écarts sont conservés au jour près', () => {
|
||||
const repousse = decalerProjet(enCours, 14);
|
||||
const [cadrage, chantier, livraison] = repousse.phases;
|
||||
|
||||
assert.deepEqual(
|
||||
{ start: cadrage.start, end: cadrage.end },
|
||||
{ start: '2026-08-03', end: '2026-08-21' },
|
||||
'la phase terminée reste où elle est'
|
||||
);
|
||||
assert.deepEqual(
|
||||
{ start: chantier.start, end: chantier.end },
|
||||
{ start: '2026-09-07', end: '2026-10-16' }
|
||||
);
|
||||
assert.deepEqual(
|
||||
{ start: livraison.start, end: livraison.end },
|
||||
{ start: '2026-10-26', end: '2026-10-26' },
|
||||
'le jalon suit, et tient toujours sur un jour'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
ecartJours(chantier.end, livraison.start),
|
||||
ecartJours(enCours.phases[1].end, enCours.phases[2].start)
|
||||
);
|
||||
});
|
||||
|
||||
test('recule aussi bien qu il avance', () => {
|
||||
const recule = decalerProjet(enCours, -7);
|
||||
assert.equal(recule.phases[1].start, '2026-08-17');
|
||||
// Rien n'interdit de chevaucher une phase terminée : il n'y a pas de
|
||||
// dépendances entre phases, et une butée en inventerait une.
|
||||
assert.ok(ecartJours(recule.phases[1].start, recule.phases[0].end) > 0);
|
||||
});
|
||||
|
||||
test('un décalage nul rend le projet inchangé', () => {
|
||||
assert.equal(decalerProjet(enCours, 0), enCours);
|
||||
});
|
||||
|
||||
test('la référence figée ne suit pas : c est elle qui mesure la dérive', () => {
|
||||
const fige = figerReference(enCours, '2026-08-01');
|
||||
const repousse = decalerProjet(fige, 21);
|
||||
|
||||
assert.deepEqual(repousse.phases[1].baseline, { start: '2026-08-24', end: '2026-10-02' });
|
||||
assert.equal(deriveProjet(repousse), 21);
|
||||
});
|
||||
|
||||
test('retrie les phases quand le décalage change leur ordre', () => {
|
||||
const desordonne = projet({
|
||||
phases: [
|
||||
phase({ id: 'a', start: '2026-08-01', end: '2026-08-05', status: 'todo' }),
|
||||
phase({ id: 'b', start: '2026-08-10', end: '2026-08-15', status: 'done' }),
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
decalerProjet(desordonne, 30).phases.map((p) => p.id),
|
||||
['b', 'a']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// --- cycle de vie -----------------------------------------------------------
|
||||
|
||||
describe('cycle de vie', () => {
|
||||
|
||||
Reference in New Issue
Block a user