Something went wrong. Try again.
A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176import { describe, expect, test } from "bun:test";import { fileURLToPath } from "node:url";
import { CsvWriter } from "../src/csv/CsvWriter.ts";import { ResourceType } from "../src/model/types.ts";import { makeMinimalProject, parseCsv, fixtureExists } from "./helpers.ts";
const FIXTURE_MPP_PATH = resolveFixturePath("./sample-schedule.mpp");const FIXTURE_CSV_PATH = resolveFixturePath("./project_schedule.csv");const HAS_MPP_FIXTURE = fixtureExists(FIXTURE_MPP_PATH);
async function getMppReader() { return import("../src/mpp/MppReader.ts");}
describe("CsvWriter", () => { test.skipIf(!HAS_MPP_FIXTURE)( "produces CSV matching the fixture baseline for leaf tasks", async () => { const { MppReader } = await getMppReader(); const data = await Bun.file(FIXTURE_MPP_PATH).arrayBuffer(); const project = new MppReader().read(data);
const writer = new CsvWriter(); const csv = writer.write(project); const baselineCsv = await Bun.file(FIXTURE_CSV_PATH).text();
const csvRows = parseCsv(csv); const baselineRows = parseCsv(baselineCsv);
expect(csvRows).toHaveLength(baselineRows.length);
for (const [index, row] of csvRows.entries()) { const baseline = baselineRows[index]!; expect(row["ID"]).toBe(baseline["ID"]); expect(row["Task Name"]).toBe(baseline["Task Name"]); expect(row["WBS"]).toBe(baseline["WBS"]); expect(formatMinuteDate(row["Start"])).toBe( formatMinuteDate(baseline["Start"]), ); expect(formatMinuteDate(row["Finish"])).toBe( formatMinuteDate(baseline["Finish"]), ); expect(row["Duration"]).toBe(baseline["Duration"]); expect(row["Critical"]).toBe(baseline["Critical"]); expect(row["Milestone"]).toBe(baseline["Milestone"]); } }, );
test.skipIf(!HAS_MPP_FIXTURE)( "includes summary tasks when option is set", async () => { const { MppReader } = await getMppReader(); const data = await Bun.file(FIXTURE_MPP_PATH).arrayBuffer(); const project = new MppReader().read(data);
const leafCsv = new CsvWriter().write(project); const allCsv = new CsvWriter().write(project, { includeSummaryTasks: true, });
const leafRows = parseCsv(leafCsv); const allRows = parseCsv(allCsv);
expect(allRows.length).toBeGreaterThan(leafRows.length); }, );
test("excludes Resources column when option is set", () => { const project = makeMinimalProject(); const csv = new CsvWriter().write(project, { includeResources: false }); const header = csv.split("\n")[0]!;
expect(header).not.toContain("Resources"); expect(header).toContain("Milestone"); });
test("escapes fields containing commas, quotes, and newlines", () => { const project = makeMinimalProject(); project.tasks[0]!.name = 'Task with "quotes" and, commas';
const csv = new CsvWriter().write(project); expect(csv).toContain('"Task with ""quotes"" and, commas"'); });
test("prefixes injection characters with apostrophe", () => { const project = makeMinimalProject();
const injectionInputs = ["=SUM(A1)", "+1", "-1", "@SUM(A1:A10)"];
for (const input of injectionInputs) { project.tasks[0]!.name = input; const csv = new CsvWriter().write(project); const dataLine = csv.split("\n")[1]!; // Apostrophe prefix should appear; original value must not appear without it expect(dataLine).toContain("'" + input); } });
test("applies both injection prefix and quote escaping together", () => { const project = makeMinimalProject(); project.tasks[0]!.name = '=cmd,with "quotes"';
const csv = new CsvWriter().write(project); // Should get apostrophe prefix AND quote wrapping: "'=cmd,with ""quotes""" expect(csv).toContain('"\'=cmd,with ""quotes"""'); });
test("resolves resource names from assignments", () => { const project = makeMinimalProject(); project.resources = [ { id: 1, uniqueId: 100, name: "Alice", type: ResourceType.Work, email: null, group: null, maxUnits: null, cost: null, work: null, resourcePool: null, }, { id: 2, uniqueId: 200, name: "Bob", type: ResourceType.Work, email: null, group: null, maxUnits: null, cost: null, work: null, resourcePool: null, }, ]; project.assignments = [ { taskUniqueId: 1, resourceUniqueId: 100, work: null, units: null, start: null, finish: null, actualWork: null, remainingWork: null, }, { taskUniqueId: 1, resourceUniqueId: 200, work: null, units: null, start: null, finish: null, actualWork: null, remainingWork: null, }, ];
const csv = new CsvWriter().write(project); const rows = parseCsv(csv); expect(rows[0]!["Resources"]).toContain("Alice"); expect(rows[0]!["Resources"]).toContain("Bob"); });});
function formatMinuteDate(value: string | null | undefined): string | null { if (!value) return null; return value.slice(0, 16);}
function resolveFixturePath(relativePath: string): string { return fileURLToPath(new URL(relativePath, import.meta.url));}