mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 19:32:21 +02:00
* refactor(notes): decompose Notes into feature-owned modules * fix(notes): avoid HTML interpolation in reminder picker * docs(notes): drop checkpoint archives, session logs and publication drafts The N0-N6 tarballs and raw pytest/node logs are not read by any script or test, and the logs carry absolute home-directory paths. Once squashed into dev they would stay in history for good. notes-pr.md and notes-issue.md were the drafts used to open #6712 and #6713, not documentation. notes-validation.md keeps the counts, the checkpoint hashes and implementation.sha256 as the record; local interpreter paths and the local Docker image name are replaced with portable placeholders. * fix(notes): keep the state store raw and stop masonry re-observing after close - state: writes built from views (filter, spread, splice) stored nested read-only views in the store, so structuredClone on a stored note threw DataCloneError. Store writes now unwrap nested views. - layout: a save that resolves after closePanel re-renders the leaving pane and re-created the masonry ResizeObserver that teardown had disposed. Skip creating it once Notes is closed. - index.js: drop 30 imported or declared names that were never used and group the imports at the top. Import order is unchanged. - Revert unrelated punctuation edits in .gitattributes and keep the header comment and docstring first in compare/vote.js and test_notes_dom_xss_helpers.py. Both fixes have a node test that fails on the previous head. * test(notes): make script rejection assertion case-insensitive Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum. --------- Co-authored-by: Léo <leograndcontact@gmail.com>
82 lines
3.9 KiB
JavaScript
82 lines
3.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { readFileSync } from 'node:fs';
|
|
import vm from 'node:vm';
|
|
|
|
const chatSource = readFileSync(new URL('../static/js/chat.js', import.meta.url), 'utf8');
|
|
const sessionSource = readFileSync(new URL('../static/js/sessions.js', import.meta.url), 'utf8');
|
|
const notesSource = readFileSync(new URL('../static/js/features/notes/application/selection.js', import.meta.url), 'utf8');
|
|
|
|
function setup() {
|
|
const start = chatSource.indexOf(" window.addEventListener('odysseus:notes-context-selected'");
|
|
const end = chatSource.indexOf(' function _fmtContextNumber(n)', start);
|
|
assert.ok(start !== -1 && end > start, 'production event handlers must exist');
|
|
const handlers = new Map();
|
|
const rendered = [];
|
|
let sessionId = null;
|
|
let pending = false;
|
|
let clear;
|
|
const win = { addEventListener: (name, fn) => handlers.set(name, fn) };
|
|
const doc = {
|
|
getElementById: () => ({ addEventListener: (name, fn) => { if (name === 'click') clear = fn; } }),
|
|
};
|
|
const sessionModule = {
|
|
getCurrentSessionId: () => sessionId,
|
|
hasPendingChat: () => pending,
|
|
};
|
|
vm.runInNewContext(
|
|
`const _selectedNoteIdsBySession = new Map(); let _pendingSelectedNoteIds = [];
|
|
${chatSource.slice(start, end)}`,
|
|
{ window: win, document: doc, sessionModule, console, _renderSelectedNotesIndicator: ids => rendered.push(Array.from(ids)) },
|
|
);
|
|
return {
|
|
setSession(id, isPending = false) { sessionId = id; pending = isPending; },
|
|
emit(name, detail) { handlers.get(name)({ detail }); },
|
|
selected(ids) { const detail = { noteIds: ids, accepted: false }; handlers.get('odysseus:notes-context-selected')({ detail }); return detail; },
|
|
clear() { clear(); },
|
|
lastRendered() { return rendered.at(-1); },
|
|
};
|
|
}
|
|
|
|
test('pending selections transfer only to the materialized chat', () => {
|
|
const ctx = setup();
|
|
ctx.setSession(null, true);
|
|
assert.equal(ctx.selected(['a', 'b']).accepted, true);
|
|
assert.deepEqual(ctx.lastRendered(), ['a', 'b']);
|
|
ctx.setSession('new-chat');
|
|
ctx.emit('odysseus:session-changed', { sessionId: 'new-chat', materialized: true });
|
|
assert.deepEqual(ctx.lastRendered(), ['a', 'b']);
|
|
ctx.setSession('other-chat');
|
|
ctx.emit('odysseus:session-changed', { sessionId: 'other-chat' });
|
|
assert.deepEqual(ctx.lastRendered(), []);
|
|
ctx.setSession('new-chat');
|
|
ctx.emit('odysseus:session-changed', { sessionId: 'new-chat' });
|
|
assert.deepEqual(ctx.lastRendered(), ['a', 'b']);
|
|
ctx.clear();
|
|
assert.deepEqual(ctx.lastRendered(), []);
|
|
});
|
|
|
|
test('new chat resets old indicator and invalid selection is rejected', () => {
|
|
const ctx = setup();
|
|
assert.equal(ctx.selected(['a']).accepted, false);
|
|
ctx.setSession('old-chat');
|
|
assert.equal(ctx.selected(['a']).accepted, true);
|
|
ctx.setSession(null, true);
|
|
ctx.emit('odysseus:session-changed', { sessionId: null });
|
|
assert.deepEqual(ctx.lastRendered(), []);
|
|
assert.equal(ctx.selected(['b']).accepted, true);
|
|
ctx.emit('odysseus:session-changed', { sessionId: null });
|
|
assert.deepEqual(ctx.lastRendered(), []);
|
|
ctx.setSession('fresh');
|
|
ctx.emit('odysseus:session-changed', { sessionId: 'fresh', materialized: true });
|
|
assert.deepEqual(ctx.lastRendered(), []);
|
|
});
|
|
|
|
test('producer events and rejection acknowledgement are wired in source', () => {
|
|
const create = sessionSource.slice(sessionSource.indexOf('export function createDirectChat'), sessionSource.indexOf('export async function materializePendingSession'));
|
|
const materialize = sessionSource.slice(sessionSource.indexOf('export async function materializePendingSession'), sessionSource.indexOf('export function preMaterializePendingSession'));
|
|
assert.match(create, /currentSessionId = null;[\s\S]{0,180}odysseus:session-changed/);
|
|
assert.match(materialize, /currentSessionId = payload.id;[\s\S]{0,180}odysseus:session-changed/);
|
|
assert.match(notesSource, /if \(!detail\.accepted\)/);
|
|
});
|