Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 205 additions & 168 deletions packages/db/tests/database-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,194 +19,231 @@
} from "./helpers/database-script-helpers";

const EMBEDDED_POSTGRES_TEST_TIMEOUT_MS = 20_000;
const TEST_HOME = "/tmp/devos-home";
const INSTANCE_CONFIG_PATH = path.join(
TEST_HOME,
".devos",
"config",
"instance.config.json",
);

describe("database scripts", () => {
it(
"runs migrations for a fresh server database",
() =>
withDatabasePath(async (dbPath) => {
const port = await createDatabasePort();

const result = await migrateDatabase({ dbPath, port });

expect(result.dbPath).toBe(dbPath);
expect(result.port).toBe(port);
const database = await initializeServerDatabase(dbPath, {
port,
runMigrations: false,
});
try {
const migrations = await database.client.query<{ id: string }>(
"SELECT id FROM schema_migrations ORDER BY id",
);
expect(migrations.rows.length).toBeGreaterThan(0);
expect(migrations.rows.at(-1)?.id).toBe(
"0018_token_usage_cost_metadata",
);
const columns = await database.client.query<{ column_name: string }>(
`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'chat_sessions'
AND column_name IN ('task_id', 'archived')
`,
describe("migrate", () => {
it(
"runs migrations for a fresh server database",
() =>
withDatabasePath(async (dbPath) => {
const port = await createDatabasePort();

const result = await migrateDatabase({ dbPath, port });

expect(result).toEqual({ dbPath, port });
const database = await initializeServerDatabase(dbPath, {
port,
runMigrations: false,
});
try {
const migrations = await database.client.query<{ id: string }>(
"SELECT id FROM schema_migrations ORDER BY id",
);
expect(migrations.rows.map((row) => row.id)).toContain(
"0018_token_usage_cost_metadata",
);
const columns = await database.client.query<{
column_name: string;
}>(
`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'chat_sessions'
AND column_name IN ('task_id', 'archived')
`,
);
expect(columns.rows.map((row) => row.column_name).sort()).toEqual([
"archived",
"task_id",
]);
} finally {
await database.close();
}
}),
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
);
});

describe("seed", () => {
it(
"seeds deterministic dev data idempotently",
() =>
withDatabasePath(async (dbPath) => {
const port = await createDatabasePort();

await seedDatabase({ dbPath, port });
await seedDatabase({ dbPath, port });

const database = await initializeServerDatabase(dbPath, { port });
try {
const boards = await database.db.select().from(projectBoardsTable);
expect(
boards.filter((board) => board.id === "dev-board"),
).toHaveLength(1);
} finally {
await database.close();
}
}),
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
);
});

describe("backup", () => {
it(
"copies a timestamped backup without mutating the source",
() =>
withDatabasePath(async (dbPath) => {
await migrateDatabase({ dbPath, port: await createDatabasePort() });

const result = await backupDatabase({
dbPath,
now: new Date("2026-05-20T01:02:03.004Z"),
});

expect(result.sourcePath).toBe(dbPath);
expect(result.backupPath).toBe(
`${dbPath}.backup-20260520T010203004Z`,
);
expect(columns.rows).toHaveLength(2);
} finally {
await database.close();
}
}),
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
);

it(
"seeds deterministic dev data idempotently",
() =>
withDatabasePath(async (dbPath) => {
const port = await createDatabasePort();

await seedDatabase({ dbPath, port });
await seedDatabase({ dbPath, port });

const database = await initializeServerDatabase(dbPath, { port });
try {
const boards = await database.db.select().from(projectBoardsTable);
expect(
boards.filter((board) => board.id === "dev-board"),
).toHaveLength(1);
} finally {
await database.close();
}
}),
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
);

it(
"copies a timestamped backup without mutating the source",
() =>
withDatabasePath(async (dbPath) => {
await migrateDatabase({ dbPath, port: await createDatabasePort() });

const result = await backupDatabase({
dbPath,
now: new Date("2026-05-20T01:02:03.004Z"),
});

expect(result.sourcePath).toBe(dbPath);
expect(result.backupPath).toBe(`${dbPath}.backup-20260520T010203004Z`);
expect((await stat(dbPath)).isDirectory()).toBe(true);
expect((await stat(result.backupPath)).isDirectory()).toBe(true);
await expect(readMigrationCount(dbPath)).resolves.toBeGreaterThan(0);
await expect(
readMigrationCount(result.backupPath),
).resolves.toBeGreaterThan(0);
}),
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
);

it("refuses to copy a live embedded PostgreSQL cluster", async () => {
await withDatabasePath(async (dbPath) => {
await mkdir(dbPath, { recursive: true });
await writeFile(path.join(dbPath, "postmaster.pid"), String(process.pid));
await expect(backupDatabase({ dbPath })).rejects.toThrow(
"Refusing to back up live embedded PostgreSQL cluster",
);
await expectDirectory(dbPath);
await expectDirectory(result.backupPath);
await expect(readMigrationCount(dbPath)).resolves.toBeGreaterThan(0);
await expect(
readMigrationCount(result.backupPath),
).resolves.toBeGreaterThan(0);

Check failure on line 119 in packages/db/tests/database-scripts.test.ts

View workflow job for this annotation

GitHub Actions / Check, typecheck, and test

error:

Expected promise that resolves Received promise that rejected: Promise { <rejected> } at <anonymous> (/home/runner/work/devos.ing/devos.ing/packages/db/tests/database-scripts.test.ts:119:17) at async withDatabasePath (/home/runner/work/devos.ing/devos.ing/packages/db/tests/helpers/database-script-helpers.ts:12:16)

Check failure on line 119 in packages/db/tests/database-scripts.test.ts

View workflow job for this annotation

GitHub Actions / Check, typecheck, and test

error:

Expected promise that resolves Received promise that rejected: Promise { <rejected> } at <anonymous> (/home/runner/work/devos.ing/devos.ing/packages/db/tests/database-scripts.test.ts:119:17) at async withDatabasePath (/home/runner/work/devos.ing/devos.ing/packages/db/tests/helpers/database-script-helpers.ts:12:16)
}),
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
);

it("refuses to copy a live embedded PostgreSQL cluster", async () => {
await withDatabasePath(async (dbPath) => {
await mkdir(dbPath, { recursive: true });
await writeFile(
path.join(dbPath, "postmaster.pid"),
String(process.pid),
);

await expect(backupDatabase({ dbPath })).rejects.toThrow(
"Refusing to back up live embedded PostgreSQL cluster",
);
});
});
});

it("resolves database paths from explicit, env, instance, then repo fallback", async () => {
let readCalls = 0;
const readText = async (targetPath: string) => {
readCalls += 1;
expect(targetPath).toBe(
path.join(
"/tmp/devos-home",
".devos",
"config",
"instance.config.json",
),
);
return JSON.stringify({
describe("database config resolution", () => {
it("prefers an explicit database path without reading instance config", async () => {
await expect(
resolveDatabasePath("/tmp/explicit-db", {
env: {
HOME: TEST_HOME,
PIV_SERVER_DATABASE_PATH: "/tmp/env-db",
},
readText: failIfConfigRead,
}),
).resolves.toBe("/tmp/explicit-db");
});

it("uses the environment database path before instance config", async () => {
await expect(
resolveDatabasePath(undefined, {
env: {
HOME: TEST_HOME,
PIV_SERVER_DATABASE_PATH: "/tmp/env-db",
},
readText: failIfConfigRead,
}),
).resolves.toBe("/tmp/env-db");
});

it("uses the instance database path and port", async () => {
const readText = createInstanceConfigReader({
database: {
embeddedPostgresDataDir: "/tmp/instance-db",
embeddedPostgresPort: 54330,
},
});
};

await expect(
resolveDatabasePath("/tmp/explicit-db", {
env: {
HOME: "/tmp/devos-home",
PIV_SERVER_DATABASE_PATH: "/tmp/env-db",
},
readText,
}),
).resolves.toBe("/tmp/explicit-db");
await expect(
resolveDatabasePath(undefined, {
env: {
HOME: "/tmp/devos-home",
PIV_SERVER_DATABASE_PATH: "/tmp/env-db",
},
readText,
}),
).resolves.toBe("/tmp/env-db");
expect(readCalls).toBe(0);
await expect(
resolveDatabasePath(undefined, {
env: { HOME: "/tmp/devos-home" },
readText,
}),
).resolves.toBe("/tmp/instance-db");
await expect(
resolveDatabaseConfig(undefined, {
env: { HOME: "/tmp/devos-home" },
readText,
}),
).resolves.toEqual({
dbPath: "/tmp/instance-db",
port: 54330,
await expect(
resolveDatabasePath(undefined, {
env: { HOME: TEST_HOME },
readText,
}),
).resolves.toBe("/tmp/instance-db");
await expect(
resolveDatabaseConfig(undefined, {
env: { HOME: TEST_HOME },
readText,
}),
).resolves.toEqual({
dbPath: "/tmp/instance-db",
port: 54330,
});
});
await expect(
resolveDatabasePath(undefined, {
env: { HOME: "/tmp/devos-home" },
readText: async () => {
throw new Error("missing");
},
}),
).resolves.toBe(
path.resolve(
import.meta.dir,
"..",
"..",
"..",
".devos",
"config",
"server-db",
),
);
});

it("rejects invalid instance embedded PostgreSQL ports", async () => {
await expect(
resolveDatabaseConfig(undefined, {
env: { HOME: "/tmp/devos-home" },
readText: async () =>
JSON.stringify({
it("falls back to the repo database path when instance config is missing", async () => {
await expect(
resolveDatabasePath(undefined, {
env: { HOME: TEST_HOME },
readText: async () => {
throw new Error("missing");
},
}),
).resolves.toBe(
path.resolve(
import.meta.dir,
"..",
"..",
"..",
".devos",
"config",
"server-db",
),
);
});

it("rejects invalid instance embedded PostgreSQL ports", async () => {
await expect(
resolveDatabaseConfig(undefined, {
env: { HOME: TEST_HOME },
readText: createInstanceConfigReader({
database: {
embeddedPostgresDataDir: "/tmp/instance-db",
embeddedPostgresPort: 0,
},
}),
}),
).rejects.toThrow("embeddedPostgresPort must be a positive integer port");
}),
).rejects.toThrow("embeddedPostgresPort must be a positive integer port");
});
});

it("keeps schema package exports available", () => {
expect(boardTasksTable).toBeDefined();
expect(boardTasksTable.id).toBeDefined();
expect(boardProjectsTable.repoName).toBeDefined();
expect(chatSessionsTable.taskId).toBeDefined();
describe("schema package exports", () => {
it("keeps schema package exports available", () => {
expect(boardTasksTable).toBeDefined();
expect(boardTasksTable.id).toBeDefined();
expect(boardProjectsTable.repoName).toBeDefined();
expect(chatSessionsTable.taskId).toBeDefined();
});
});
});

async function expectDirectory(directoryPath: string): Promise<void> {
expect((await stat(directoryPath)).isDirectory()).toBe(true);
}

async function failIfConfigRead(): Promise<string> {
throw new Error("instance config should not be read");
}

function createInstanceConfigReader(content: unknown) {
return async (targetPath: string, encoding: BufferEncoding) => {
expect(targetPath).toBe(INSTANCE_CONFIG_PATH);
expect(encoding).toBe("utf8");
return JSON.stringify(content);
};
}
Loading