fix(desktop): bridge the already-decoded media stream path once
The hermes-media://stream resolveLocalFile boundary ran
resolveMediaRequestPath on the path the protocol handler had already
percent-decoded and slash-stripped (parseMediaProtocolTarget), so the
second decode+strip turned every absolute POSIX media path
(/home/.../.hermes/...) into a cwd-relative one. The resolver then
pinned it under the app's cwd and the file ENOENTed into the handler's
silent 404 — inline TTS/audio playback dead with no log line, which is
the Linux symptom in hermes-agent 123823. A second decodeURIComponent
also threw URIError on filenames containing a literal %.
Restore the single decode at the protocol layer: the boundary now only
bridges (resolveMediaStreamFile), matching what 7722761581 intended as
byte-for-byte behavior but broke when it rewired main.ts.
Fixes https://github.com/NousResearch/hermes-agent/issues/123823
This commit is contained in:
committed by
brooklyn!
parent
ba5e3bfa31
commit
e9c60209a7
@@ -10,7 +10,11 @@ const { bridge } = vi.hoisted(() => ({ bridge: vi.fn() }))
|
||||
|
||||
vi.mock('./wsl-path-bridge', () => ({ resolveLocalReadPath: bridge }))
|
||||
|
||||
import { resolveIpcFileReadPath, resolveMediaRequestPath, resolvePreviewTargetPath } from './local-read-path'
|
||||
import {
|
||||
resolveIpcFileReadPath,
|
||||
resolveMediaStreamFile,
|
||||
resolvePreviewTargetPath
|
||||
} from './local-read-path'
|
||||
|
||||
const BRIDGED = '\\\\wsl.localhost\\Ubuntu\\home\\alex\\file'
|
||||
|
||||
@@ -19,26 +23,44 @@ beforeEach(() => {
|
||||
bridge.mockImplementation(() => BRIDGED)
|
||||
})
|
||||
|
||||
test('resolveMediaRequestPath (hermes-media:// handler) bridges the decoded request pathname', () => {
|
||||
// Mirror how the renderer builds the URL: hermes-media://stream/<encoded path>.
|
||||
const url = new URL(`hermes-media://stream/${encodeURIComponent('/home/alex/My Clips/clip.mp4')}`)
|
||||
|
||||
const result = resolveMediaRequestPath(url.pathname)
|
||||
// Regression for hermes-agent 123823: the media protocol handler decodes the
|
||||
// request pathname itself (parseMediaProtocolTarget in media-protocol.ts), so
|
||||
// the resolveLocalFile boundary must NOT decode or strip leading slashes again.
|
||||
// The pre-fix wiring ran decodeURIComponent + strip on the already-decoded
|
||||
// path, turning `/home/...` into a cwd-relative `home/...` that ENOENTs into
|
||||
// the handler's silent 404, and threw URIError on filenames with a literal `%`.
|
||||
test('resolveMediaStreamFile (hermes-media:// resolveLocalFile) bridges the already-decoded path unchanged', () => {
|
||||
const result = resolveMediaStreamFile('/home/alex/My Clips/clip.mp4')
|
||||
|
||||
assert.equal(bridge.mock.calls.length, 1)
|
||||
assert.equal(bridge.mock.calls[0][0], '/home/alex/My Clips/clip.mp4')
|
||||
assert.equal(result, BRIDGED)
|
||||
})
|
||||
|
||||
test('resolveMediaRequestPath strips leading slashes before decoding and bridging', () => {
|
||||
resolveMediaRequestPath(`///${encodeURIComponent('/mnt/c/Users/alex/clip.mp4')}`)
|
||||
test('resolveMediaStreamFile preserves a leading slash so absolute POSIX paths stay absolute', () => {
|
||||
// What the protocol handler hands over for hermes-media://stream/%2Fhome%2F...:
|
||||
// already decoded, still absolute. Stripping the leading slash here is the
|
||||
// 123823 regression (cwd-relative ENOENT -> silent 404 on Linux/macOS).
|
||||
const url = new URL(`hermes-media://stream/${encodeURIComponent('/home/alex/clip.mp4')}`)
|
||||
const filePath = decodeURIComponent(url.pathname.replace(/^\/+/, ''))
|
||||
|
||||
resolveMediaStreamFile(filePath)
|
||||
|
||||
assert.equal(bridge.mock.calls.length, 1)
|
||||
assert.equal(bridge.mock.calls[0][0], '/mnt/c/Users/alex/clip.mp4')
|
||||
assert.equal(bridge.mock.calls[0][0], '/home/alex/clip.mp4')
|
||||
})
|
||||
|
||||
test('resolveMediaRequestPath tolerates nullish pathname', () => {
|
||||
const result = resolveMediaRequestPath(undefined as unknown as string)
|
||||
test('resolveMediaStreamFile keeps percent-sign bytes in filenames intact', () => {
|
||||
// A filename containing a literal `%` (e.g. "100% clip.mp4") arrives
|
||||
// decoded; a second decodeURIComponent would throw URIError.
|
||||
resolveMediaStreamFile('/home/alex/100% clip.mp4')
|
||||
|
||||
assert.equal(bridge.mock.calls.length, 1)
|
||||
assert.equal(bridge.mock.calls[0][0], '/home/alex/100% clip.mp4')
|
||||
})
|
||||
|
||||
test('resolveMediaStreamFile tolerates nullish input', () => {
|
||||
const result = resolveMediaStreamFile(undefined as unknown as string)
|
||||
|
||||
assert.equal(bridge.mock.calls.length, 1)
|
||||
assert.equal(bridge.mock.calls[0][0], '')
|
||||
|
||||
@@ -9,11 +9,16 @@ import { resolveLocalReadPath } from './wsl-path-bridge'
|
||||
// so the wiring is exercised in isolation instead of buried in main.ts.
|
||||
|
||||
/**
|
||||
* hermes-media:// stream handler: the request `pathname` (leading slashes plus
|
||||
* percent-encoding) → a bridged fs path.
|
||||
* hermes-media:// stream handler's `resolveLocalFile` dependency: the protocol
|
||||
* handler already percent-decoded the request pathname into `filePath`
|
||||
* (`parseMediaProtocolTarget` in media-protocol.ts), so this boundary only
|
||||
* bridges. Decoding or stripping leading slashes again would turn an absolute
|
||||
* POSIX path (`/home/...`) into a cwd-relative one that ENOENTs into the
|
||||
* handler's silent 404, and a second `decodeURIComponent` throws on filenames
|
||||
* containing a literal `%`.
|
||||
*/
|
||||
export function resolveMediaRequestPath(pathname: string): string {
|
||||
return resolveLocalReadPath(decodeURIComponent(String(pathname ?? '').replace(/^\/+/, '')))
|
||||
export function resolveMediaStreamFile(filePath: unknown): string {
|
||||
return resolveLocalReadPath(String(filePath ?? ''))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -315,7 +315,7 @@ import { CHROMIUM_LOG_FILENAME, enableLinuxCrashDiagnostics, linuxCrashDiagnosti
|
||||
import { notifyLauncherWindowRevealed } from './linux-launcher-ready'
|
||||
import { decideNvidiaEglFallback, parseNvidiaDriverMajor } from './linux-nvidia-egl-fallback'
|
||||
import { createLocalBackendLifecycle, waitForTeardown } from './local-backend-lifecycle'
|
||||
import { resolveIpcFileReadPath, resolveMediaRequestPath, resolvePreviewTargetPath } from './local-read-path'
|
||||
import { resolveIpcFileReadPath, resolveMediaStreamFile, resolvePreviewTargetPath } from './local-read-path'
|
||||
import { localSkinProfileKey, readLocalSkinPayload } from './local-skin'
|
||||
import { ACTIVE_LOG_POLL_MS, planLogRotation, reclaimActiveLogIfOversized } from './log-rotation'
|
||||
import { registerMachineProfile } from './machine-profile'
|
||||
@@ -1543,7 +1543,9 @@ function registerMediaProtocol(): void {
|
||||
// On a Windows host with a WSL backend the media path arrives as a
|
||||
// WSL/POSIX path (`/home/...`, `/mnt/c/...`) the Windows fs can't open
|
||||
// as-is; bridge it to a UNC/drive form first, same as directory reads.
|
||||
const { resolvedPath } = await resolveReadableFileForIpc(resolveMediaRequestPath(filePath), {
|
||||
// The protocol handler already percent-decoded the pathname, so this
|
||||
// boundary bridges only — re-decoding/stripping would corrupt the path.
|
||||
const { resolvedPath } = await resolveReadableFileForIpc(resolveMediaStreamFile(filePath), {
|
||||
purpose: 'Media stream'
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user