## Summary - make SQLite-to-PostgreSQL cutover retryable, fail-closed, versioned, and transactionally serialized - isolate migration sessions from runtime traffic and apply schema upgrades through `0002` - enforce tenant ownership across automations, analytics, activity, usage, agent runs, evals, and todos - replace expired SQLite-only coverage with PostgreSQL parity and concurrency coverage This is PR 1 of 2. The stacked follow-up restores PostgreSQL parity for CLI, engine, dashboard, and bundled integrations. ## Verification - `pnpm check:changesets --strict` - `pnpm --filter @fusion/core typecheck` - migration schema, connection, and SQLite cutover suite: 57 tests passed - `pnpm test:gate`: 463 tests passed ## Post-Deploy Monitoring & Validation - take a restorable PostgreSQL backup before deploy - confirm `fusion_schema_migrations` contains `0002` - confirm each expected project has a complete `fusion_sqlite_migrations` row - verify no null or empty tenant ownership in automations, activity logs, agent runs, and usage events - monitor for ownership inference failures, cutover verification failures, and migration session errors - restore the backup for data rollback; do not downgrade the tenant-isolation schema in place <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * PostgreSQL-backed analytics and live dashboard metrics are now project-scoped (activity, tools, monitor, signals, and live snapshots). * Evaluation runs and scheduled eval batches received lifecycle improvements (ordering, updates, and execution flow). * Todo list changes now emit events; WhatsApp persistence and project-scoped roadmap data are supported. * **Bug Fixes** * SQLite-to-PostgreSQL cutovers now fail safely with stronger verification, serialized cutover handling, and safer project ownership. * PostgreSQL backend writes and reads are now strictly project-isolated and fail closed when project context is missing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
46 lines
1.7 KiB
JavaScript
46 lines
1.7 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const checkerModule = await import(["..", "/check-no-no", "hup", ".mjs"].join(""));
|
|
const { formatFailureMessage, scanFileContent, scanTrackedFiles } = checkerModule;
|
|
const bannedToken = ["no", "hup"].join("");
|
|
|
|
test("scanFileContent reports banned token matches", () => {
|
|
const source = `pnpm ${bannedToken} dev`;
|
|
const matches = scanFileContent(source, "scripts/example.mjs");
|
|
assert.equal(matches.length, 1);
|
|
assert.equal(matches[0].lineNumber, 1);
|
|
assert.match(matches[0].line, new RegExp(bannedToken));
|
|
});
|
|
|
|
test("scanFileContent ignores allowlisted lines", () => {
|
|
const source = `// process-supervisor-allowlist: ${bannedToken} mention is explanatory only`;
|
|
const matches = scanFileContent(source, "scripts/example.mjs");
|
|
assert.equal(matches.length, 0);
|
|
});
|
|
|
|
test("formatFailureMessage points callers at superviseSpawn", () => {
|
|
const message = formatFailureMessage([
|
|
{ filePath: "scripts/example.mjs", lineNumber: 3, line: `pnpm ${bannedToken} dev` },
|
|
]);
|
|
assert.match(message, /superviseSpawn/);
|
|
assert.match(message, /scripts\/example\.mjs:3/);
|
|
});
|
|
|
|
test("scanTrackedFiles skips only tracked files missing from the working tree", () => {
|
|
const missing = Object.assign(new Error("missing"), { code: "ENOENT" });
|
|
const readFile = () => { throw missing; };
|
|
|
|
assert.deepEqual(scanTrackedFiles(["scripts/deleted.mjs"], readFile), []);
|
|
});
|
|
|
|
test("scanTrackedFiles rethrows tracked-file read failures other than ENOENT", () => {
|
|
const denied = Object.assign(new Error("denied"), { code: "EACCES" });
|
|
const readFile = () => { throw denied; };
|
|
|
|
assert.throws(
|
|
() => scanTrackedFiles(["scripts/unreadable.mjs"], readFile),
|
|
(error) => error === denied,
|
|
);
|
|
});
|