feat(server): add regression test for handling empty request bodies

feat(terminal): focus terminal on click
fix(vite): update API proxy path to avoid frontend route conflicts
This commit is contained in:
catlog22
2026-02-25 09:59:54 +08:00
parent 6c16c121d2
commit 45c61186c4
4 changed files with 123 additions and 4 deletions

View File

@@ -107,6 +107,11 @@ export function TerminalInstance({ sessionId, className, onRevealPath }: Termina
const projectPathRef = useRef<string | null>(projectPath);
projectPathRef.current = projectPath;
// Focus terminal when clicked
const handleTerminalClick = useCallback(() => {
xtermRef.current?.focus();
}, []);
const handleArtifactClick = useCallback((path: string) => {
const resolved = resolveArtifactPath(path, projectPathRef.current);
navigator.clipboard.writeText(resolved).catch((err) => {
@@ -313,7 +318,7 @@ export function TerminalInstance({ sessionId, className, onRevealPath }: Termina
))}
</div>
)}
<div ref={terminalHostRef} className="h-full w-full bg-black/90" />
<div ref={terminalHostRef} className="h-full w-full bg-black/90" onClick={handleTerminalClick} />
</div>
);
}

View File

@@ -41,7 +41,8 @@ export default defineConfig({
strictPort: true,
proxy: {
// Backend API proxy
'/api': {
// Use `/api/` (not `/api`) to avoid accidentally proxying frontend routes like `/api-settings`.
'/api/': {
target: backendHttpTarget,
changeOrigin: true,
},

View File

@@ -114,7 +114,8 @@ function handlePostRequest(req: http.IncomingMessage, res: http.ServerResponse,
if (typeof cachedRawBody === 'string') {
try {
void handleBody(JSON.parse(cachedRawBody));
const trimmed = cachedRawBody.trim();
void handleBody(trimmed.length === 0 ? {} : JSON.parse(cachedRawBody));
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -128,7 +129,8 @@ function handlePostRequest(req: http.IncomingMessage, res: http.ServerResponse,
req.on('end', async () => {
try {
(req as any).__ccwRawBody = body;
const parsed = JSON.parse(body);
const trimmed = body.trim();
const parsed = trimmed.length === 0 ? {} : JSON.parse(body);
(req as any).body = parsed;
await handleBody(parsed);
} catch (error: unknown) {

View File

@@ -0,0 +1,111 @@
/**
* Regression test: handlePostRequest should tolerate empty request bodies.
*
* Background:
* - Several endpoints use POST with no JSON payload (e.g. "install"/"uninstall" actions).
* - The server's handlePostRequest previously called JSON.parse(''), throwing:
* "Unexpected end of JSON input".
*
* This test exercises a safe no-body endpoint:
* POST /api/mcp/apply-windows-fix
*/
import { after, before, describe, it, mock } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
function httpRequest(options, body, timeout = 10000) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => resolve({
status: res.statusCode || 0,
body: data,
headers: res.headers,
}));
});
req.on('error', reject);
req.setTimeout(timeout, () => {
req.destroy();
reject(new Error('Request timeout'));
});
if (body) req.write(body);
req.end();
});
}
const ORIGINAL_ENV = { ...process.env };
const serverUrl = new URL('../dist/core/server.js', import.meta.url);
serverUrl.searchParams.set('t', String(Date.now()));
describe('handlePostRequest (empty body)', async () => {
let server;
let port;
let projectRoot;
let ccwHome;
before(async () => {
projectRoot = mkdtempSync(join(tmpdir(), 'ccw-empty-body-project-'));
ccwHome = mkdtempSync(join(tmpdir(), 'ccw-empty-body-home-'));
process.env = { ...ORIGINAL_ENV, CCW_DATA_DIR: ccwHome };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serverMod = await import(serverUrl.href);
mock.method(console, 'log', () => {});
mock.method(console, 'error', () => {});
server = await serverMod.startServer({ initialPath: projectRoot, port: 0 });
const addr = server.address();
port = typeof addr === 'object' && addr ? addr.port : 0;
assert.ok(port > 0, 'Server should start on a valid port');
});
after(async () => {
await new Promise((resolve) => server.close(() => resolve()));
mock.restoreAll();
process.env = ORIGINAL_ENV;
rmSync(projectRoot, { recursive: true, force: true });
rmSync(ccwHome, { recursive: true, force: true });
});
it('accepts POST routes with no body', async () => {
const tokenRes = await httpRequest({
hostname: '127.0.0.1',
port,
path: '/api/auth/token',
method: 'GET',
});
assert.equal(tokenRes.status, 200);
const { token } = JSON.parse(tokenRes.body);
assert.ok(typeof token === 'string' && token.length > 0);
const response = await httpRequest({
hostname: '127.0.0.1',
port,
path: '/api/mcp/apply-windows-fix',
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
// Make it explicit: empty body
'Content-Length': '0',
},
});
assert.equal(response.status, 200);
assert.doesNotMatch(response.body, /Unexpected end of JSON input/);
const payload = JSON.parse(response.body);
assert.equal(payload.success, false);
assert.equal(
payload.message,
'Auto-fix is not supported. Please install missing commands manually.',
);
});
});