Files
odysseus/tests/test_chat_spinner_completion_js.py
T
pewdiepie-archdaemon 2e8413a54a Preserve preview harness, editor, email and task improvements
Snapshot current maintainer-preview application changes and regression fixtures for integration into lab. Excludes local runtime data, evaluation outputs and source backups. Focused Python regression selection: 140 passed; full suite not certified.
2026-10-01 01:34:26 +00:00

100 lines
4.9 KiB
Python

"""Exercise the stream-owned sidebar cleanup for terminal and detached paths."""
import json
import subprocess
from pathlib import Path
def test_sidebar_completion_clears_only_the_owning_finished_stream():
root = Path(__file__).resolve().parents[1]
source = (root / 'static/js/chat.js').read_text()
# The finalizer is a sibling of try, so its completion flag must be in
# the shared outer scope alongside abortCtrl, not the SSE parser block.
assert source.count('let _streamSawDone = false;') == 1
assert source.index('let _streamSawDone = false;') < source.index('let abortCtrl = null;')
start = source.index(' if (_ownsStreamState && _streamSawDone) {')
end = source.index(' const _finallyRegistered', start)
script = '''
const cleanup = new Function('_ownsStreamState', '_streamSawDone', 'abortCtrl',
'sessionModule', 'streamSessionId', BODY);
const results = [];
for (const [owner, done, reason] of [
[true,true,null], [true,false,'user-stop'], [true,false,'detach'],
[false,true,null], [false,false,'user-stop'], [true,false,null]
]) {
const calls=[];
cleanup(owner,done,{_reason:reason},{
markStreamComplete: id=>calls.push('complete:'+id),
clearStreaming: id=>calls.push('clear:'+id),
},'chat');
results.push(calls);
}
console.log(JSON.stringify(results));
'''.replace('BODY', json.dumps(source[start:end]))
result = subprocess.run(['node', '--input-type=module', '-e', script],
capture_output=True, text=True, check=True)
assert json.loads(result.stdout) == [['complete:chat'], ['clear:chat'], [], [], [], []]
def test_tool_wait_indicator_cannot_reappear_after_completion_or_stop():
root = Path(__file__).resolve().parents[1]
source = (root / 'static/js/chat.js').read_text()
start = source.index(' let _toolPauseTimer = null;')
end = source.index(' // Document streaming state', start)
# The only scheduling call belongs to tool completion, not prose deltas.
assert source.count('_scheduleToolWaitSpinner();') == 1
tool_output = source.index("json.type === 'tool_output'", source.index("json.type === 'tool_start')"))
assert source.index('_scheduleToolWaitSpinner();') > tool_output
script = r'''
const results = [];
for (const mode of ['waiting', 'done', 'stopped', 'replaced', 'hidden', 'cancelled']) {
let callback, shown = 0;
const streamSessionId = 'chat';
const abortCtrl = {signal: {aborted: mode === 'stopped'}};
const _activeStreams = new Map([['chat', {abortCtrl: mode === 'replaced' ? {} : abortCtrl}]]);
const sessionModule = {getCurrentSessionId: () => mode === 'hidden' ? 'other' : 'chat'};
let _streamSawDone = false, _thinkingSpinnerEl = null, _cancelThinkingTimer;
const _showThinkingSpinner = () => shown++;
const _thinkingLabel = () => 'Thinking';
const setTimeout = fn => {callback = fn; return 1;};
const clearTimeout = () => {callback = null;};
BODY
_scheduleToolWaitSpinner();
if (mode === 'done') _streamSawDone = true;
if (mode === 'cancelled') _cancelThinkingTimer();
callback?.();
results.push(shown);
}
console.log(JSON.stringify(results));
'''.replace('BODY', source[start:end])
result = subprocess.run(['node', '--input-type=module', '-e', script],
capture_output=True, text=True, check=True)
assert json.loads(result.stdout) == [1, 0, 0, 0, 0, 0]
def test_done_exits_reader_without_waiting_for_connection_close():
source = (Path(__file__).resolve().parents[1] / 'static/js/chat.js').read_text()
start = source.index(" if (data === '[DONE]') {")
end = source.index(' try {\n const json = JSON.parse(data);', start)
body = source[start:end]
script = r'''
let reads = 0, cancellations = 0, _streamSawDone = false;
const reader = {cancel: async () => {cancellations++;}};
const _cancelThinkingTimer = () => {}, _removeThinkingSpinner = () => {};
const _activeStreams = new Map(), _backgroundStreams = new Map();
const streamSessionId = 'test', document = {visibilityState: 'visible'};
const _closeOpenThinkingMarkup = () => {};
const _isBg = false, isThinking = false;
streamReadLoop:
while (true) {
reads++;
if (reads > 1) throw Error('Waited for EOF after DONE');
for (const data of ['[DONE]']) {
BODY
}
}
console.log(JSON.stringify({reads, cancellations, done: _streamSawDone}));
'''.replace('BODY', body)
result = subprocess.run(['node', '--input-type=module', '-e', script],
capture_output=True, text=True, check=True)
assert json.loads(result.stdout) == {'reads': 1, 'cancellations': 1, 'done': True}