diff --git a/src/index.ts b/src/index.ts index f5362ab..72753de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -866,7 +866,7 @@ export function apply(ctx: Context, config?: SidebarConfig): void { const type = mediaTypeForPath(absolute) const body = await readFile(absolute) res.writeHead(200, { - 'content-type': type, + 'content-type': type === 'text/html' ? 'text/html; charset=utf-8' : type, 'cache-control': 'no-cache', 'x-content-type-options': 'nosniff', 'referrer-policy': 'no-referrer', diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts index 5cabab8..c0806f8 100644 --- a/tests/smoke.spec.ts +++ b/tests/smoke.spec.ts @@ -10,6 +10,7 @@ import { tmpdir } from 'node:os' import { join, resolve as resolvePath } from 'node:path' import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-settings' import { apply, mediaTypeForPath } from '../src/index.ts' +import { encodeHtmlUrl } from '../src/html-route.ts' import * as git from '../src/git.ts' import { listDirectory } from '../src/fs-tree.ts' import { encodeHtmlUrl } from '../src/html-route.ts' @@ -115,6 +116,58 @@ describe('host plugin smoke', () => { for (const cleanup of effects) cleanup() }) + it('serves HTML previews as UTF-8 without changing the file bytes', async () => { + const directory = mkdtempSync(join(tmpdir(), 'dsh-sidebar-html-utf8-')) + const path = join(directory, 'fragment.html') + const source = Buffer.from('
排序算法可视化
', 'utf8') + writeFileSync(path, source) + const routes: SidebarWebRoute[] = [] + const effects: Array<() => void | (() => void)> = [] + const ctx: FakeContext = { + webRuntime: { trustedHosts: [] }, + webServer: { + register: (route) => { routes.push(route); return () => {} }, + registerUpgrade: () => () => {}, + }, + sessions: { get: () => ({ header: { cwd: directory } }) }, + tools: { register: () => () => {} }, + effect: (fn) => { + const cleanup = fn() + if (typeof cleanup === 'function') effects.push(cleanup) + }, + inject: () => () => {}, + get: () => undefined, + } + try { + apply(ctx as never) + const route = routes.find(candidate => candidate.path === '/sidebar/html')! + const req = { + method: 'GET', + url: encodeHtmlUrl('s-html', path), + headers: { host: '127.0.0.1:3080' }, + } as never + const response: { status?: number; headers?: Record; chunks: Buffer[] } = { chunks: [] } + const res = { + writeHead: (status: number, headers?: Record) => { + response.status = status + response.headers = headers + }, + end: (chunk?: string | Buffer) => { + if (chunk !== undefined) response.chunks.push(Buffer.from(chunk)) + }, + } as never + + await route.handler(req, res) + + expect(response.status).toBe(200) + expect(Buffer.concat(response.chunks)).toEqual(source) + expect(response.headers?.['content-type']).toBe('text/html; charset=utf-8') + } finally { + for (const cleanup of effects) cleanup() + rmSync(directory, { recursive: true, force: true }) + } + }) + it('runs git status/log/branches against this repository', async () => { const cwd = process.cwd() const status = await git.status(cwd)