diff --git a/test/admin-docs-toc.test.js b/test/admin-docs-toc.test.js
new file mode 100644
index 0000000..601b3a8
--- /dev/null
+++ b/test/admin-docs-toc.test.js
@@ -0,0 +1,67 @@
+const { test } = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const vm = require('node:vm');
+const { JSDOM } = require('jsdom');
+
+function loadAdminDocsHarness() {
+ const dom = new JSDOM(`
+
+
+ `, { url: 'https://app.example.test/' });
+ const source = fs.readFileSync(path.join(__dirname, '..', 'public/js/admin-docs.js'), 'utf8');
+ const context = vm.createContext({
+ window: dom.window,
+ document: dom.window.document,
+ console: { log() {}, warn() {}, error() {} },
+ URL: dom.window.URL,
+ history: dom.window.history,
+ requestAnimationFrame: function(fn) { fn(); },
+ setTimeout: function(fn) { fn(); return 0; },
+ fetch: function() { throw new Error('fetch not expected'); },
+ getAuthHeaders: function() { return {}; }
+ });
+ vm.runInContext(source + '\nwindow.__adminDocsTest = { prepareDocAnchors, prepareDocLinks, scrollReaderToHash, handleDocAnchorClick };', context);
+ return { dom, api: dom.window.__adminDocsTest };
+}
+
+test('admin docs TOC links keep href and scroll the internal reader', () => {
+ const { dom, api } = loadAdminDocsHarness();
+ const document = dom.window.document;
+ const body = document.getElementById('docs-reader-body');
+ const reader = document.getElementById('docs-reader');
+ body.innerHTML = 'Admin - Configuration
';
+
+ api.prepareDocAnchors(body);
+ api.prepareDocLinks(body);
+
+ const link = body.querySelector('a');
+ const target = document.getElementById('admin---configuration');
+ assert.equal(link.getAttribute('href'), '#admin---configuration');
+ assert.equal(link.dataset.docHash, '#admin---configuration');
+ assert.ok(target, 'heading should get a stable GitHub-style id');
+
+ reader.getBoundingClientRect = function() { return { top: 10 }; };
+ target.getBoundingClientRect = function() { return { top: 110 }; };
+
+ const handled = api.handleDocAnchorClick({ target: link, preventDefault() {} });
+ assert.equal(handled, true);
+ assert.equal(reader.scrollTop, 92);
+ assert.equal(dom.window.location.hash, '#admin---configuration');
+});
+
+test('admin docs rewrites relative markdown hash links as in-app doc links', () => {
+ const { dom, api } = loadAdminDocsHarness();
+ const body = dom.window.document.getElementById('docs-reader-body');
+ body.innerHTML = 'Architecture';
+
+ api.prepareDocLinks(body);
+
+ const link = body.querySelector('a');
+ assert.equal(link.getAttribute('href'), 'logic/architecture.md#runtime-architecture');
+ assert.equal(link.dataset.docFile, 'logic/architecture.md');
+ assert.equal(link.dataset.docHash, '#runtime-architecture');
+});