Compare commits

..

21 Commits

Author SHA1 Message Date
renovate[bot]
deb078b8c7 chore(deps): update dependency @redocly/cli to v2.20.4 2026-03-06 06:48:45 +00:00
Elian Doran
11e4b672d1 Fix CI test issues (#8932) 2026-03-06 08:43:51 +02:00
Elian Doran
bace3daadc Update apps/server/src/routes/session_parser.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 08:43:31 +02:00
Elian Doran
dee5380e60 fix(ci): sequential tests ended up run in parallel 2026-03-06 08:20:17 +02:00
Elian Doran
bc6a6fd860 Revert "test(server): reset ws module"
This reverts commit 0212398815.
2026-03-05 23:44:24 +02:00
Elian Doran
e928337fe9 test(server): adjust timeout 2026-03-05 23:40:43 +02:00
Elian Doran
432f86ea4b Revert "test(server): switch to forks with 2 max workers"
This reverts commit 4ac22678df.
2026-03-05 23:37:28 +02:00
Elian Doran
5d2daecee0 test(server): switch to forks with 6 max workers 2026-03-05 23:35:15 +02:00
Elian Doran
7c8eb311af test(server): switch to forks with 3 max workers 2026-03-05 23:31:54 +02:00
Elian Doran
4ac22678df test(server): switch to forks with 2 max workers 2026-03-05 23:25:45 +02:00
Elian Doran
5057c02176 test(server): fix errors due to database already existing 2026-03-05 22:52:26 +02:00
Elian Doran
d301e56216 refactor(server): don't set up other timers on module init 2026-03-05 22:19:04 +02:00
Elian Doran
3c22ab8c9c refactor(server): don't set up session timer on module init 2026-03-05 22:17:19 +02:00
Elian Doran
0212398815 test(server): reset ws module 2026-03-05 22:14:34 +02:00
Elian Doran
db0c515bad test(server): fake timers not restored 2026-03-05 22:11:51 +02:00
Elian Doran
9b4f8c5003 feat(ci/client): HTML output 2026-03-05 22:07:11 +02:00
Elian Doran
85d8c4c8fa feat(ci/server): HTML output 2026-03-05 22:06:46 +02:00
Elian Doran
5afab6938a test(server): reduce max workers to 1 2026-03-05 21:54:30 +02:00
Elian Doran
a437169ad5 test(server): increase hook timeout 2026-03-05 21:20:12 +02:00
Elian Doran
f632d3aeb6 Merge remote-tracking branch 'origin/main' into fix/ci 2026-03-05 21:14:57 +02:00
Elian Doran
513fffcb1a ci(dev): escape test filter 2026-03-05 21:14:21 +02:00
17 changed files with 153 additions and 117 deletions

View File

@@ -40,11 +40,32 @@ 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
- 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 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

View File

@@ -16,7 +16,7 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.30.3",
"devDependencies": {
"@redocly/cli": "2.20.2",
"@redocly/cli": "2.20.4",
"archiver": "7.0.1",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",

View File

@@ -1,6 +1,6 @@
import "./TableOfContents.css";
import { attributeChangeAffectsHeading, CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
@@ -170,14 +170,11 @@ function EditableTextTableOfContents() {
const affectsHeadings = changes.some( change => {
return (
change.type === 'insert' || change.type === 'remove' ||
(change.type === 'attribute' && attributeChangeAffectsHeading(change, textEditor))
change.type === 'insert' || change.type === 'remove' || (change.type === 'attribute' && change.attributeKey === 'headingLevel')
);
});
if (affectsHeadings) {
requestAnimationFrame(() => {
setHeadings(extractTocFromTextEditor(textEditor));
});
setHeadings(extractTocFromTextEditor(textEditor));
}
};

View File

@@ -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,

View File

@@ -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())
@@ -98,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();

View File

@@ -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

View File

@@ -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 sessions.
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;

View File

@@ -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,

View File

@@ -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(

View File

@@ -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");

View File

@@ -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"

View File

@@ -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,

View File

@@ -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;
});

View File

@@ -19,16 +19,18 @@ export default defineConfig(() => ({
exclude: [
"spec/build-checks/**",
],
hookTimeout: 20000,
hookTimeout: 20_000,
testTimeout: 40_000,
reporters: [
"verbose"
"verbose",
["html", { outputFile: "./test-output/vitest/html/index.html" }]
],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
reporter: [ "text", "html" ]
},
pool: "vmForks",
maxWorkers: 3
pool: "forks",
maxWorkers: 6
},
}));

View File

@@ -11,7 +11,6 @@ export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, Model
export type { TemplateDefinition } from "ckeditor5-premium-features";
export { default as buildExtraCommands } from "./extra_slash_commands.js";
export { default as getCkLocale } from "./i18n.js";
export * from "./utils.js";
// Import with sideffects to ensure that type augmentations are present.
import "@triliumnext/ckeditor5-math";

View File

@@ -1,28 +0,0 @@
import type { DifferItemAttribute, Editor, ModelDocumentFragment, ModelElement, ModelNode } from "ckeditor5";
function hasHeadingAncestor(node: ModelElement | ModelNode | ModelDocumentFragment | null): boolean {
let current: ModelElement | ModelNode | ModelDocumentFragment | null = node;
while (current) {
if (!!current && current.is('element') && (current as ModelElement).name.startsWith("heading")) return true;
current = current.parent;
}
return false;
}
export function attributeChangeAffectsHeading(change: DifferItemAttribute, editor: Editor): boolean {
if (change.type !== "attribute") return false;
// Fast checks on range boundaries
if (hasHeadingAncestor(change.range.start.parent) || hasHeadingAncestor(change.range.end.parent)) {
return true;
}
// Robust check across the whole changed range
const range = editor.model.createRange(change.range.start, change.range.end);
for (const item of range.getItems()) {
const baseNode = item.is("$textProxy") ? item.parent : item;
if (hasHeadingAncestor(baseNode)) return true;
}
return false;
}

55
pnpm-lock.yaml generated
View File

@@ -156,8 +156,8 @@ importers:
apps/build-docs:
devDependencies:
'@redocly/cli':
specifier: 2.20.2
version: 2.20.2(@opentelemetry/api@1.9.0)(bufferutil@4.0.9)(core-js@3.46.0)(encoding@0.1.13)(utf-8-validate@6.0.5)
specifier: 2.20.4
version: 2.20.4(@opentelemetry/api@1.9.0)(bufferutil@4.0.9)(core-js@3.46.0)(encoding@0.1.13)(utf-8-validate@6.0.5)
archiver:
specifier: 7.0.1
version: 7.0.1
@@ -5122,8 +5122,8 @@ packages:
'@redocly/ajv@8.18.0':
resolution: {integrity: sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==}
'@redocly/cli@2.20.2':
resolution: {integrity: sha512-wfEoGFoXq1vZjd9uEtTd9mIixF5I5Ci1rusK/9HHcS6UGy3o2kuvrSn1daDHl1T3KcG5YFqQoLzzEgcL4Je7KQ==}
'@redocly/cli@2.20.4':
resolution: {integrity: sha512-ahZmcTfEvw2ghWBm418A3anN3XS88Izbqn+HyH6fm/iJ/x04vFwW2KSv/V2K2JZON5ImpTCQBHPQd9bSmmZw9A==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
hasBin: true
@@ -5137,12 +5137,12 @@ packages:
resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@redocly/openapi-core@2.20.2':
resolution: {integrity: sha512-L3rzEZWMxq9SpAHP8k9C+/Fqxex4vxXgSZ5hQ+dg7++LXre67ZbT3RkhKsmYadJB/EUdbS6Z+h1Ont5TAI6zyQ==}
'@redocly/openapi-core@2.20.4':
resolution: {integrity: sha512-3WZh8dPF6MrxLDbTG4GXtV81EOqHrpMWlOhELWBIICRieMMt/LKcGFuOBRdLEp/KMU2ypwQLKYHrKYCeUNwO3Q==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
'@redocly/respect-core@2.20.2':
resolution: {integrity: sha512-oUCp+H83py0DEq5DJ+XmGPJQW9Yoq/tgml4OGwQrPEt1sC7QF7GjMRCA1HnRS4/dQ9psWGyFj9FwI1fCupQ8uw==}
'@redocly/respect-core@2.20.4':
resolution: {integrity: sha512-kOM1cCDQtTwZkD58Lx3IIUPcFuRD8bnlkCFUvi0iigy6JJKfpRwgXuzkzFf6FHVO9Z5LwJuYgIP58q3TftWrqg==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
'@replit/codemirror-indentation-markers@6.5.3':
@@ -9851,6 +9851,9 @@ packages:
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fast-xml-builder@1.0.0:
resolution: {integrity: sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==}
fast-xml-parser@4.4.1:
resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
hasBin: true
@@ -9863,6 +9866,10 @@ packages:
resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==}
hasBin: true
fast-xml-parser@5.4.2:
resolution: {integrity: sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==}
hasBin: true
fastest-levenshtein@1.0.16:
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
engines: {node: '>= 4.9.1'}
@@ -12662,6 +12669,9 @@ packages:
openapi-sampler@1.7.0:
resolution: {integrity: sha512-fWq32F5vqGpgRJYIarC/9Y1wC9tKnRDcCOjsDJ7MIcSv2HsE7kNifcXIZ8FVtNStBUWxYrEk/MKqVF0SwZ5gog==}
openapi-sampler@1.7.1:
resolution: {integrity: sha512-pKFRROcYyxRt9GIn0NmS+GkWPS19l0CLQRYAnHk4m1Qp+G43ssVNcfRMs1sLkGrVMuFWO4P4F6YMXeXnfyFGuQ==}
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
@@ -21507,14 +21517,14 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
'@redocly/cli@2.20.2(@opentelemetry/api@1.9.0)(bufferutil@4.0.9)(core-js@3.46.0)(encoding@0.1.13)(utf-8-validate@6.0.5)':
'@redocly/cli@2.20.4(@opentelemetry/api@1.9.0)(bufferutil@4.0.9)(core-js@3.46.0)(encoding@0.1.13)(utf-8-validate@6.0.5)':
dependencies:
'@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
'@redocly/openapi-core': 2.20.2
'@redocly/respect-core': 2.20.2
'@redocly/openapi-core': 2.20.4
'@redocly/respect-core': 2.20.4
abort-controller: 3.0.0
ajv: '@redocly/ajv@8.18.0'
ajv-formats: 3.0.1(@redocly/ajv@8.18.0)
@@ -21566,7 +21576,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@redocly/openapi-core@2.20.2':
'@redocly/openapi-core@2.20.4':
dependencies:
'@redocly/ajv': 8.18.0
'@redocly/config': 0.44.0
@@ -21579,18 +21589,18 @@ snapshots:
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
'@redocly/respect-core@2.20.2':
'@redocly/respect-core@2.20.4':
dependencies:
'@faker-js/faker': 7.6.0
'@noble/hashes': 1.8.0
'@redocly/ajv': 8.18.0
'@redocly/openapi-core': 2.20.2
'@redocly/openapi-core': 2.20.4
ajv: '@redocly/ajv@8.18.0'
better-ajv-errors: 1.2.0(@redocly/ajv@8.18.0)
colorette: 2.0.20
json-pointer: 0.6.2
jsonpath-rfc9535: 1.3.0
openapi-sampler: 1.7.0
openapi-sampler: 1.7.1
outdent: 0.8.0
picomatch: 4.0.3
@@ -28316,6 +28326,8 @@ snapshots:
fast-uri@3.1.0: {}
fast-xml-builder@1.0.0: {}
fast-xml-parser@4.4.1:
dependencies:
strnum: 1.1.2
@@ -28328,6 +28340,11 @@ snapshots:
dependencies:
strnum: 2.1.2
fast-xml-parser@5.4.2:
dependencies:
fast-xml-builder: 1.0.0
strnum: 2.1.2
fastest-levenshtein@1.0.16: {}
fastq@1.19.1:
@@ -31699,6 +31716,12 @@ snapshots:
fast-xml-parser: 5.3.6
json-pointer: 0.6.2
openapi-sampler@1.7.1:
dependencies:
'@types/json-schema': 7.0.15
fast-xml-parser: 5.4.2
json-pointer: 0.6.2
openapi-types@12.1.3: {}
opener@1.5.2: {}
@@ -32547,7 +32570,7 @@ snapshots:
postcss@8.4.49:
dependencies:
nanoid: 5.1.5
nanoid: 5.1.6
picocolors: 1.1.1
source-map-js: 1.2.1