mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(remote): keep the wake banner hidden after switching to a local session
refreshHostWakeBanner clears _hostWake before calling _hostWakeTick, so the clear branch's `if (this._hostWake)` guard skipped the repaint: once the banner had appeared for an unreachable remote session it stayed up on every chat (local ones included) until a reload, and the 30s ticker never cleared it either. Render unconditionally in that branch — _renderHostWakeBanner is idempotent with a null state. Reproduced in a real browser (Puppeteer, mobile viewport): state went null but banner.hidden stayed false. Regression test added in test/host-wake-banner.test.ts (red before, green after).
This commit is contained in:
@@ -85,10 +85,12 @@ Object.assign(CodemanApp.prototype, {
|
||||
const sessionId = this.activeSessionId;
|
||||
const session = sessionId && this.sessions ? this.sessions.get(sessionId) : null;
|
||||
if (!sessionId || !session || !session.remote) {
|
||||
if (this._hostWake) {
|
||||
this._hostWake = null;
|
||||
this._renderHostWakeBanner();
|
||||
}
|
||||
// Render unconditionally: `refreshHostWakeBanner` clears `_hostWake` BEFORE
|
||||
// calling this tick, so a guard here would skip the repaint and leave the
|
||||
// banner up on every chat (the clear and the repaint must not be coupled to
|
||||
// whoever cleared the state). Idempotent — with a null state it just hides.
|
||||
this._hostWake = null;
|
||||
this._renderHostWakeBanner();
|
||||
return;
|
||||
}
|
||||
let state = this._hostWake;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Port: none (pure frontend module in a node VM with a fake DOM — no browser, no server).
|
||||
//
|
||||
// The remote-host wake banner (src/web/public/host-wake-ui.js) is a SINGLE global
|
||||
// element that is shown only for the active remote session. The regression this
|
||||
// guards: `refreshHostWakeBanner` clears `_hostWake` when the tab switches, but
|
||||
// `_hostWakeTick`'s clear branch only re-rendered when IT was the one clearing —
|
||||
// so switching from an unreachable remote session to a LOCAL one left the banner
|
||||
// visible ("Hufflepuff is not reachable") on every chat until a full reload.
|
||||
//
|
||||
// The bug is a pure ordering problem between two methods, so it can be reproduced
|
||||
// here without a browser: render the remote state, switch to a local session, and
|
||||
// assert the banner is hidden again.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
||||
|
||||
const REMOTE_ID = 'remote-session-0001';
|
||||
const LOCAL_ID = 'local-session-0001';
|
||||
|
||||
type El = { hidden: boolean; textContent: string; disabled: boolean; classList: { add(): void; remove(): void } };
|
||||
|
||||
function fakeElement(): El {
|
||||
return { hidden: false, textContent: '', disabled: false, classList: { add() {}, remove() {} } };
|
||||
}
|
||||
|
||||
/** Load `host-wake-ui.js` with the minimal DOM it touches, and return a wired app. */
|
||||
function loadWakeApp() {
|
||||
const elements = new Map<string, El>([
|
||||
['hostWakeBanner', fakeElement()],
|
||||
['hostWakeBannerText', fakeElement()],
|
||||
['hostWakeBannerDetail', fakeElement()],
|
||||
['hostWakeBannerAction', fakeElement()],
|
||||
]);
|
||||
const CodemanApp = function CodemanApp(this: unknown) {};
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
console,
|
||||
setInterval: () => 1,
|
||||
clearInterval: () => {},
|
||||
fetch: () => Promise.resolve({ json: () => Promise.resolve({ success: false }) }),
|
||||
document: {
|
||||
visibilityState: 'visible',
|
||||
getElementById: (id: string) => elements.get(id) ?? null,
|
||||
addEventListener: () => {},
|
||||
},
|
||||
window: {},
|
||||
});
|
||||
vm.runInContext(readFileSync(resolve(PUBLIC, 'host-wake-ui.js'), 'utf8'), context, { filename: 'host-wake-ui.js' });
|
||||
|
||||
const app = new (CodemanApp as new () => Record<string, unknown>)();
|
||||
app.$ = (id: string) => elements.get(id) ?? null;
|
||||
app.activeSessionId = REMOTE_ID;
|
||||
app.sessions = new Map<string, { remote?: Record<string, unknown> }>([
|
||||
[
|
||||
REMOTE_ID,
|
||||
{ remote: { hostId: 'hufflepuff', host: '192.168.50.137', label: 'Hufflepuff', wakeMac: '04:d9:f5:80:c6:58' } },
|
||||
],
|
||||
[LOCAL_ID, {}],
|
||||
]);
|
||||
return { app, banner: elements.get('hostWakeBanner') as El, text: elements.get('hostWakeBannerText') as El };
|
||||
}
|
||||
|
||||
describe('host wake banner visibility', () => {
|
||||
it('hides the banner when switching from an unreachable remote session to a local one', () => {
|
||||
const { app, banner, text } = loadWakeApp();
|
||||
|
||||
// The banner is up for the active, unreachable remote session.
|
||||
app._hostWake = {
|
||||
sessionId: REMOTE_ID,
|
||||
reachable: false,
|
||||
wakeConfigured: 'mac',
|
||||
host: '192.168.50.137',
|
||||
label: 'Hufflepuff',
|
||||
waking: false,
|
||||
error: '',
|
||||
};
|
||||
(app._renderHostWakeBanner as () => void)();
|
||||
expect(banner.hidden).toBe(false);
|
||||
expect(text.textContent).toBe('Hufflepuff is not reachable');
|
||||
|
||||
// Switch to a LOCAL session. `refreshHostWakeBanner` clears the state, and the
|
||||
// tick that follows must still repaint the (now empty) banner as hidden.
|
||||
app.activeSessionId = LOCAL_ID;
|
||||
(app.refreshHostWakeBanner as (id: string) => void)(LOCAL_ID);
|
||||
|
||||
expect(app._hostWake).toBeNull();
|
||||
expect(banner.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the banner hidden on a later poller tick once the state is cleared', () => {
|
||||
const { app, banner } = loadWakeApp();
|
||||
app.activeSessionId = LOCAL_ID;
|
||||
app._hostWake = null;
|
||||
// A page-wide tick on a local session must be idempotent and leave it hidden.
|
||||
(app._hostWakeTick as () => void)();
|
||||
expect(banner.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the banner only while the active session is remote and unreachable', () => {
|
||||
const { app, banner } = loadWakeApp();
|
||||
app._hostWake = {
|
||||
sessionId: REMOTE_ID,
|
||||
reachable: false,
|
||||
wakeConfigured: 'mac',
|
||||
host: '192.168.50.137',
|
||||
label: 'Hufflepuff',
|
||||
waking: false,
|
||||
error: '',
|
||||
};
|
||||
(app._renderHostWakeBanner as () => void)();
|
||||
expect(banner.hidden).toBe(false);
|
||||
|
||||
// Reachable again → hidden, state intact (the banner must not leak across the
|
||||
// reachable/unreachable transition either).
|
||||
app._hostWake.reachable = true;
|
||||
(app._renderHostWakeBanner as () => void)();
|
||||
expect(banner.hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user