mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 08:25:26 +08:00
130 lines
4.2 KiB
JavaScript
130 lines
4.2 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const handler = require('./chat-stream');
|
|
const {
|
|
createToolSieveState,
|
|
processToolSieveChunk,
|
|
flushToolSieve,
|
|
} = require('./helpers/stream-tool-sieve');
|
|
|
|
const { parseChunkForContent } = handler.__test;
|
|
|
|
test('chat-stream exposes parser test hooks', () => {
|
|
assert.equal(typeof parseChunkForContent, 'function');
|
|
});
|
|
|
|
test('parseChunkForContent keeps split response/content fragments inside response array', () => {
|
|
const chunk = {
|
|
p: 'response',
|
|
v: [
|
|
{ p: 'response/content', v: '{"' },
|
|
{ p: 'response/content', v: 'tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}' },
|
|
],
|
|
};
|
|
const parsed = parseChunkForContent(chunk, false, 'text');
|
|
assert.equal(parsed.finished, false);
|
|
assert.equal(parsed.newType, 'text');
|
|
assert.equal(parsed.parts.length, 2);
|
|
const combined = parsed.parts.map((p) => p.text).join('');
|
|
assert.equal(combined, '{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}');
|
|
});
|
|
|
|
test('parseChunkForContent + sieve does not leak suspicious prefix in split tool json case', () => {
|
|
const chunk = {
|
|
p: 'response',
|
|
v: [
|
|
{ p: 'response/content', v: '{"' },
|
|
{ p: 'response/content', v: 'tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}' },
|
|
],
|
|
};
|
|
const parsed = parseChunkForContent(chunk, false, 'text');
|
|
const state = createToolSieveState();
|
|
const events = [];
|
|
for (const part of parsed.parts) {
|
|
events.push(...processToolSieveChunk(state, part.text, ['read_file']));
|
|
}
|
|
events.push(...flushToolSieve(state, ['read_file']));
|
|
|
|
const hasToolCalls = events.some((evt) => evt.type === 'tool_calls' && evt.calls && evt.calls.length > 0);
|
|
const hasToolDeltas = events.some((evt) => evt.type === 'tool_call_deltas' && evt.deltas && evt.deltas.length > 0);
|
|
const leakedText = events
|
|
.filter((evt) => evt.type === 'text' && evt.text)
|
|
.map((evt) => evt.text)
|
|
.join('');
|
|
|
|
assert.equal(hasToolCalls || hasToolDeltas, true);
|
|
assert.equal(leakedText.includes('{'), false);
|
|
assert.equal(leakedText.toLowerCase().includes('tool_calls'), false);
|
|
});
|
|
|
|
test('parseChunkForContent consumes nested item.v array payloads', () => {
|
|
const chunk = {
|
|
p: 'response',
|
|
v: [
|
|
{ p: 'response/content', v: ['A', 'B'] },
|
|
{ p: 'response/content', v: [{ content: 'C', type: 'RESPONSE' }] },
|
|
],
|
|
};
|
|
const parsed = parseChunkForContent(chunk, false, 'text');
|
|
assert.equal(parsed.finished, false);
|
|
assert.equal(parsed.parts.map((p) => p.text).join(''), 'ABC');
|
|
});
|
|
|
|
test('parseChunkForContent detects nested status FINISHED in array payload', () => {
|
|
const chunk = {
|
|
p: 'response',
|
|
v: [{ p: 'status', v: 'FINISHED' }],
|
|
};
|
|
const parsed = parseChunkForContent(chunk, false, 'text');
|
|
assert.equal(parsed.finished, true);
|
|
assert.deepEqual(parsed.parts, []);
|
|
});
|
|
|
|
test('parseChunkForContent ignores items without v to match Go parser behavior', () => {
|
|
const chunk = {
|
|
p: 'response',
|
|
v: [{ type: 'RESPONSE', content: 'no-v-content' }],
|
|
};
|
|
const parsed = parseChunkForContent(chunk, false, 'text');
|
|
assert.equal(parsed.finished, false);
|
|
assert.deepEqual(parsed.parts, []);
|
|
});
|
|
|
|
test('parseChunkForContent handles response/fragments APPEND with thinking and response transitions', () => {
|
|
const chunk = {
|
|
p: 'response/fragments',
|
|
o: 'APPEND',
|
|
v: [
|
|
{ type: 'THINK', content: '思考中' },
|
|
{ type: 'RESPONSE', content: '结论' },
|
|
],
|
|
};
|
|
const parsed = parseChunkForContent(chunk, true, 'thinking');
|
|
assert.equal(parsed.finished, false);
|
|
assert.equal(parsed.newType, 'text');
|
|
assert.deepEqual(parsed.parts, [
|
|
{ text: '思考中', type: 'thinking' },
|
|
{ text: '结论', type: 'text' },
|
|
]);
|
|
});
|
|
|
|
test('parseChunkForContent supports wrapped response.fragments object shape', () => {
|
|
const chunk = {
|
|
p: 'response',
|
|
v: {
|
|
response: {
|
|
fragments: [
|
|
{ type: 'RESPONSE', content: 'A' },
|
|
{ type: 'RESPONSE', content: 'B' },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
const parsed = parseChunkForContent(chunk, false, 'text');
|
|
assert.equal(parsed.finished, false);
|
|
assert.equal(parsed.parts.map((p) => p.text).join(''), 'AB');
|
|
});
|