195 lines
6.8 KiB
JavaScript
195 lines
6.8 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const http = require('node:http');
|
|
const net = require('node:net');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawn } = require('node:child_process');
|
|
const WebSocket = require('ws');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SERVER = path.join(ROOT, 'server.js');
|
|
|
|
function freePort() {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const port = server.address().port;
|
|
server.close(() => resolve(port));
|
|
});
|
|
});
|
|
}
|
|
|
|
function waitForPort(port) {
|
|
const started = Date.now();
|
|
return new Promise((resolve, reject) => {
|
|
const check = () => {
|
|
const socket = net.createConnection({ host: '127.0.0.1', port });
|
|
socket.once('connect', () => { socket.destroy(); resolve(); });
|
|
socket.once('error', () => {
|
|
if (Date.now() - started > 8_000) reject(new Error('等待关闭态测试服务超时'));
|
|
else setTimeout(check, 40);
|
|
});
|
|
};
|
|
check();
|
|
});
|
|
}
|
|
|
|
function callMcp(port, token, sourceSessionId, method, params = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const body = JSON.stringify({ jsonrpc: '2.0', id: `disabled-${Date.now()}`, method, params });
|
|
const request = http.request({
|
|
host: '127.0.0.1',
|
|
port,
|
|
path: `/api/internal/mcp/stream?sourceSessionId=${encodeURIComponent(sourceSessionId)}`,
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'content-length': Buffer.byteLength(body),
|
|
'x-cc-web-mcp-token': token,
|
|
},
|
|
}, (response) => {
|
|
let text = '';
|
|
response.setEncoding('utf8');
|
|
response.on('data', (chunk) => { text += chunk; });
|
|
response.on('end', () => resolve({ status: response.statusCode, body: JSON.parse(text || '{}') }));
|
|
});
|
|
request.once('error', reject);
|
|
request.end(body);
|
|
});
|
|
}
|
|
|
|
function connect(port, password) {
|
|
return new Promise((resolve, reject) => {
|
|
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
const messages = [];
|
|
ws.on('open', () => ws.send(JSON.stringify({ type: 'auth', password })));
|
|
ws.on('message', (raw) => {
|
|
const message = JSON.parse(String(raw));
|
|
messages.push(message);
|
|
if (message.type === 'auth_result') {
|
|
if (message.success) resolve({ ws, messages, auth: message });
|
|
else reject(new Error('关闭态测试认证失败'));
|
|
}
|
|
});
|
|
ws.once('error', reject);
|
|
});
|
|
}
|
|
|
|
function next(messages, predicate) {
|
|
return new Promise((resolve, reject) => {
|
|
const started = Date.now();
|
|
const timer = setInterval(() => {
|
|
const index = messages.findIndex(predicate);
|
|
if (index >= 0) {
|
|
clearInterval(timer);
|
|
resolve(messages.splice(index, 1)[0]);
|
|
} else if (Date.now() - started > 5_000) {
|
|
clearInterval(timer);
|
|
reject(new Error(`关闭态 WS 响应超时: ${messages.map((item) => item.type).join(',')}`));
|
|
}
|
|
}, 25);
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-task-board-disabled-'));
|
|
const configDir = path.join(root, 'config');
|
|
const sessionsDir = path.join(root, 'sessions');
|
|
const logsDir = path.join(root, 'logs');
|
|
[configDir, sessionsDir, logsDir].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
|
|
const sessionId = 'disabled-existing-task';
|
|
const sessionPath = path.join(sessionsDir, `${sessionId}.json`);
|
|
const before = {
|
|
id: sessionId,
|
|
title: '重构前历史任务',
|
|
agent: 'codexapp',
|
|
cwd: root,
|
|
created: '2026-08-21T00:00:00.000Z',
|
|
updated: '2026-08-21T00:00:00.000Z',
|
|
messages: [],
|
|
taskTracking: {
|
|
enabled: true,
|
|
statusId: 'in_progress',
|
|
source: 'user',
|
|
version: 7,
|
|
archivedAt: '2026-08-20T23:59:00.000Z',
|
|
archivedBy: 'user',
|
|
},
|
|
};
|
|
fs.writeFileSync(sessionPath, JSON.stringify(before, null, 2));
|
|
const port = await freePort();
|
|
const password = 'Disabled!234';
|
|
const token = 'DisabledMcp!234';
|
|
const child = spawn(process.execPath, [SERVER], {
|
|
cwd: ROOT,
|
|
env: {
|
|
...process.env,
|
|
PORT: String(port),
|
|
CC_WEB_PASSWORD: password,
|
|
CC_WEB_INTERNAL_MCP_TOKEN: token,
|
|
CC_WEB_CONFIG_DIR: configDir,
|
|
CC_WEB_SESSIONS_DIR: sessionsDir,
|
|
CC_WEB_LOGS_DIR: logsDir,
|
|
CC_WEB_TASK_BOARD_ENABLED: '0',
|
|
HOME: root,
|
|
CODEX_PATH: path.join(ROOT, 'scripts', 'mock-codex-app-server.js'),
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
try {
|
|
await waitForPort(port);
|
|
const html = await (await fetch(`http://127.0.0.1:${port}/`)).text();
|
|
assert.match(html, /id="task-board-open"[^>]*hidden/);
|
|
assert.match(html, /id="task-tracking-control"[^>]*hidden/);
|
|
|
|
const tools = await callMcp(port, token, sessionId, 'tools/list');
|
|
assert.equal(tools.status, 200);
|
|
assert.deepEqual(
|
|
(tools.body.result?.tools || []).map((tool) => tool.name).filter((name) => name.startsWith('ccweb_task_')),
|
|
[],
|
|
);
|
|
const update = await callMcp(port, token, sessionId, 'tools/call', {
|
|
name: 'ccweb_task_update',
|
|
arguments: { statusId: 'completed' },
|
|
});
|
|
assert.equal(update.body.result?.structuredContent?.code, 'task_board_disabled');
|
|
|
|
const client = await connect(port, password);
|
|
client.ws.send(JSON.stringify({ type: 'task_board_query', requestId: 'disabled-query' }));
|
|
const query = await next(client.messages, (message) => message.type === 'task_board_result' && message.requestId === 'disabled-query');
|
|
assert.equal(query.code, 'task_board_disabled');
|
|
client.ws.send(JSON.stringify({ type: 'new_session', requestId: 'disabled-new', taskTrackingEnabled: true }));
|
|
const created = await next(client.messages, (message) => message.type === 'session_info' && message.requestId === 'disabled-new');
|
|
assert.equal(created.taskTracking.enabled, false);
|
|
|
|
client.ws.send(JSON.stringify({
|
|
type: 'message',
|
|
text: '关闭态普通轮次不得触发任务状态 Hook',
|
|
sessionId,
|
|
mode: 'yolo',
|
|
agent: 'codexapp',
|
|
}));
|
|
await next(client.messages, (message) => message.type === 'done' && message.sessionId === sessionId);
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
client.ws.close();
|
|
|
|
assert.deepEqual(JSON.parse(fs.readFileSync(sessionPath, 'utf8')).taskTracking, before.taskTracking);
|
|
assert.equal(client.auth.features.taskBoard, false);
|
|
console.log('Task board disabled checks passed.');
|
|
} finally {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill('SIGTERM');
|
|
await new Promise((resolve) => child.once('exit', resolve));
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|