leistungsbilanz-ts/tests/linear-xlsx-external-model-migration.test.ts

115 lines
6 KiB
TypeScript

import assert from "node:assert/strict";
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { it } from "node:test";
import { spawnSync } from "node:child_process";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { createDatabaseContext } from "../src/db/database-context.js";
it("replaces legacy CSV state and resets linked row quantities to their manual share", () => {
const source = path.resolve("src", "db", "migrations");
const staging = mkdtempSync(path.join(tmpdir(), "leistungsbilanz-linear-xlsx-migration-"));
const journal = JSON.parse(readFileSync(path.join(source, "meta", "_journal.json"), "utf8"));
journal.entries = journal.entries.filter((entry: { idx: number }) => entry.idx <= 13);
mkdirSync(path.join(staging, "meta"));
writeFileSync(path.join(staging, "meta", "_journal.json"), JSON.stringify(journal));
for (const entry of journal.entries) {
copyFileSync(path.join(source, `${entry.tag}.sql`), path.join(staging, `${entry.tag}.sql`));
}
const context = createDatabaseContext(":memory:");
try {
migrate(context.db, { migrationsFolder: staging });
context.sqlite.exec(`
INSERT INTO projects (id, name) VALUES ('p', 'Projekt');
INSERT INTO distribution_boards (id, project_id, name) VALUES ('b', 'p', 'UV');
INSERT INTO circuit_lists (id, project_id, distribution_board_id, name) VALUES ('l', 'p', 'b', 'Liste');
INSERT INTO circuit_sections (id, circuit_list_id, key, display_name, prefix) VALUES ('s', 'l', 'single_phase', 'Einphasig', '-2F1.');
INSERT INTO circuits (id, circuit_list_id, section_id, equipment_identifier) VALUES ('c', 'l', 's', '-2F1.1');
INSERT INTO circuit_device_rows
(id, circuit_id, name, display_name, quantity, manual_quantity, power_per_unit, simultaneity_factor)
VALUES ('r', 'c', 'Steckdose', 'Steckdose', 5, 2, 0.2, 1);
INSERT INTO external_csv_configurations
(id, project_id, configuration_version, configuration)
VALUES ('cfg', 'p', 3, '{}');
INSERT INTO external_model_sources (id, project_id, name, source_type)
VALUES ('src', 'p', 'Revit', 'revit_csv');
INSERT INTO external_import_batches
(id, project_id, source_id, import_kind, imported_at_iso, file_name, sha256,
applied_project_revision, configuration_snapshot, original_bytes, document, configuration_version)
VALUES ('batch', 'p', 'src', 'initial', '2026-01-01T00:00:00.000Z', 'legacy.csv',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 0, '{}', X'00', '{}', 3);
INSERT INTO external_model_objects
(id, project_id, source_id, ifc_guid, last_seen_import_batch_id,
last_accepted_import_batch_id, accepted_source_values, planning_values,
overridden_fields, circuit_device_row_id)
VALUES ('object', 'p', 'src', 'Ifc-GUID', 'batch', 'batch', '{}', '{}', '[]', 'r');
`);
migrate(context.db, { migrationsFolder: source });
assert.equal(
(context.sqlite.prepare("SELECT quantity FROM circuit_device_rows WHERE id = 'r'").get() as { quantity: number }).quantity,
2
);
assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM external_model_objects").get()!.count, 0);
assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM external_model_configurations").get()!.count, 0);
assert.throws(() => context.sqlite.prepare("SELECT * FROM external_csv_configurations").all(), /no such table/);
const columns = context.sqlite.prepare("PRAGMA table_info(external_model_objects)").all() as Array<{ name: string; notnull: number }>;
assert.equal(columns.find((column) => column.name === "worksharing_id")?.notnull, 1);
assert.equal(columns.find((column) => column.name === "ifc_guid")?.notnull, 0);
assert.deepEqual(context.sqlite.pragma("foreign_key_check"), []);
assert.equal(context.sqlite.pragma("integrity_check", { simple: true }), "ok");
} finally {
context.close();
rmSync(staging, { recursive: true, force: true });
}
});
it("creates and verifies a backup before the Linear-XLSX migration", () => {
const source = path.resolve("src", "db", "migrations");
const root = mkdtempSync(path.join(tmpdir(), "leistungsbilanz-linear-xlsx-backup-"));
const staging = path.join(root, "migrations");
const dataDirectory = path.join(root, "data");
mkdirSync(path.join(staging, "meta"), { recursive: true });
mkdirSync(dataDirectory, { recursive: true });
const journal = JSON.parse(readFileSync(path.join(source, "meta", "_journal.json"), "utf8"));
journal.entries = journal.entries.filter((entry: { idx: number }) => entry.idx <= 13);
writeFileSync(path.join(staging, "meta", "_journal.json"), JSON.stringify(journal));
for (const entry of journal.entries) {
copyFileSync(path.join(source, `${entry.tag}.sql`), path.join(staging, `${entry.tag}.sql`));
}
const databasePath = path.join(dataDirectory, "leistungsbilanz.db");
const context = createDatabaseContext(databasePath);
try {
migrate(context.db, { migrationsFolder: staging });
} finally {
context.close();
}
try {
const result = spawnSync(process.execPath, ["scripts/run-migrations.js"], {
cwd: path.resolve("."),
env: { ...process.env, LEISTUNGSBILANZ_DATA_DIR: dataDirectory },
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Verified pre-migration backup created/);
const backups = readdirSync(path.join(dataDirectory, "backups"));
assert.equal(backups.length, 1);
const migrated = createDatabaseContext(databasePath);
try {
assert.ok(migrated.sqlite.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'external_model_configurations'"
).get());
} finally {
migrated.close();
}
} finally {
rmSync(root, { recursive: true, force: true });
}
});