* fix(bootstrap): discover .venv layout in agent_dir (closes #938) (#941) * fix(btw): harden _streamDone flag — defensive ordering + session guard + stream_end coverage (#935) * fix(btw): align /reasoning toast prefix with BRAIN const (#939) * docs: v0.50.185 release notes, update test counts to 2107 --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
This commit is contained in:
1 parent
87d4136a43
commit
061af78cde
7 files changed
+96
-10
No files matched your search
@@ -5,6 +5,13 @@
|
|||||||
### Fixed
|
### Fixed
|
||||||
- **Reasoning chip now appears after the model chip** in the composer toolbar — model is a more fundamental choice and should be stable in position regardless of whether reasoning is active. Order: Profile → Workspace → Model → Reasoning. (`static/index.html`)
|
- **Reasoning chip now appears after the model chip** in the composer toolbar — model is a more fundamental choice and should be stable in position regardless of whether reasoning is active. Order: Profile → Workspace → Model → Reasoning. (`static/index.html`)
|
||||||
|
|
||||||
|
## v0.50.185 — 2026-04-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`/btw` stream handler hardened** — `_streamDone=true` now set *before* `src.close()` in `done` and `apperror` handlers (defensive ordering); `_ensureBtwRow()` in `done` gated on session match (`S.session.session_id === parentSid`) to prevent btw bubble leaking into a different session if the user switches mid-stream; `stream_end` handler also sets `_streamDone=true` for defense-in-depth. 14 new regression tests added. (`static/messages.js`, `tests/test_reasoning_chip_btw_fixes.py`) [#935]
|
||||||
|
- **`/reasoning` toast aligned with BRAIN prefix** — success toast now reads `🧠 Reasoning effort: <level>` consistent with the command's other toasts. (`static/commands.js`) [#939]
|
||||||
|
- **Bootstrap Python discovery finds `.venv/` layout** — `discover_launcher_python` now checks both `venv/` and `.venv/` inside the agent directory, covering installations that use a leading-dot venv layout. (`bootstrap.py`) [#941]
|
||||||
|
|
||||||
## v0.50.184 — 2026-04-24
|
## v0.50.184 — 2026-04-24
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+2
-2
@@ -3,8 +3,8 @@
|
|||||||
> Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI.
|
> Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI.
|
||||||
> Everything you can do from the CLI terminal, you can do from this UI.
|
> Everything you can do from the CLI terminal, you can do from this UI.
|
||||||
>
|
>
|
||||||
> Last updated: v0.50.156 (April 22, 2026) — 1903 tests collected
|
> Last updated: v0.50.185 (April 24, 2026) — 2107 tests collected
|
||||||
> Tests: 1903 collected (`pytest tests/ --collect-only -q`)
|
> Tests: 2107 collected (`pytest tests/ --collect-only -q`)
|
||||||
> Source: <repo>/
|
> Source: <repo>/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@
|
|||||||
> Prerequisites: SSH tunnel is active on port 8787. Open http://localhost:8787 in browser.
|
> Prerequisites: SSH tunnel is active on port 8787. Open http://localhost:8787 in browser.
|
||||||
> Server health check: curl http://127.0.0.1:8787/health should return {"status":"ok"}.
|
> Server health check: curl http://127.0.0.1:8787/health should return {"status":"ok"}.
|
||||||
>
|
>
|
||||||
> Automated coverage: 1777 tests collected via `pytest tests/ --collect-only -q`. Includes onboarding coverage for bootstrap/static wizard presence, real provider config persistence (`config.yaml` + `.env`), the `/api/onboarding/*` backend, the onboarding skip/existing-config guard, and CSS regression coverage for smooth thinking/tool card disclosure animation.
|
> Automated coverage: 2107 tests collected via `pytest tests/ --collect-only -q`. Includes onboarding coverage for bootstrap/static wizard presence, real provider config persistence (`config.yaml` + `.env`), the `/api/onboarding/*` backend, the onboarding skip/existing-config guard, and CSS regression coverage for smooth thinking/tool card disclosure animation.
|
||||||
> Run: `pytest tests/ -v --timeout=60`
|
> Run: `pytest tests/ -v --timeout=60`
|
||||||
>
|
>
|
||||||
> Local regression focus: verify that a previously closed workspace panel stays visually closed from first paint through boot completion on desktop refresh; there should be no brief open-then-close flash.
|
> Local regression focus: verify that a previously closed workspace panel stays visually closed from first paint through boot completion on desktop refresh; there should be no brief open-then-close flash.
|
||||||
@@ -1783,7 +1783,7 @@ Bridged CLI sessions:
|
|||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: v0.50.91, April 19, 2026*
|
*Last updated: v0.50.91, April 19, 2026*
|
||||||
*Total automated tests collected: 1777*
|
*Total automated tests collected: 2107*
|
||||||
*Regression gate: tests/test_regressions.py*
|
*Regression gate: tests/test_regressions.py*
|
||||||
*Run: pytest tests/ -v --timeout=60*
|
*Run: pytest tests/ -v --timeout=60*
|
||||||
*Source: <repo>/*
|
*Source: <repo>/*
|
||||||
+1
-1
@@ -113,7 +113,7 @@ def discover_launcher_python(agent_dir: Path | None) -> str:
|
|||||||
if env_python:
|
if env_python:
|
||||||
return env_python
|
return env_python
|
||||||
if agent_dir:
|
if agent_dir:
|
||||||
for rel in ("venv/bin/python", "venv/Scripts/python.exe"):
|
for rel in ("venv/bin/python", "venv/Scripts/python.exe", ".venv/bin/python", ".venv/Scripts/python.exe"):
|
||||||
candidate = agent_dir / rel
|
candidate = agent_dir / rel
|
||||||
if candidate.exists():
|
if candidate.exists():
|
||||||
return str(candidate)
|
return str(candidate)
|
||||||
|
|||||||
+1
-1
@@ -654,7 +654,7 @@ function cmdReasoning(args){
|
|||||||
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:arg})})
|
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:arg})})
|
||||||
.then(function(st){
|
.then(function(st){
|
||||||
const eff=(st && st.reasoning_effort)||arg;
|
const eff=(st && st.reasoning_effort)||arg;
|
||||||
showToast('Reasoning effort set to '+eff+' (saved; applies to next turn)');
|
showToast(BRAIN+' Reasoning effort: '+eff+' (saved; applies to next turn)');
|
||||||
if(typeof _applyReasoningChip==='function') _applyReasoningChip(eff);
|
if(typeof _applyReasoningChip==='function') _applyReasoningChip(eff);
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
|
|||||||
+4
-4
@@ -1363,13 +1363,13 @@ function attachBtwStream(parentSid, streamId, question){
|
|||||||
if(ansEl) ansEl.innerHTML=renderMd(answer);
|
if(ansEl) ansEl.innerHTML=renderMd(answer);
|
||||||
});
|
});
|
||||||
src.addEventListener('done',e=>{
|
src.addEventListener('done',e=>{
|
||||||
src.close();
|
|
||||||
_streamDone=true;
|
_streamDone=true;
|
||||||
|
src.close();
|
||||||
try{
|
try{
|
||||||
const d=JSON.parse(e.data);
|
const d=JSON.parse(e.data);
|
||||||
if(d.answer&&!answer) answer=d.answer;
|
if(d.answer&&!answer) answer=d.answer;
|
||||||
}catch(_){}
|
}catch(_){}
|
||||||
_ensureBtwRow();
|
if(S.session&&S.session.session_id===parentSid) _ensureBtwRow();
|
||||||
if(btwRow&&btwRow.isConnected){
|
if(btwRow&&btwRow.isConnected){
|
||||||
const ansEl=btwRow.querySelector('.msg-btw-answer');
|
const ansEl=btwRow.querySelector('.msg-btw-answer');
|
||||||
if(ansEl) ansEl.innerHTML=renderMd(answer||t('btw_no_answer'));
|
if(ansEl) ansEl.innerHTML=renderMd(answer||t('btw_no_answer'));
|
||||||
@@ -1377,15 +1377,15 @@ function attachBtwStream(parentSid, streamId, question){
|
|||||||
showToast(t('btw_done'));
|
showToast(t('btw_done'));
|
||||||
});
|
});
|
||||||
src.addEventListener('apperror',e=>{
|
src.addEventListener('apperror',e=>{
|
||||||
src.close();
|
|
||||||
_streamDone=true;
|
_streamDone=true;
|
||||||
|
src.close();
|
||||||
try{
|
try{
|
||||||
const d=JSON.parse(e.data);
|
const d=JSON.parse(e.data);
|
||||||
showToast(t('btw_failed')+(d.message||''));
|
showToast(t('btw_failed')+(d.message||''));
|
||||||
}catch(_){showToast(t('btw_failed'));}
|
}catch(_){showToast(t('btw_failed'));}
|
||||||
if(btwRow&&btwRow.isConnected) btwRow.remove();
|
if(btwRow&&btwRow.isConnected) btwRow.remove();
|
||||||
});
|
});
|
||||||
src.addEventListener('stream_end',()=>{src.close();});
|
src.addEventListener('stream_end',()=>{_streamDone=true;src.close();});
|
||||||
src.onerror=()=>{src.close();if(!_streamDone&&btwRow&&btwRow.isConnected) btwRow.remove();};
|
src.onerror=()=>{src.close();if(!_streamDone&&btwRow&&btwRow.isConnected) btwRow.remove();};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,85 @@ class TestBtwStreamDoneGuard:
|
|||||||
"even if no token events arrived before done"
|
"even if no token events arrived before done"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_ensure_btw_row_gated_on_session_match(self):
|
||||||
|
"""Regression for PR #935: _ensureBtwRow reads $('msgInner') — the
|
||||||
|
CURRENTLY-viewed session's container. If the user switched sessions
|
||||||
|
during the /btw stream, creating a bubble here would put it in the
|
||||||
|
wrong session. The done handler must guard on S.session.session_id
|
||||||
|
matching the parent sid before creating a new bubble.
|
||||||
|
"""
|
||||||
|
fn = self.get_attach_btw()
|
||||||
|
done_block_m = re.search(
|
||||||
|
r"addEventListener\('done'[\s\S]*?(?=addEventListener\(')",
|
||||||
|
fn,
|
||||||
|
)
|
||||||
|
assert done_block_m
|
||||||
|
block = done_block_m.group(0)
|
||||||
|
# The _ensureBtwRow call must be guarded by a session-match check
|
||||||
|
assert ("S.session" in block and "parentSid" in block), (
|
||||||
|
"_ensureBtwRow() in the done handler must be gated on "
|
||||||
|
"S.session.session_id === parentSid — otherwise a user who "
|
||||||
|
"switched sessions during the /btw stream gets the answer "
|
||||||
|
"bubble injected into the wrong session's container"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stream_done_set_before_close_in_done(self):
|
||||||
|
"""Regression for PR #935 defensive ordering: _streamDone=true must be
|
||||||
|
set BEFORE src.close() in the done handler. Setting it after works
|
||||||
|
today because EventSource.close() is synchronous per spec and doesn't
|
||||||
|
dispatch events, but the defensive-correct ordering is flag-first so
|
||||||
|
no future browser quirk or event-queue race can bypass the guard.
|
||||||
|
"""
|
||||||
|
fn = self.get_attach_btw()
|
||||||
|
done_block_m = re.search(
|
||||||
|
r"addEventListener\('done'[\s\S]*?(?=addEventListener\(')",
|
||||||
|
fn,
|
||||||
|
)
|
||||||
|
assert done_block_m
|
||||||
|
block = done_block_m.group(0)
|
||||||
|
flag_pos = block.find("_streamDone=true")
|
||||||
|
close_pos = block.find("src.close()")
|
||||||
|
assert flag_pos > -1 and close_pos > -1
|
||||||
|
assert flag_pos < close_pos, (
|
||||||
|
"_streamDone=true must be set BEFORE src.close() in the done handler "
|
||||||
|
"so any event the browser fires during close() sees the flag already set"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stream_done_set_before_close_in_apperror(self):
|
||||||
|
"""Same defensive ordering as test_stream_done_set_before_close_in_done,
|
||||||
|
applied to the apperror handler."""
|
||||||
|
fn = self.get_attach_btw()
|
||||||
|
apperror_m = re.search(
|
||||||
|
r"addEventListener\('apperror'[\s\S]*?(?=addEventListener\(')",
|
||||||
|
fn,
|
||||||
|
)
|
||||||
|
assert apperror_m
|
||||||
|
block = apperror_m.group(0)
|
||||||
|
flag_pos = block.find("_streamDone=true")
|
||||||
|
close_pos = block.find("src.close()")
|
||||||
|
assert flag_pos > -1 and close_pos > -1, (
|
||||||
|
"apperror handler must both set _streamDone=true and call src.close()"
|
||||||
|
)
|
||||||
|
assert flag_pos < close_pos, (
|
||||||
|
"_streamDone=true must be set BEFORE src.close() in the apperror handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stream_end_sets_stream_done(self):
|
||||||
|
"""Regression for PR #935/#939/#942: stream_end handler must also set
|
||||||
|
_streamDone=true. Today the ephemeral /btw path returns before emitting
|
||||||
|
stream_end so this is moot, but if the server emits stream_end as a
|
||||||
|
standalone terminator (e.g. for a non-ephemeral /btw variant), the
|
||||||
|
subsequent browser-fired onerror would wipe the bubble without the flag.
|
||||||
|
"""
|
||||||
|
fn = self.get_attach_btw()
|
||||||
|
m = re.search(r"addEventListener\('stream_end'[\s\S]*?\}\s*\)", fn)
|
||||||
|
assert m, "stream_end handler not found"
|
||||||
|
block = m.group(0)
|
||||||
|
assert "_streamDone=true" in block or "_streamDone = true" in block, (
|
||||||
|
"stream_end handler must set _streamDone=true before closing the "
|
||||||
|
"connection — consistent with the done/apperror handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── #5 resize handler symmetry (non-blocking polish) ─────────────────────────
|
# ── #5 resize handler symmetry (non-blocking polish) ─────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user