mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-27 09:13:07 +08:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
111
ccw/tests/handle-post-request-empty-body.test.js
Normal file
111
ccw/tests/handle-post-request-empty-body.test.js
Normal 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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user