From 513fffcb1aa09a95a6a3b798809bbd7e88c4c90e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 21:14:21 +0200 Subject: [PATCH 01/18] ci(dev): escape test filter --- .github/workflows/dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1076208a39..640e16c655 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -44,7 +44,7 @@ jobs: run: pnpm run --filter=server test - name: Run the rest of the tests - run: pnpm run --filter=!client --filter=!server test + run: pnpm run --filter=\!client --filter=\!server test build_docker: name: Build Docker image From a437169ad5c284e107c6ed241092bc7fb856d8e7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 21:20:12 +0200 Subject: [PATCH 02/18] test(server): increase hook timeout --- apps/server/vite.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index 8211ba8479..2b5fbdcb40 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -19,7 +19,7 @@ export default defineConfig(() => ({ exclude: [ "spec/build-checks/**", ], - hookTimeout: 20000, + hookTimeout: 40_000, reporters: [ "verbose" ], From 5afab6938ac34944bf9e87d6a4412417c9dfdd75 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 21:54:30 +0200 Subject: [PATCH 03/18] test(server): reduce max workers to 1 --- apps/server/vite.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index 2b5fbdcb40..bb3be97077 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -29,6 +29,6 @@ export default defineConfig(() => ({ reporter: [ "text", "html" ] }, pool: "vmForks", - maxWorkers: 3 + maxWorkers: 1 }, })); From 85d8c4c8fab25aad8f678a392d335ff84786668d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 22:06:46 +0200 Subject: [PATCH 04/18] feat(ci/server): HTML output --- .github/workflows/dev.yml | 8 ++++++++ apps/server/vite.config.mts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 640e16c655..f27a16923b 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -43,6 +43,14 @@ jobs: - name: Run the server-side tests run: pnpm run --filter=server test + - name: Upload server test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: server-test-report + path: apps/server/test-output/vitest/html/ + retention-days: 30 + - name: Run the rest of the tests run: pnpm run --filter=\!client --filter=\!server test diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index bb3be97077..ec98733acd 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -21,7 +21,8 @@ export default defineConfig(() => ({ ], hookTimeout: 40_000, reporters: [ - "verbose" + "verbose", + ["html", { outputFile: "./test-output/vitest/html/index.html" }] ], coverage: { reportsDirectory: './test-output/vitest/coverage', From 9b4f8c50033cd7548646821996bb72bed3227609 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 22:07:11 +0200 Subject: [PATCH 05/18] feat(ci/client): HTML output --- .github/workflows/dev.yml | 8 ++++++++ apps/client/vite.config.mts | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index f27a16923b..5a281f5bf2 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -40,6 +40,14 @@ jobs: - name: Run the client-side tests run: pnpm run --filter=client test + - name: Upload client test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: client-test-report + path: apps/client/test-output/vitest/html/ + retention-days: 30 + - name: Run the server-side tests run: pnpm run --filter=server test diff --git a/apps/client/vite.config.mts b/apps/client/vite.config.mts index 716ec8b912..29e4ee2080 100644 --- a/apps/client/vite.config.mts +++ b/apps/client/vite.config.mts @@ -120,7 +120,11 @@ export default defineConfig(() => ({ environment: "happy-dom", setupFiles: [ "./src/test/setup.ts" - ] + ], + reporters: [ + "verbose", + ["html", { outputFile: "./test-output/vitest/html/index.html" }] + ], }, commonjsOptions: { transformMixedEsModules: true, From db0c515bade7a9a87aa93a2f9c707d4ce4ae812e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 22:11:51 +0200 Subject: [PATCH 06/18] test(server): fake timers not restored --- apps/server/src/routes/login.spec.ts | 6 +++++- apps/server/src/share/routes.spec.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/server/src/routes/login.spec.ts b/apps/server/src/routes/login.spec.ts index 0f210bfdc1..e6c7131ed0 100644 --- a/apps/server/src/routes/login.spec.ts +++ b/apps/server/src/routes/login.spec.ts @@ -2,7 +2,7 @@ import { dayjs } from "@triliumnext/commons"; import type { Application } from "express"; import { SessionData } from "express-session"; import supertest, { type Response } from "supertest"; -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import cls from "../services/cls.js"; import { type SQLiteSessionStore } from "./session_parser.js"; @@ -20,6 +20,10 @@ describe("Login Route test", () => { ({ sessionStore, CLEAN_UP_INTERVAL } = (await import("./session_parser.js"))); }); + afterAll(() => { + vi.useRealTimers(); + }); + it("should return the login page, when using a GET request", async () => { // RegExp for login page specific string in HTML diff --git a/apps/server/src/share/routes.spec.ts b/apps/server/src/share/routes.spec.ts index a920a3620d..f4ce2c1736 100644 --- a/apps/server/src/share/routes.spec.ts +++ b/apps/server/src/share/routes.spec.ts @@ -1,6 +1,6 @@ import type { Application, NextFunction,Request, Response } from "express"; import supertest from "supertest"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { safeExtractMessageAndStackFromError } from "../services/utils.js"; @@ -23,6 +23,10 @@ describe("Share API test", () => { }); }); + afterAll(() => { + vi.useRealTimers(); + }); + beforeEach(() => { cannotSetHeadersCount = 0; }); From 02123988153d92a3dcb473789d0aaf27b7165401 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 22:14:34 +0200 Subject: [PATCH 07/18] test(server): reset ws module --- apps/server/src/services/ws.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/ws.spec.ts b/apps/server/src/services/ws.spec.ts index bd3b105aa2..4cea6a2f2d 100644 --- a/apps/server/src/services/ws.spec.ts +++ b/apps/server/src/services/ws.spec.ts @@ -56,8 +56,9 @@ describe('WebSocket Service', () => { let log: any; beforeEach(async () => { + vi.resetModules(); vi.clearAllMocks(); - + // Get mocked log log = (await import('./log.js')).default; From 3c22ab8c9c655f216740e2544465a5f948745550 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 22:17:19 +0200 Subject: [PATCH 08/18] refactor(server): don't set up session timer on module init --- apps/server/src/app.ts | 3 ++- apps/server/src/routes/session_parser.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index d2d830ee06..4c6e97dfae 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -86,8 +86,9 @@ export default async function buildApp() { app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt"))); app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png"))); - const sessionParser = (await import("./routes/session_parser.js")).default; + const { default: sessionParser, startSessionCleanup } = await import("./routes/session_parser.js"); app.use(sessionParser); + startSessionCleanup(); app.use(favicon(path.join(assetsDir, isDev ? "icon-dev.ico" : "icon.ico"))); if (openID.isOpenIDEnabled()) diff --git a/apps/server/src/routes/session_parser.ts b/apps/server/src/routes/session_parser.ts index b630b09055..882701ab54 100644 --- a/apps/server/src/routes/session_parser.ts +++ b/apps/server/src/routes/session_parser.ts @@ -113,11 +113,13 @@ const sessionParser: express.RequestHandler = session({ store: sessionStore }); -setInterval(() => { - // Clean up expired sesions. - const now = Date.now(); - const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now); - console.log("Cleaning up expired sessions: ", result.changes); -}, CLEAN_UP_INTERVAL); +export function startSessionCleanup() { + setInterval(() => { + // Clean up expired sesions. + const now = Date.now(); + const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now); + console.log("Cleaning up expired sessions: ", result.changes); + }, CLEAN_UP_INTERVAL); +} export default sessionParser; From d301e562162a25690e4567b80d6f007d156bb884 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 22:19:04 +0200 Subject: [PATCH 09/18] refactor(server): don't set up other timers on module init --- apps/server/src/app.ts | 12 ++-- .../server/src/services/consistency_checks.ts | 12 ++-- apps/server/src/services/scheduler.ts | 64 ++++++++++--------- apps/server/src/services/sync.ts | 16 +++-- 4 files changed, 55 insertions(+), 49 deletions(-) diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 4c6e97dfae..3c5f4bdb72 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -99,16 +99,16 @@ export default async function buildApp() { custom.register(app); error_handlers.register(app); - // triggers sync timer - await import("./services/sync.js"); + const { startSyncTimer } = await import("./services/sync.js"); + startSyncTimer(); - // triggers backup timer await import("./services/backup.js"); - // trigger consistency checks timer - await import("./services/consistency_checks.js"); + const { startConsistencyChecks } = await import("./services/consistency_checks.js"); + startConsistencyChecks(); - await import("./services/scheduler.js"); + const { startScheduler } = await import("./services/scheduler.js"); + startScheduler(); startScheduledCleanup(); diff --git a/apps/server/src/services/consistency_checks.ts b/apps/server/src/services/consistency_checks.ts index 7b4ba72adf..a92daf5b7e 100644 --- a/apps/server/src/services/consistency_checks.ts +++ b/apps/server/src/services/consistency_checks.ts @@ -953,12 +953,14 @@ function runEntityChangesChecks() { consistencyChecks.findEntityChangeIssues(); } -sqlInit.dbReady.then(() => { - setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000); +export function startConsistencyChecks() { + sqlInit.dbReady.then(() => { + setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000); - // kickoff checks soon after startup (to not block the initial load) - setTimeout(cls.wrap(runPeriodicChecks), 4 * 1000); -}); + // kickoff checks soon after startup (to not block the initial load) + setTimeout(cls.wrap(runPeriodicChecks), 4 * 1000); + }); +} export default { runOnDemandChecks, diff --git a/apps/server/src/services/scheduler.ts b/apps/server/src/services/scheduler.ts index f44e3aa430..ef39e76009 100644 --- a/apps/server/src/services/scheduler.ts +++ b/apps/server/src/services/scheduler.ts @@ -35,39 +35,41 @@ function runNotesWithLabel(runAttrValue: string) { } } -// If the database is already initialized, we need to check the hidden subtree. Otherwise, hidden subtree -// is also checked before importing the demo.zip, so no need to do it again. -if (sqlInit.isDbInitialized()) { - console.log("Checking hidden subtree."); - sqlInit.dbReady.then(() => cls.init(() => hiddenSubtreeService.checkHiddenSubtree())); -} - -// Periodic checks. -sqlInit.dbReady.then(() => { - if (!process.env.TRILIUM_SAFE_MODE) { - setTimeout( - cls.wrap(() => runNotesWithLabel("backendStartup")), - 10 * 1000 - ); - - setInterval( - cls.wrap(() => runNotesWithLabel("hourly")), - 3600 * 1000 - ); - - setInterval( - cls.wrap(() => runNotesWithLabel("daily")), - 24 * 3600 * 1000 - ); - - setInterval( - cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()), - 7 * 3600 * 1000 - ); +export function startScheduler() { + // If the database is already initialized, we need to check the hidden subtree. Otherwise, hidden subtree + // is also checked before importing the demo.zip, so no need to do it again. + if (sqlInit.isDbInitialized()) { + console.log("Checking hidden subtree."); + sqlInit.dbReady.then(() => cls.init(() => hiddenSubtreeService.checkHiddenSubtree())); } - setInterval(() => checkProtectedSessionExpiration(), 30000); -}); + // Periodic checks. + sqlInit.dbReady.then(() => { + if (!process.env.TRILIUM_SAFE_MODE) { + setTimeout( + cls.wrap(() => runNotesWithLabel("backendStartup")), + 10 * 1000 + ); + + setInterval( + cls.wrap(() => runNotesWithLabel("hourly")), + 3600 * 1000 + ); + + setInterval( + cls.wrap(() => runNotesWithLabel("daily")), + 24 * 3600 * 1000 + ); + + setInterval( + cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()), + 7 * 3600 * 1000 + ); + } + + setInterval(() => checkProtectedSessionExpiration(), 30000); + }); +} function checkProtectedSessionExpiration() { const protectedSessionTimeout = options.getOptionInt("protectedSessionTimeout"); diff --git a/apps/server/src/services/sync.ts b/apps/server/src/services/sync.ts index ef3bd6cba9..9b01a360bc 100644 --- a/apps/server/src/services/sync.ts +++ b/apps/server/src/services/sync.ts @@ -446,15 +446,17 @@ function getOutstandingPullCount() { return outstandingPullCount; } -becca_loader.beccaLoaded.then(() => { - setInterval(cls.wrap(sync), 60000); +export function startSyncTimer() { + becca_loader.beccaLoaded.then(() => { + setInterval(cls.wrap(sync), 60000); - // kickoff initial sync immediately, but should happen after initial consistency checks - setTimeout(cls.wrap(sync), 5000); + // kickoff initial sync immediately, but should happen after initial consistency checks + setTimeout(cls.wrap(sync), 5000); - // called just so ws.setLastSyncedPush() is called - getLastSyncedPush(); -}); + // called just so ws.setLastSyncedPush() is called + getLastSyncedPush(); + }); +} export default { sync, From 5057c02176c41ed20da406a8ecec179c7b6d2430 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 22:52:26 +0200 Subject: [PATCH 10/18] test(server): fix errors due to database already existing --- apps/server/src/services/import/single.spec.ts | 1 + apps/server/src/services/sql_init.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/import/single.spec.ts b/apps/server/src/services/import/single.spec.ts index e8a341a6b9..866a0874d6 100644 --- a/apps/server/src/services/import/single.spec.ts +++ b/apps/server/src/services/import/single.spec.ts @@ -24,6 +24,7 @@ async function testImport(fileName: string, mimetype: string) { const rootNote = becca.getNote("root"); if (!rootNote) { reject("Missing root note."); + return; } const importedNote = single.importSingleFile( diff --git a/apps/server/src/services/sql_init.ts b/apps/server/src/services/sql_init.ts index 93452669fc..c6a084a72b 100644 --- a/apps/server/src/services/sql_init.ts +++ b/apps/server/src/services/sql_init.ts @@ -50,7 +50,7 @@ async function initDbConnection() { await migrationService.migrateIfNecessary(); - sql.execute('CREATE TEMP TABLE "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)'); + sql.execute('CREATE TEMP TABLE IF NOT EXISTS "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)'); sql.execute(` CREATE TABLE IF NOT EXISTS "user_data" From 4ac22678df9a467023aef3028bfd6b0d0318398a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 23:25:45 +0200 Subject: [PATCH 11/18] test(server): switch to forks with 2 max workers --- apps/server/src/services/sql.ts | 58 ++++++++++++++++++++++----------- apps/server/vite.config.mts | 4 +-- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/apps/server/src/services/sql.ts b/apps/server/src/services/sql.ts index 206a828d66..b65bd88013 100644 --- a/apps/server/src/services/sql.ts +++ b/apps/server/src/services/sql.ts @@ -1,24 +1,28 @@ -"use strict"; + /** * @module sql */ -import log from "./log.js"; -import type { Statement, Database as DatabaseType, RunResult } from "better-sqlite3"; -import dataDir from "./data_dir.js"; -import cls from "./cls.js"; -import fs from "fs"; +import type { Database as DatabaseType, RunResult,Statement } from "better-sqlite3"; import Database from "better-sqlite3"; -import ws from "./ws.js"; +import fs from "fs"; + import becca_loader from "../becca/becca_loader.js"; -import entity_changes from "./entity_changes.js"; +import cls from "./cls.js"; import config from "./config.js"; +import dataDir from "./data_dir.js"; +import entity_changes from "./entity_changes.js"; +import log from "./log.js"; +import ws from "./ws.js"; const dbOpts: Database.Options = { nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined }; +const INTEGRATION_TEST_SAVEPOINT = "trilium_test_reset"; +// The resolved path used to build the current test connection (for savepoint-based resets). +let integrationTestDbPath: string | null = null; let dbConnection: DatabaseType = buildDatabase(); let statementCache: Record = {}; @@ -37,17 +41,33 @@ function buildDatabase() { } function buildIntegrationTestDatabase(dbPath?: string) { - const dbBuffer = fs.readFileSync(dbPath ?? dataDir.DOCUMENT_PATH); - return new Database(dbBuffer, dbOpts); + const resolvedPath = dbPath ?? dataDir.DOCUMENT_PATH; + const dbBuffer = fs.readFileSync(resolvedPath); + const db = new Database(dbBuffer, dbOpts); + integrationTestDbPath = resolvedPath; + // Establish a savepoint so subsequent rebuilds can roll back instantly + // instead of re-reading the file and allocating a new connection. + db.exec(`SAVEPOINT ${INTEGRATION_TEST_SAVEPOINT}`); + return db; } function rebuildIntegrationTestDatabase(dbPath?: string) { - if (dbConnection) { - dbConnection.close(); + const resolvedPath = dbPath ?? dataDir.DOCUMENT_PATH; + + if (dbConnection && resolvedPath === integrationTestDbPath) { + // Fast path: roll back all changes to the initial state without + // closing the connection or re-reading from disk. + dbConnection.exec(`ROLLBACK TO SAVEPOINT ${INTEGRATION_TEST_SAVEPOINT}`); + } else { + // Path changed (e.g. migration tests using a different fixture DB): + // close the old connection and open a fresh one. + if (dbConnection) { + dbConnection.close(); + } + // This allows a database that is read normally but is kept in memory and discards all modifications. + dbConnection = buildIntegrationTestDatabase(dbPath); } - // This allows a database that is read normally but is kept in memory and discards all modifications. - dbConnection = buildIntegrationTestDatabase(dbPath); statementCache = {}; } @@ -129,7 +149,7 @@ function upsert(tableName: string, primaryKey: string, rec: T) { * @returns the corresponding {@link Statement}. */ function stmt(sql: string, isRaw?: boolean) { - const key = (isRaw ? "raw/" + sql : sql); + const key = (isRaw ? `raw/${ sql}` : sql); if (!(key in statementCache)) { statementCache[key] = dbConnection.prepare(sql); @@ -169,11 +189,11 @@ function getManyRows(query: string, params: Params): T[] { let j = 1; for (const param of curParams) { - curParamsObj["param" + j++] = param; + curParamsObj[`param${ j++}`] = param; } let i = 1; - const questionMarks = curParams.map(() => ":param" + i++).join(","); + const questionMarks = curParams.map(() => `:param${ i++}`).join(","); const curQuery = query.replace(/\?\?\?/g, questionMarks); const statement = curParams.length === PARAM_LIMIT ? stmt(curQuery) : dbConnection.prepare(curQuery); @@ -240,11 +260,11 @@ function executeMany(query: string, params: Params) { let j = 1; for (const param of curParams) { - curParamsObj["param" + j++] = param; + curParamsObj[`param${ j++}`] = param; } let i = 1; - const questionMarks = curParams.map(() => ":param" + i++).join(","); + const questionMarks = curParams.map(() => `:param${ i++}`).join(","); const curQuery = query.replace(/\?\?\?/g, questionMarks); dbConnection.prepare(curQuery).run(curParamsObj); diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index ec98733acd..8eb99bde7e 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -29,7 +29,7 @@ export default defineConfig(() => ({ provider: 'v8' as const, reporter: [ "text", "html" ] }, - pool: "vmForks", - maxWorkers: 1 + pool: "forks", + maxWorkers: 2 }, })); From 7c8eb311af425ed3087bc5d2957b52781409fe59 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 23:31:54 +0200 Subject: [PATCH 12/18] test(server): switch to forks with 3 max workers --- apps/server/vite.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index 8eb99bde7e..02829af1e1 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -30,6 +30,6 @@ export default defineConfig(() => ({ reporter: [ "text", "html" ] }, pool: "forks", - maxWorkers: 2 + maxWorkers: 3 }, })); From 5d2daecee0d3737c12455b877e80dbc50633d4e5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 23:35:15 +0200 Subject: [PATCH 13/18] test(server): switch to forks with 6 max workers --- apps/server/vite.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index 02829af1e1..0f2ac57121 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -30,6 +30,6 @@ export default defineConfig(() => ({ reporter: [ "text", "html" ] }, pool: "forks", - maxWorkers: 3 + maxWorkers: 6 }, })); From 432f86ea4b21587452c4a7deb541a60ab7b29404 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 23:37:28 +0200 Subject: [PATCH 14/18] Revert "test(server): switch to forks with 2 max workers" This reverts commit 4ac22678df9a467023aef3028bfd6b0d0318398a. --- apps/server/src/services/sql.ts | 60 +++++++++++---------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/apps/server/src/services/sql.ts b/apps/server/src/services/sql.ts index b65bd88013..206a828d66 100644 --- a/apps/server/src/services/sql.ts +++ b/apps/server/src/services/sql.ts @@ -1,28 +1,24 @@ - +"use strict"; /** * @module sql */ -import type { Database as DatabaseType, RunResult,Statement } from "better-sqlite3"; -import Database from "better-sqlite3"; -import fs from "fs"; - -import becca_loader from "../becca/becca_loader.js"; -import cls from "./cls.js"; -import config from "./config.js"; -import dataDir from "./data_dir.js"; -import entity_changes from "./entity_changes.js"; import log from "./log.js"; +import type { Statement, Database as DatabaseType, RunResult } from "better-sqlite3"; +import dataDir from "./data_dir.js"; +import cls from "./cls.js"; +import fs from "fs"; +import Database from "better-sqlite3"; import ws from "./ws.js"; +import becca_loader from "../becca/becca_loader.js"; +import entity_changes from "./entity_changes.js"; +import config from "./config.js"; const dbOpts: Database.Options = { nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined }; -const INTEGRATION_TEST_SAVEPOINT = "trilium_test_reset"; -// The resolved path used to build the current test connection (for savepoint-based resets). -let integrationTestDbPath: string | null = null; let dbConnection: DatabaseType = buildDatabase(); let statementCache: Record = {}; @@ -41,33 +37,17 @@ function buildDatabase() { } function buildIntegrationTestDatabase(dbPath?: string) { - const resolvedPath = dbPath ?? dataDir.DOCUMENT_PATH; - const dbBuffer = fs.readFileSync(resolvedPath); - const db = new Database(dbBuffer, dbOpts); - integrationTestDbPath = resolvedPath; - // Establish a savepoint so subsequent rebuilds can roll back instantly - // instead of re-reading the file and allocating a new connection. - db.exec(`SAVEPOINT ${INTEGRATION_TEST_SAVEPOINT}`); - return db; + const dbBuffer = fs.readFileSync(dbPath ?? dataDir.DOCUMENT_PATH); + return new Database(dbBuffer, dbOpts); } function rebuildIntegrationTestDatabase(dbPath?: string) { - const resolvedPath = dbPath ?? dataDir.DOCUMENT_PATH; - - if (dbConnection && resolvedPath === integrationTestDbPath) { - // Fast path: roll back all changes to the initial state without - // closing the connection or re-reading from disk. - dbConnection.exec(`ROLLBACK TO SAVEPOINT ${INTEGRATION_TEST_SAVEPOINT}`); - } else { - // Path changed (e.g. migration tests using a different fixture DB): - // close the old connection and open a fresh one. - if (dbConnection) { - dbConnection.close(); - } - // This allows a database that is read normally but is kept in memory and discards all modifications. - dbConnection = buildIntegrationTestDatabase(dbPath); + if (dbConnection) { + dbConnection.close(); } + // This allows a database that is read normally but is kept in memory and discards all modifications. + dbConnection = buildIntegrationTestDatabase(dbPath); statementCache = {}; } @@ -149,7 +129,7 @@ function upsert(tableName: string, primaryKey: string, rec: T) { * @returns the corresponding {@link Statement}. */ function stmt(sql: string, isRaw?: boolean) { - const key = (isRaw ? `raw/${ sql}` : sql); + const key = (isRaw ? "raw/" + sql : sql); if (!(key in statementCache)) { statementCache[key] = dbConnection.prepare(sql); @@ -189,11 +169,11 @@ function getManyRows(query: string, params: Params): T[] { let j = 1; for (const param of curParams) { - curParamsObj[`param${ j++}`] = param; + curParamsObj["param" + j++] = param; } let i = 1; - const questionMarks = curParams.map(() => `:param${ i++}`).join(","); + const questionMarks = curParams.map(() => ":param" + i++).join(","); const curQuery = query.replace(/\?\?\?/g, questionMarks); const statement = curParams.length === PARAM_LIMIT ? stmt(curQuery) : dbConnection.prepare(curQuery); @@ -260,11 +240,11 @@ function executeMany(query: string, params: Params) { let j = 1; for (const param of curParams) { - curParamsObj[`param${ j++}`] = param; + curParamsObj["param" + j++] = param; } let i = 1; - const questionMarks = curParams.map(() => `:param${ i++}`).join(","); + const questionMarks = curParams.map(() => ":param" + i++).join(","); const curQuery = query.replace(/\?\?\?/g, questionMarks); dbConnection.prepare(curQuery).run(curParamsObj); From e928337fe9a6fb284163266cdb7cb4fce6966c27 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 23:40:43 +0200 Subject: [PATCH 15/18] test(server): adjust timeout --- apps/server/vite.config.mts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index 0f2ac57121..2fa10ff798 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -19,7 +19,8 @@ export default defineConfig(() => ({ exclude: [ "spec/build-checks/**", ], - hookTimeout: 40_000, + hookTimeout: 20_000, + testTimeout: 40_000, reporters: [ "verbose", ["html", { outputFile: "./test-output/vitest/html/index.html" }] From bc6a6fd8608946cfe0f7d5c80b932d6e54ee0249 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 23:44:24 +0200 Subject: [PATCH 16/18] Revert "test(server): reset ws module" This reverts commit 02123988153d92a3dcb473789d0aaf27b7165401. --- apps/server/src/services/ws.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/services/ws.spec.ts b/apps/server/src/services/ws.spec.ts index 4cea6a2f2d..bd3b105aa2 100644 --- a/apps/server/src/services/ws.spec.ts +++ b/apps/server/src/services/ws.spec.ts @@ -56,9 +56,8 @@ describe('WebSocket Service', () => { let log: any; beforeEach(async () => { - vi.resetModules(); vi.clearAllMocks(); - + // Get mocked log log = (await import('./log.js')).default; From dee5380e608287c60639c50ff4f23d17be671fb7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 5 Mar 2026 23:46:50 +0200 Subject: [PATCH 17/18] fix(ci): sequential tests ended up run in parallel --- .github/workflows/dev.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 5a281f5bf2..5392690413 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -59,8 +59,13 @@ jobs: path: apps/server/test-output/vitest/html/ retention-days: 30 + - name: Run CKEditor e2e tests + run: | + pnpm run --filter=ckeditor5-mermaid test + pnpm run --filter=ckeditor5-math test + - name: Run the rest of the tests - run: pnpm run --filter=\!client --filter=\!server test + run: pnpm run --filter=\!client --filter=\!server --filter=\!ckeditor5-mermaid --filter=\!ckeditor5-math test build_docker: name: Build Docker image From bace3daadc355c276b8da9a511f68e40a61cd45c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 6 Mar 2026 08:43:31 +0200 Subject: [PATCH 18/18] Update apps/server/src/routes/session_parser.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/server/src/routes/session_parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/routes/session_parser.ts b/apps/server/src/routes/session_parser.ts index 882701ab54..a52a9183aa 100644 --- a/apps/server/src/routes/session_parser.ts +++ b/apps/server/src/routes/session_parser.ts @@ -115,7 +115,7 @@ const sessionParser: express.RequestHandler = session({ export function startSessionCleanup() { setInterval(() => { - // Clean up expired sesions. + // Clean up expired sessions. const now = Date.now(); const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now); console.log("Cleaning up expired sessions: ", result.changes);