feat: support conversation state events in JavaScript sessions
This commit is contained in:
@@ -33,6 +33,7 @@ async function main() {
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
|
||||
const sourceId = '11111111-1111-4111-8111-111111111111';
|
||||
const childId = '33333333-3333-4333-8333-333333333333';
|
||||
const sessions = new Map([[sourceId, {
|
||||
id: sourceId,
|
||||
cwd,
|
||||
@@ -40,6 +41,18 @@ async function main() {
|
||||
}]]);
|
||||
fs.writeFileSync(path.join(sessionsDir, `${sourceId}.json`), JSON.stringify(sessions.get(sourceId)));
|
||||
const notifications = [];
|
||||
let statusCallCount = 0;
|
||||
const statusSequence = [
|
||||
'idle',
|
||||
'idle',
|
||||
'running',
|
||||
'idle',
|
||||
'waiting_for_children',
|
||||
'idle',
|
||||
'idle',
|
||||
'idle',
|
||||
'idle',
|
||||
];
|
||||
|
||||
const mcpServer = http.createServer((req, res) => {
|
||||
let body = '';
|
||||
@@ -56,6 +69,17 @@ async function main() {
|
||||
case 'ccweb_script_get_last_message':
|
||||
response = { ok: true, conversationId: payload.args.conversationId, message: '服务端最后消息' };
|
||||
break;
|
||||
case 'ccweb_script_get_conversation_status':
|
||||
statusCallCount += 1;
|
||||
response = {
|
||||
ok: true,
|
||||
conversationId: payload.args.conversationId,
|
||||
status: statusSequence[Math.min(statusCallCount - 1, statusSequence.length - 1)],
|
||||
};
|
||||
break;
|
||||
case 'ccweb_script_get_child_conversation_ids':
|
||||
response = { ok: true, conversationId: payload.args.conversationId, childConversationIds: [childId] };
|
||||
break;
|
||||
default:
|
||||
response = { ok: false, code: 'unexpected_test_tool', message: payload.tool };
|
||||
}
|
||||
@@ -77,24 +101,41 @@ async function main() {
|
||||
assert.equal(runtime.getApiManifest().ok, true);
|
||||
assert.equal(runtime.getApiManifest().packageName, '@ccweb/session');
|
||||
const manifest = runtime.getApiManifest();
|
||||
assert.equal(manifest.functions.length, 5);
|
||||
assert.equal(manifest.functions.length, 8);
|
||||
assert.equal(manifest.functions.every((item) => item.description && Array.isArray(item.errors)), true);
|
||||
assert.deepEqual(manifest.statusValues, ['running', 'waiting_for_children', 'idle']);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(manifest, 'scriptTools'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(manifest, 'scriptToolExamples'), false);
|
||||
assert.match(packageJsonSource(), /"type": "module"/);
|
||||
assert.match(packageIndexSource(), /export async function sendMessage/);
|
||||
assert.match(packageIndexSource(), /export async function getConversationStatus/);
|
||||
assert.match(packageIndexSource(), /export async function getChildConversationIds/);
|
||||
assert.match(packageIndexSource(), /export async function onConversationEvent/);
|
||||
|
||||
assert.equal(runtime.createScript({ name: '../escape.js' }, sourceId).code, 'invalid_script_name');
|
||||
assert.equal(runtime.createScript({ name: 'workflow.txt' }, sourceId).code, 'invalid_script_name');
|
||||
assert.equal(runtime.createScript({ name: 'workflow.js' }, sourceId).ok, true);
|
||||
assert.equal(runtime.createScript({ name: 'workflow.js' }, sourceId).code, 'script_exists');
|
||||
const injectedPackageIndex = path.join(cwd, '.ccweb', 'scripts', 'node_modules', '@ccweb', 'session', 'index.js');
|
||||
fs.writeFileSync(injectedPackageIndex, '// stale generated package\n');
|
||||
|
||||
const workflow = [
|
||||
"import { getCurrentConversationId, getLastMessage } from '@ccweb/session';",
|
||||
'await new Promise((resolve) => setTimeout(resolve, 400));',
|
||||
'console.log(JSON.stringify({ current: await getCurrentConversationId(), last: await getLastMessage(await getCurrentConversationId()) }));',
|
||||
"import { getCurrentConversationId, getLastMessage, getConversationStatus, getChildConversationIds, onConversationEvent } from '@ccweb/session';",
|
||||
'const current = await getCurrentConversationId();',
|
||||
'const initialStatus = await getConversationStatus(current);',
|
||||
'const children = await getChildConversationIds(current);',
|
||||
'let unsubscribe = () => {};',
|
||||
'let resolveIdle;',
|
||||
'const idlePromise = new Promise((resolve) => { resolveIdle = resolve; });',
|
||||
"unsubscribe = await onConversationEvent(current, 'idle', (event) => { unsubscribe(); resolveIdle(event); });",
|
||||
'const idleEvent = await idlePromise;',
|
||||
'await new Promise((resolve) => setTimeout(resolve, 250));',
|
||||
"let invalidEventCode = '';",
|
||||
"try { await onConversationEvent(current, 'running', () => {}); } catch (error) { invalidEventCode = error.code; }",
|
||||
'console.log(JSON.stringify({ current, last: await getLastMessage(current), initialStatus, children, idleEvent, invalidEventCode }));',
|
||||
].join('\n');
|
||||
assert.equal(runtime.writeScript({ name: 'workflow.js', content: workflow }, sourceId).ok, true);
|
||||
assert.match(fs.readFileSync(injectedPackageIndex, 'utf8'), /export async function onConversationEvent/);
|
||||
assert.equal(runtime.writeScript({ name: 'too-large.js', content: 'x'.repeat(1024 * 1024 + 1) }, sourceId).code, 'script_content_too_large');
|
||||
|
||||
const started = runtime.runScript({ name: 'workflow.js' }, sourceId);
|
||||
@@ -105,6 +146,13 @@ async function main() {
|
||||
assert.equal(succeeded.exitCode, 0);
|
||||
assert.match(succeeded.stdout, new RegExp(sourceId));
|
||||
assert.match(succeeded.stdout, /服务端最后消息/);
|
||||
const workflowResult = JSON.parse(succeeded.stdout.trim());
|
||||
assert.equal(workflowResult.initialStatus, 'idle');
|
||||
assert.deepEqual(workflowResult.children, [childId]);
|
||||
assert.equal(workflowResult.idleEvent.previousStatus, 'waiting_for_children');
|
||||
assert.equal(workflowResult.idleEvent.status, 'idle');
|
||||
assert.equal(workflowResult.invalidEventCode, 'conversation_event_invalid');
|
||||
assert.equal(statusCallCount, statusSequence.length, '瞬时 idle 不应触发,注销后不应继续查询状态');
|
||||
assert.equal(succeeded.stderr, '');
|
||||
assert.equal(notifications.length, 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user