fix(remote): stop the wake handlers shadowing each other; enforce the input cap

Two findings from a final review pass over the wake-on-LAN feature.

`_onRemoteHostWaking` / `_onRemoteHostWakeFailed` were defined in BOTH
`panels-ui.js` (toasts) and `host-wake-ui.js` (banner). Both files mix into
`CodemanApp.prototype` and `host-wake-ui.js` loads later, so the panels-ui copies
were silently shadowed: the toast never fired, and a wake started for a BACKGROUND
session (input on a non-active tab) produced no notification at all, since the
banner handler only acts on the active session. The handlers now live only in
`host-wake-ui.js`, show the toast unconditionally, and update the banner when the
woken session is the active one.

`appendBoundedPending` dropped only WHOLE chunks, so a single input value over the
cap (one large paste is one `input` value, up to the 100 KB input schema) was kept
in full: "bounded at 4 KB" held per chunk, not per session, and nothing was logged.
The surviving chunk's head is now trimmed too, code-point aware so a multi-byte
character is never split into a replacement char.

Adds the guard that would have caught the first one: every SSE dispatch handler must
be defined in exactly ONE frontend module. The existing test only asserts a handler
EXISTS somewhere, which two modules both satisfy while one is shadowed.
This commit is contained in:
Randalix
2026-09-15 21:01:53 +02:00
parent 1380b023e2
commit 8dfc965d13
6 changed files with 81 additions and 17 deletions
+14 -2
View File
@@ -76,9 +76,21 @@ describe('appendBoundedPending', () => {
expect(appendBoundedPending([big], 'newest')).toEqual(['newest']);
});
it('never drops the just-typed chunk even when it alone exceeds the cap', () => {
it('trims a single oversized chunk to the cap, keeping its TAIL', () => {
// One large paste is one input value, so the cap has to hold WITHIN a chunk too
// (otherwise "bounded at 4 KB" would only be true per chunk, not per session).
const huge = 'y'.repeat(REMOTE_WAKE_PENDING_MAX_BYTES + 100);
expect(appendBoundedPending([], huge)).toEqual([huge]);
const result = appendBoundedPending([], huge);
expect(result).toEqual(['y'.repeat(REMOTE_WAKE_PENDING_MAX_BYTES)]);
expect(result[0].length).toBe(REMOTE_WAKE_PENDING_MAX_BYTES);
});
it('trims a multi-byte tail without splitting a character', () => {
const cap = 10;
const value = 'ä'.repeat(8); // 2 bytes each → 16 bytes
const result = appendBoundedPending([], value, cap);
expect(Buffer.byteLength(result[0])).toBeLessThanOrEqual(cap);
expect(result[0]).toBe('ä'.repeat(5)); // 10 bytes, no U+FFFD
});
});
+18
View File
@@ -57,4 +57,22 @@ describe('SSE dispatch table', () => {
.filter((handler) => !new RegExp(`(^|\\s)${handler}\\s*\\(`, 'm').test(allModules));
expect(missing).toEqual([]);
});
it('defines every handler in exactly ONE module (a second copy is shadowed)', () => {
// Modules mix into `CodemanApp.prototype` and run in script order, so two
// definitions of the same handler name silently shadow each other: the later file
// wins and the earlier one never runs. The existence check above cannot see that
// (both names resolve), which is how a duplicate banner handler can leave a toast
// dead with no error anywhere.
const byModule = readdirSync(PUBLIC_DIR)
.filter((name) => name.endsWith('.js'))
.map((name) => ({ name, source: readFileSync(join(PUBLIC_DIR, name), 'utf-8') }));
const shadowed = dispatchEntries()
.map((entry) => entry.handler)
.filter((handler) => {
const re = new RegExp(`(^|\\s)${handler}\\s*\\(`, 'm');
return byModule.filter((mod) => re.test(mod.source)).length > 1;
});
expect(shadowed).toEqual([]);
});
});