forked from jappel/leistungsbilanz-ts
tsx runs tests without type checking and tsconfig.json covers only src, so 71 type errors had accumulated unnoticed. tsconfig.tests.json now checks every test and helper, and npm run typecheck includes it. Fixtures gain the fields the DTOs have grown since they were written (manualQuantity, circuit apparent power/cos phi, downstream board loads, section totals, building, cover and board info fields). Repository lookups that return "row ?? null" now declare the null in their type, which their callers already handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
6 KiB
TypeScript
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<[], { count: number }>("SELECT count(*) AS count FROM external_model_objects").get()!.count, 0);
|
|
assert.equal(context.sqlite.prepare<[], { count: number }>("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 });
|
|
}
|
|
});
|