Add goal tree toolbox module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s

This commit is contained in:
Shinuwa 2026-08-22 18:15:39 +02:00
parent e4cbe3e087
commit 59fdcb0927
63 changed files with 4062 additions and 252 deletions

View file

@ -101,9 +101,9 @@ test("diablo4 data and route are available", async () => {
assert.match(listingSource, /diablo4\.filterOptions\.affixes/);
await readFile("website/public/static/img/games/diablo4/card-cover.png");
await readFile("website/public/static/img/games/diablo4/hero-bg.png");
await readFile("website/public/static/img/diablo4/axe.svg");
await readFile("website/public/static/img/diablo4/cube.svg");
await readFile("website/public/static/img/diablo4/fire.svg");
await readFile("website/public/static/img/diablo4/shield.svg");
await readFile("website/public/static/img/diablo4/wing.svg");
await readFile("website/public/static/icons/axe.svg");
await readFile("website/public/static/icons/cube.svg");
await readFile("website/public/static/icons/fire.svg");
await readFile("website/public/static/icons/shield.svg");
await readFile("website/public/static/icons/wing.svg");
});

View file

@ -269,6 +269,67 @@ export function validateSiteContent(site) {
"toolboxes.modules.calculator.useEntryTitle",
"toolboxes.modules.calculator.renameTitle",
"toolboxes.modules.calculator.deleteTitle",
"toolboxes.modules.goalTree.editorTitle",
"toolboxes.modules.goalTree.treeTitle",
"toolboxes.modules.goalTree.addRootButton",
"toolboxes.modules.goalTree.addRootSchemaButton",
"toolboxes.modules.goalTree.addChildButton",
"toolboxes.modules.goalTree.addChildTitle",
"toolboxes.modules.goalTree.addContentItemTitle",
"toolboxes.modules.goalTree.addContentItemTooltip",
"toolboxes.modules.goalTree.verticalViewTitle",
"toolboxes.modules.goalTree.horizontalViewTitle",
"toolboxes.modules.goalTree.cardActionsLabel",
"toolboxes.modules.goalTree.editTitle",
"toolboxes.modules.goalTree.baseTab",
"toolboxes.modules.goalTree.contentTab",
"toolboxes.modules.goalTree.advancedTab",
"toolboxes.modules.goalTree.resetTimeLabel",
"toolboxes.modules.goalTree.weeklyResetDayLabel",
"toolboxes.modules.goalTree.titleLabel",
"toolboxes.modules.goalTree.listTitleLabel",
"toolboxes.modules.goalTree.kindLabel",
"toolboxes.modules.goalTree.stepKind",
"toolboxes.modules.goalTree.textKind",
"toolboxes.modules.goalTree.checklistKind",
"toolboxes.modules.goalTree.iconLabel",
"toolboxes.modules.goalTree.iconColorLabel",
"toolboxes.modules.goalTree.targetQuantityLabel",
"toolboxes.modules.goalTree.quantitySectionTitle",
"toolboxes.modules.goalTree.progressSectionTitle",
"toolboxes.modules.goalTree.autoProgressLabel",
"toolboxes.modules.goalTree.manualProgressLabel",
"toolboxes.modules.goalTree.autoProgressTitle",
"toolboxes.modules.goalTree.manualProgressTitle",
"toolboxes.modules.goalTree.autoProgressHint",
"toolboxes.modules.goalTree.manualProgressHint",
"toolboxes.modules.goalTree.contentSectionTitle",
"toolboxes.modules.goalTree.itemLabelLabel",
"toolboxes.modules.goalTree.itemLabelPlaceholder",
"toolboxes.modules.goalTree.itemCheckedLabel",
"toolboxes.modules.goalTree.itemTotalLabel",
"toolboxes.modules.goalTree.itemTotalPlaceholder",
"toolboxes.modules.goalTree.itemTotalHelp",
"toolboxes.modules.goalTree.prerequisitesSectionTitle",
"toolboxes.modules.goalTree.contentTabHint",
"toolboxes.modules.goalTree.stepContentUnavailable",
"toolboxes.modules.goalTree.decrementItemTitle",
"toolboxes.modules.goalTree.incrementItemTitle",
"toolboxes.modules.goalTree.addContentItemButton",
"toolboxes.modules.goalTree.formulaPlaceholder",
"toolboxes.modules.goalTree.formulaHelp",
"toolboxes.modules.goalTree.resetTypeLabel",
"toolboxes.modules.goalTree.noneReset",
"toolboxes.modules.goalTree.dailyReset",
"toolboxes.modules.goalTree.weeklyReset",
"toolboxes.modules.goalTree.closeButton",
"toolboxes.modules.goalTree.deleteButton",
"toolboxes.modules.goalTree.deleteTitle",
"toolboxes.modules.goalTree.reorderTitle",
"toolboxes.modules.goalTree.showChildrenTitle",
"toolboxes.modules.goalTree.hideChildrenTitle",
"toolboxes.modules.goalTree.emptySelection",
"toolboxes.modules.goalTree.emptyTree",
"toolboxes.modules.table.toolbarLabel",
"toolboxes.modules.table.gridLabel",
"toolboxes.modules.table.cellLabel",
@ -557,7 +618,7 @@ export function validateSiteContent(site) {
});
const libraryToolDocs = valueAt(site, "library.toolDocs");
["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
const docs = libraryToolDocs?.[toolType];
assert.equal(typeof docs, "object", `library.toolDocs.${toolType} must be an object`);
assert.ok(Array.isArray(docs.controls), `library.toolDocs.${toolType}.controls must be an array`);
@ -770,7 +831,7 @@ export function validateLibraryData(payload) {
assert.deepEqual(new Set(twoColumnIds), moduleIds, "library.toolbox.moduleOrder.two must contain every module exactly once");
assert.equal(twoColumnIds.length, moduleIds.size, "library.toolbox.moduleOrder.two must not contain duplicate modules");
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);
});
@ -790,6 +851,11 @@ export function validateLibraryData(payload) {
assert.ok(Array.isArray(taskData.relations), "library task planner example must contain relations");
assert.ok(taskData.relations.length >= 1, "library task planner example must include a parent/child relation");
const goalTreeModule = payload.toolbox.modules.find((module) => module.type === "goalTree");
const goalTreeData = payload.modules[goalTreeModule.id];
assert.ok(Array.isArray(goalTreeData.nodes), "library goal tree example must contain nodes");
assert.ok(goalTreeData.nodes.some((node) => node.parentId), "library goal tree example must include a parent/child node");
const equipmentModule = payload.toolbox.modules.find((module) => module.type === "equipmentPlanner");
const equipmentData = payload.modules[equipmentModule.id];
assert.ok(Array.isArray(equipmentData.types), "library equipment planner example must contain types");

View file

@ -128,6 +128,7 @@ test("legacy icons and primary images are available", async () => {
await readFile("website/public/static/icons/copy.svg", "utf8");
await readFile("website/public/static/icons/abacus.svg", "utf8");
await readFile("website/public/static/icons/calculator.svg", "utf8");
await readFile("website/public/static/icons/diagram.svg", "utf8");
await readFile("website/public/static/icons/map.svg", "utf8");
await readFile("website/public/static/icons/enter.svg", "utf8");
await readFile("website/public/static/icons/export.svg", "utf8");
@ -142,6 +143,11 @@ test("legacy icons and primary images are available", async () => {
await readFile("website/public/static/icons/trashcan.svg", "utf8");
await readFile("website/public/static/icons/zoom.svg", "utf8");
await readFile("website/public/static/icons/warning.svg", "utf8");
await readFile("website/public/static/icons/axe.svg", "utf8");
await readFile("website/public/static/icons/cube.svg", "utf8");
await readFile("website/public/static/icons/fire.svg", "utf8");
await readFile("website/public/static/icons/shield.svg", "utf8");
await readFile("website/public/static/icons/wing.svg", "utf8");
await readFile("website/public/favicon.ico");
await readFile("website/public/static/img/dragon-home.png");
await readFile("website/public/static/img/dragon-sidebar.png");

View file

@ -62,6 +62,7 @@ test("toolbox storage, cards and pages are wired", async () => {
assert.match(toolboxStorage, /normalizeTimerData/);
assert.match(toolboxStorage, /normalizeCalendarData/);
assert.match(toolboxStorage, /normalizeTaskPlannerData/);
assert.match(toolboxStorage, /normalizeGoalTreeData/);
assert.match(toolboxStorage, /normalizeEquipmentPlannerData/);
assert.match(toolboxStorage, /summarizeEquipmentPlannerData/);
assert.match(toolboxStorage, /normalizeImageAnnotationData/);
@ -69,6 +70,7 @@ test("toolbox storage, cards and pages are wired", async () => {
assert.match(storageCompactorRegistry, /export const MODULE_DATA_COMPACTORS/);
assert.match(storageCompactorRegistry, /timer: compactTimerDataForStorage/);
assert.match(storageCompactorRegistry, /calendar: compactCalendarDataForStorage/);
assert.match(storageCompactorRegistry, /goalTree: compactGoalTreeDataForStorage/);
assert.match(timerStorage, /scrollResults/);
assert.match(calendarStorage, /normalizeCalendarData/);
assert.match(toolboxStorage, /nextId\("calendar"\)/);
@ -153,6 +155,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8");
const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8");
const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8");
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
const goalTreeUtils = await readFile("website/src/features/toolboxes/modules/goalTreeUtils.js", "utf8");
const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8");
const equipmentPlannerEditors = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerEditors.jsx", "utf8");
const equipmentPlannerItem = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerItem.jsx", "utf8");
@ -189,6 +193,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(moduleRegistry, /timer:/);
assert.match(moduleRegistry, /calendar:/);
assert.match(moduleRegistry, /taskPlanner:/);
assert.match(moduleRegistry, /goalTree:/);
assert.match(moduleRegistry, /equipmentPlanner:/);
assert.match(moduleMetadataRegistry, /Planificateur d'équipements/);
assert.match(moduleRegistry, /imageAnnotation:/);
@ -346,6 +351,38 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(taskPlannerUtils, /export function applyDueResets/);
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
assert.doesNotMatch(taskPlannerModule, /canMoveGroup:/);
assert.match(goalTreeModule, /export function GoalTreeModule/);
assert.match(goalTreeModule, /normalizeGoalTreeData/);
assert.match(goalTreeModule, /applyGoalTreeDueResets/);
assert.match(goalTreeModule, /TextExchangeActions/);
assert.match(goalTreeModule, /DiagramConnectors/);
assert.match(goalTreeModule, /Tooltip/);
assert.match(goalTreeModule, /GoalTreeIconPicker/);
assert.match(goalTreeModule, /GoalTreeContentEditor/);
assert.match(goalTreeModule, /GoalTreeNodeContentPreview/);
assert.match(goalTreeModule, /GoalTreeChecklistInlineControl/);
assert.match(goalTreeModule, /GoalTreeStepLeafControl/);
assert.match(goalTreeModule, /GoalTreePrerequisitePicker/);
assert.match(goalTreeModule, /function toggleBranch/);
assert.match(goalTreeModule, /function editNode/);
assert.match(goalTreeModule, /function addNodeContentItem/);
assert.match(goalTreeModule, /closedSiblingIds/);
assert.match(goalTreeModule, /useGroupedReorder/);
assert.match(goalTreeModule, /const treeOrientation = data\.orientation \|\| "horizontal"/);
assert.match(goalTreeModule, /const reorderOrientation = treeOrientation === "horizontal" \? "vertical" : "horizontal"/);
assert.match(goalTreeModule, /orientation: reorderOrientation/);
assert.match(goalTreeModule, /orientation={treeOrientation}/);
assert.match(goalTreeModule, /module-scroll-button goal-tree-orientation-switch/);
assert.match(goalTreeModule, /Icon name="rotate"/);
assert.match(goalTreeModule, /function toggleLinkedParent/);
assert.match(goalTreeModule, /grouped: false/);
assert.match(goalTreeModule, /getGoalTreeConvergenceKeepOpenIds/);
assert.match(goalTreeModule, /progress\.current/);
assert.match(goalTreeModule, /label: textContent\.itemLabelPlaceholder \|\| "Élément"/);
assert.doesNotMatch(goalTreeUtils, /"calculator"/);
assert.match(goalTreeModule, /getItemProps\(\{ itemId: node\.id, parentId \}\)/);
assert.match(goalTreeUtils, /export function calculateGoalTreeFormula/);
assert.match(goalTreeUtils, /export function evaluateGoalTreeNodes/);
assert.match(equipmentPlannerModule, /export function EquipmentPlannerModule/);
assert.match(equipmentPlannerModule, /normalizeEquipmentPlannerData/);
assert.match(equipmentPlannerModule, /summarizeEquipmentPlannerData/);
@ -439,6 +476,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(textImport, /line\.indexOf\(":"\)/);
assert.match(textImport, /exportCalendarText/);
assert.match(textImport, /importCalendarText/);
assert.match(textImport, /exportGoalTreeText/);
assert.match(textImport, /importGoalTreeText/);
assert.match(textImport, /CALENDAR_WEEKDAYS/);
assert.match(imageViewer, /image-viewer-media/);
assert.match(imageViewer, /image-viewer-image-frame/);

View file

@ -3,8 +3,9 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
import { exportModuleText, importModuleText, parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
import { calculateGoalTreeFormula, evaluateGoalTreeNodes } from "../website/src/features/toolboxes/modules/goalTreeUtils.js";
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
function createTextImportContext() {
@ -20,6 +21,7 @@ function createTextImportContext() {
normalizeTableData,
normalizeTimerData,
normalizeTaskPlannerData,
normalizeGoalTreeData,
normalizeEquipmentPlannerData,
normalizeImageAnnotationData,
normalizeUrl
@ -118,6 +120,22 @@ test("text exchange imports table TSV and calculator hierarchy", () => {
assert.equal(calculatorImport.data.entries[1].formula, "base*5");
});
test("goal tree text exchange imports formulas and hierarchy", () => {
const context = createTextImportContext();
const imported = importModuleText("goalTree", "- Breuvage de rage x10\n - Élixir de destruction = base * 3\n - Poudre de flamme x5\n - Sève de cèdre x7\n - Catalyseur magique x10", context);
assert.equal(imported.ok, true);
assert.equal(imported.data.nodes.length, 5);
assert.equal(imported.data.nodes[0].kind, "step");
assert.equal(imported.data.nodes[1].parentId, imported.data.nodes[0].id);
assert.equal(imported.data.nodes[1].formula, "base * 3");
assert.equal(imported.data.nodes[2].kind, "step");
assert.equal(imported.data.nodes[2].qtyCurrent, 0);
const exported = exportModuleText("goalTree", imported.data);
assert.match(exported, /Breuvage de rage x10/);
assert.match(exported, /Élixir de destruction = base \* 3/);
});
test("text exchange round trips combos, tasks, equipment, images and timers", () => {
const context = createTextImportContext();
const combos = normalizeCombosData({
@ -910,3 +928,107 @@ test("task planner storage compacts defaults and export remaps task relations",
assert.equal(exportedPlanner.relations[0].toTaskId, "a2");
assert.equal(exportedPlanner.relations[0].prerequisite, true);
});
test("goal tree data normalizes, compacts, calculates and remaps nodes", () => {
assert.equal(calculateGoalTreeFormula("base * 3", 10), 30);
assert.equal(calculateGoalTreeFormula("base / 2", 10), 5);
assert.equal(calculateGoalTreeFormula("base - 12", 10, { clamp: false }), -2);
assert.equal(calculateGoalTreeFormula("base,5", 10), null);
assert.equal(calculateGoalTreeFormula("alert(1)", 10), null);
const normalized = normalizeGoalTreeData({
orientation: "vertical",
nodes: [
{ id: "root", title: "Root", kind: "step", icon: "fire", color: "cyan", qtyTarget: 10, qtyCurrent: 1 },
{ id: "child", parentId: "root", title: "Child", formula: "base*3", qtyCurrent: 15, resetType: "daily", resetTime: "09:30", checkedAt: 99 },
{ id: "bad", parentId: "missing", title: "Bad", kind: "weird", resetType: "later" },
{ id: "list", title: "List", kind: "checklist", items: [{ id: "i1", label: "Done", qtyCurrent: 1 }, { id: "i2", label: "Todo", formula: "5", qtyCurrent: 2 }, { id: "i3", label: "" }] },
{ id: "formulaList", parentId: "root", parentIds: ["root", "list"], title: "Formula list", kind: "checklist", items: [{ id: "i4", label: "Triple", formula: "base * 3", qtyCurrent: 30 }, { id: "i5", label: "Delta", formula: "base - 12" }] }
]
});
assert.equal(normalized.orientation, "vertical");
assert.equal(normalized.nodes[2].parentId, "");
assert.equal(normalized.nodes[2].kind, "step");
assert.equal(normalized.nodes[2].resetType, "none");
assert.equal(normalized.nodes[1].resetTime, "09:30");
assert.deepEqual(normalized.nodes[4].parentIds, ["root", "list"]);
const evaluated = evaluateGoalTreeNodes(normalized.nodes);
assert.equal(evaluated[1].computedQtyTarget, 30);
assert.equal(evaluated[1].progress.ratio, 0.5);
assert.equal(evaluated[0].progress.current, 45);
assert.equal(evaluated[0].progress.target, 61);
assert.equal(evaluated[3].progress.current, 3);
assert.equal(evaluated[3].progress.target, 7);
assert.equal(evaluated[4].items[0].qtyTarget, 30);
assert.equal(evaluated[4].items[1].qtyTarget, 1);
assert.equal(evaluated[4].progress.current, 30);
const compact = compactModuleDataForStorage("goalTree", normalized);
assert.equal(compact.settings, undefined);
assert.equal(compact.orientation, "vertical");
assert.equal(compact.nodes[0].kind, undefined);
assert.equal(compact.nodes[0].icon, "fire");
assert.equal(compact.nodes[0].color, "cyan");
assert.equal(compact.nodes[0].qtyCurrent, 1);
assert.equal(compact.nodes[1].formula, "base*3");
assert.equal(compact.nodes[1].resetType, "daily");
assert.equal(compact.nodes[1].resetTime, "09:30");
assert.deepEqual(compact.nodes[3].items, [{ id: "i1", label: "Done", qtyCurrent: 1 }, { id: "i2", label: "Todo", formula: "5", qtyCurrent: 2 }, { id: "i3", label: "" }]);
assert.deepEqual(compact.nodes[4].parentIds, ["root", "list"]);
assert.deepEqual(compact.nodes[4].items, [{ id: "i4", label: "Triple", formula: "base * 3", qtyCurrent: 30 }, { id: "i5", label: "Delta", formula: "base - 12" }]);
const exported = createToolboxExportPayload(
{ id: "toolbox1", name: "Goals", modules: [{ id: "module1", type: "goalTree" }], updatedAt: "2026-01-01T00:00:00.000Z" },
{ "toolbox1:module1": compact }
);
const exportedTree = exported.modules.m1;
assert.deepEqual(exportedTree.nodes.map((node) => node.id), ["g1", "g2", "g3", "g4", "g5"]);
assert.equal(exportedTree.nodes[1].parentId, "g1");
assert.deepEqual(exportedTree.nodes[4].parentIds, ["g1", "g4"]);
assert.deepEqual(exportedTree.nodes[3].items.map((item) => item.id), ["g6", "g7", "g8"]);
assert.deepEqual(exportedTree.nodes[4].items.map((item) => item.id), ["g9", "ga"]);
});
test("goal tree parent progress uses base-scaled child bottlenecks", () => {
const normalized = normalizeGoalTreeData({
nodes: [
{ id: "root", title: "Potion de vie", kind: "step", qtyTarget: 10 },
{
id: "ingredients",
parentId: "root",
title: "Ingrédients",
kind: "checklist",
items: [
{ id: "herb", label: "Herbe magique", formula: "base", qtyCurrent: 7 },
{ id: "water", label: "Eau rajeunissante", formula: "base", qtyCurrent: 5 }
]
},
{ id: "manual", title: "Objectif manuel", kind: "step", qtyTarget: 10, qtyCurrent: 3, progressMode: "manual" },
{ id: "manualChild", parentId: "manual", title: "Enfant ignoré", kind: "step", formula: "base", qtyCurrent: 10 },
{ id: "defaultManual", title: "Validation manuelle par défaut", kind: "step", qtyTarget: 1, qtyCurrent: 1 },
{ id: "defaultManualChild", parentId: "defaultManual", title: "Validation enfant", kind: "step", qtyTarget: 1, qtyCurrent: 0 },
{ id: "forcedAuto", title: "Validation auto forcée", kind: "step", qtyTarget: 1, qtyCurrent: 1, progressMode: "auto" },
{ id: "forcedAutoChild", parentId: "forcedAuto", title: "Validation enfant auto", kind: "step", qtyTarget: 1, qtyCurrent: 0 }
]
});
const evaluated = evaluateGoalTreeNodes(normalized.nodes);
assert.equal(evaluated[0].progress.current, 5);
assert.equal(evaluated[0].progress.target, 10);
assert.equal(evaluated[0].progress.ratio, 0.5);
assert.equal(evaluated[1].progress.current, 12);
assert.equal(evaluated[1].progress.target, 20);
assert.equal(evaluated[2].progress.current, 3);
assert.equal(evaluated[2].progress.target, 10);
assert.equal(evaluated[4].progress.current, 1);
assert.equal(evaluated[4].progress.target, 1);
assert.equal(evaluated[6].progress.current, 0);
assert.equal(evaluated[6].progress.target, 1);
const compact = compactModuleDataForStorage("goalTree", normalized);
assert.equal(compact.nodes[0].progressMode, undefined);
assert.equal(compact.nodes[2].progressMode, "manual");
assert.equal(compact.nodes[4].progressMode, undefined);
assert.equal(compact.nodes[6].progressMode, "auto");
});