diff --git a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.spec.ts index d953157a9..679ad27c1 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.spec.ts @@ -640,6 +640,35 @@ describe('embedded-mpv-frame-copy-runtime', () => { expect(fileSystem?.readFileSync).toHaveBeenCalled(); }); + it('invalidates a cached result when helper or manifest bytes change under identical stats', () => { + const fixture = createFixture(rootDir); + const fixedStats = new Map([ + [fixture.helperPath, lstatSync(fixture.helperPath)], + [fixture.manifestPath, lstatSync(fixture.manifestPath)], + ]); + fileSystem = { + ...fileSystem, + lstatSync: jest.fn( + (filePath: string) => + fixedStats.get(filePath) ?? lstatSync(filePath) + ), + }; + const probeRuntime = createProbe(); + + expect(probeRuntime(fixture.helperPath).usable).toBe(true); + + const changedHelper = readFileSync(fixture.helperPath); + changedHelper[0] ^= 0xff; + writeFileSync(fixture.helperPath, changedHelper); + expect(probeRuntime(fixture.helperPath).usable).toBe(true); + + fixture.manifest.generatedAt = '2026-07-18T00:00:00.000Z'; + writeManifest(fixture.manifestPath, fixture.manifest); + expect(probeRuntime(fixture.helperPath).usable).toBe(true); + + expect(spawnRuntimeProbe).toHaveBeenCalledTimes(3); + }); + it.each(['portable', 'flatpak'] as const)( 'validates the exact %s bundled closure and uses only its private library directory', (profile) => { diff --git a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.ts b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.ts index 297abd2c7..5722fdc79 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.ts @@ -438,7 +438,11 @@ function isMissingFileError(error: unknown): boolean { ); } -function fileIdentity(filePath: string, stat: nodeFileSystem.Stats): string { +function fileIdentity( + filePath: string, + stat: nodeFileSystem.Stats, + contents: Buffer +): string { return [ path.resolve(filePath), stat.dev, @@ -447,6 +451,7 @@ function fileIdentity(filePath: string, stat: nodeFileSystem.Stats): string { stat.size, stat.mtimeMs, stat.ctimeMs, + createHash('sha256').update(contents).digest('hex'), ].join(':'); } @@ -1546,10 +1551,25 @@ export function createEmbeddedMpvFrameCopyRuntimeProbe( ); } + let helperContents: Buffer; + let manifestContents: Buffer; + try { + helperContents = dependencies.fileSystem.readFileSync(helperPath); + } catch { + return failure('runtime-artifact-invalid'); + } + try { + manifestContents = + dependencies.fileSystem.readFileSync(manifestPath); + } catch { + return failure('runtime-manifest-invalid'); + } + const cacheKey = `${manifestContract}\0${fileIdentity( helperPath, - helperStat - )}\0${fileIdentity(manifestPath, manifestStat)}`; + helperStat, + helperContents + )}\0${fileIdentity(manifestPath, manifestStat, manifestContents)}`; const cached = resultCache.get(cacheKey); if (cached) { return cached;