mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 03:12:21 +02:00
feat(notes): attach selected Notes/Todos as Agent context (#6495)
* feat: add Notes and Todos as agent chat context - Send selected Note/Todo IDs from the Notes UI - Load selected notes with owner scoping in chat routes - Format note content and checklist items for the model - Inject selected notes as untrusted chat context - Add a selected-context indicator beside Shell Access - Keep selected context associated with the active chat session * Improved selected notes context handling and discouragin redundant note lookup and permission requests * Fix selected notes UI when switching chats * fix(notes): harden selected context lifecycle and validation --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
co-authored by
Alexandre Teixeira
parent
a8c147b238
commit
8feb50e894
@@ -0,0 +1,81 @@
|
||||
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/notes.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\)/);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Regression tests for untrusted selected-note ID parsing."""
|
||||
import json
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from routes.chat_routes import _parse_selected_note_ids
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw,expected", [
|
||||
(None, []),
|
||||
("", []),
|
||||
("[]", []),
|
||||
('["one", "two", "one"]', ["one", "two"]),
|
||||
])
|
||||
def test_selected_note_ids_valid(raw, expected):
|
||||
assert _parse_selected_note_ids(raw) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", [
|
||||
"42", "true", "null", '{"id":"a"}',
|
||||
'["one", 5]', '["one", null]', '["one", false]',
|
||||
'[""]', '[" "]', '["one", {}]',
|
||||
"not json", json.dumps(["a"] * 51),
|
||||
json.dumps(["a" * 129]), " " * 8193,
|
||||
])
|
||||
def test_selected_note_ids_rejected(raw):
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_parse_selected_note_ids(raw)
|
||||
assert exc.value.status_code == 400
|
||||
Reference in New Issue
Block a user