mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 00:14:37 +00:00
refactor: replace custom test runner with Node.js assert
- Simplify test files by removing custom TestRunner class - Use standard Node.js assert module across all unit tests - Update CLAUDE.md with streamlined development instructions - Update tests/README.md with current testing approach - Reduce boilerplate in delegation and GLMT test suites (652 lines removed)
This commit is contained in:
1 parent
ce9d60d0ca
commit
cb7e38d2b1
13 files changed
+1656
-2308
No files matched your search
@@ -1,350 +1,278 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { DeltaAccumulator } = require('../../../dist/glmt/delta-accumulator');
|
||||
|
||||
console.log('[TEST] DeltaAccumulator unit tests');
|
||||
console.log('');
|
||||
describe('DeltaAccumulator', () => {
|
||||
describe('Initialization', () => {
|
||||
it('has correct initial state', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
assert(acc.messageId.startsWith('msg_'), 'Message ID should start with msg_');
|
||||
assert.strictEqual(acc.role, 'assistant');
|
||||
assert.strictEqual(acc.contentBlocks.length, 0);
|
||||
assert.strictEqual(acc.currentBlockIndex, -1);
|
||||
assert.strictEqual(acc.messageStarted, false);
|
||||
assert.strictEqual(acc.finalized, false);
|
||||
});
|
||||
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`[PASS] ${name}`);
|
||||
passedTests++;
|
||||
} catch (error) {
|
||||
console.log(`[FAIL] ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
failedTests++;
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Initialization
|
||||
test('Initial state', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
assert(acc.messageId.startsWith('msg_'), 'Message ID should start with msg_');
|
||||
assert(acc.role === 'assistant', 'Default role should be assistant');
|
||||
assert(acc.contentBlocks.length === 0, 'Should have no blocks initially');
|
||||
assert(acc.currentBlockIndex === -1, 'Current index should be -1');
|
||||
assert(!acc.messageStarted, 'Message should not be started');
|
||||
assert(!acc.finalized, 'Should not be finalized');
|
||||
});
|
||||
|
||||
// Test: Start thinking block
|
||||
test('Start thinking block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
const block = acc.startBlock('thinking');
|
||||
assert(block.type === 'thinking', 'Block type should be thinking');
|
||||
assert(block.index === 0, 'First block index should be 0');
|
||||
assert(block.started === true, 'Block should be marked as started');
|
||||
assert(block.stopped === false, 'Block should not be stopped');
|
||||
assert(acc.contentBlocks.length === 1, 'Should have 1 block');
|
||||
assert(acc.currentBlockIndex === 0, 'Current index should be 0');
|
||||
});
|
||||
|
||||
// Test: Add delta to thinking block
|
||||
test('Add delta to thinking block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Hello ');
|
||||
acc.addDelta('world');
|
||||
const block = acc.getCurrentBlock();
|
||||
assert(block.content === 'Hello world', `Expected 'Hello world', got '${block.content}'`);
|
||||
assert(acc.thinkingBuffer === 'Hello world', 'Thinking buffer should match');
|
||||
});
|
||||
|
||||
// Test: Start text block
|
||||
test('Start text block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
const block = acc.startBlock('text');
|
||||
assert(block.type === 'text', 'Block type should be text');
|
||||
assert(block.index === 0, 'First block index should be 0');
|
||||
});
|
||||
|
||||
// Test: Add delta to text block
|
||||
test('Add delta to text block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('text');
|
||||
acc.addDelta('Answer: ');
|
||||
acc.addDelta('42');
|
||||
const block = acc.getCurrentBlock();
|
||||
assert(block.content === 'Answer: 42', `Expected 'Answer: 42', got '${block.content}'`);
|
||||
assert(acc.textBuffer === 'Answer: 42', 'Text buffer should match');
|
||||
});
|
||||
|
||||
// Test: Multiple blocks
|
||||
test('Multiple blocks (thinking → text)', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
// Thinking block
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Analyzing...');
|
||||
|
||||
// Text block
|
||||
acc.startBlock('text');
|
||||
acc.addDelta('The answer is 42');
|
||||
|
||||
assert(acc.contentBlocks.length === 2, 'Should have 2 blocks');
|
||||
assert(acc.currentBlockIndex === 1, 'Current index should be 1');
|
||||
assert(acc.contentBlocks[0].type === 'thinking', 'First block should be thinking');
|
||||
assert(acc.contentBlocks[1].type === 'text', 'Second block should be text');
|
||||
assert(acc.contentBlocks[0].content === 'Analyzing...', 'Thinking content incorrect');
|
||||
assert(acc.contentBlocks[1].content === 'The answer is 42', 'Text content incorrect');
|
||||
});
|
||||
|
||||
// Test: Stop current block
|
||||
test('Stop current block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Done');
|
||||
acc.stopCurrentBlock();
|
||||
const block = acc.getCurrentBlock();
|
||||
assert(block.stopped === true, 'Block should be marked as stopped');
|
||||
});
|
||||
|
||||
// Test: Get current block (no blocks)
|
||||
test('Get current block when none exist', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
const block = acc.getCurrentBlock();
|
||||
assert(block === null, 'Should return null when no blocks');
|
||||
});
|
||||
|
||||
// Test: Update usage statistics
|
||||
test('Update usage statistics', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.updateUsage({ prompt_tokens: 100, completion_tokens: 50 });
|
||||
assert(acc.inputTokens === 100, `Expected 100, got ${acc.inputTokens}`);
|
||||
assert(acc.outputTokens === 50, `Expected 50, got ${acc.outputTokens}`);
|
||||
});
|
||||
|
||||
// Test: Get summary
|
||||
test('Get summary', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.model = 'glm-4.6';
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('test');
|
||||
acc.updateUsage({ prompt_tokens: 10, completion_tokens: 20 });
|
||||
|
||||
const summary = acc.getSummary();
|
||||
assert(summary.messageId === acc.messageId, 'Message ID mismatch');
|
||||
assert(summary.model === 'glm-4.6', 'Model mismatch');
|
||||
assert(summary.role === 'assistant', 'Role mismatch');
|
||||
assert(summary.blockCount === 1, `Expected 1 block, got ${summary.blockCount}`);
|
||||
assert(summary.usage.input_tokens === 10, 'Input tokens mismatch');
|
||||
assert(summary.usage.output_tokens === 20, 'Output tokens mismatch');
|
||||
});
|
||||
|
||||
// Test: Thinking config preservation
|
||||
test('Thinking config preservation', () => {
|
||||
const config = { thinking: true, effort: 'high' };
|
||||
const acc = new DeltaAccumulator(config);
|
||||
assert(acc.thinkingConfig.thinking === true, 'Thinking config not preserved');
|
||||
assert(acc.thinkingConfig.effort === 'high', 'Effort config not preserved');
|
||||
});
|
||||
|
||||
// Test: Message lifecycle flags
|
||||
test('Message lifecycle flags', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
assert(!acc.messageStarted, 'Should not be started initially');
|
||||
acc.messageStarted = true;
|
||||
assert(acc.messageStarted, 'Should be marked as started');
|
||||
acc.finalized = true;
|
||||
assert(acc.finalized, 'Should be marked as finalized');
|
||||
});
|
||||
|
||||
// Test: Finish reason tracking
|
||||
test('Finish reason tracking', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
assert(acc.finishReason === null, 'Finish reason should be null initially');
|
||||
acc.finishReason = 'stop';
|
||||
assert(acc.finishReason === 'stop', 'Finish reason should be updated');
|
||||
});
|
||||
|
||||
// Test: Loop detection - No loop (default threshold 3)
|
||||
test('Loop detection - No loop detected with default threshold', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
// Add only 2 thinking blocks (below threshold)
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 1');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 2');
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert(!hasLoop, 'Should not detect loop with only 2 thinking blocks');
|
||||
assert(!acc.loopDetected, 'loopDetected flag should be false');
|
||||
});
|
||||
|
||||
// Test: Loop detection - Loop detected with 3 consecutive thinking blocks
|
||||
test('Loop detection - Loop detected with 3 consecutive thinking blocks', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
// Add 3 consecutive thinking blocks with no tool calls
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Planning step 1...');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Planning step 2...');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Planning step 3...');
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert(hasLoop, 'Should detect loop with 3 consecutive thinking blocks');
|
||||
assert(acc.loopDetected, 'loopDetected flag should be true');
|
||||
|
||||
const summary = acc.getSummary();
|
||||
assert(summary.loopDetected === true, 'Summary should reflect loop detection');
|
||||
});
|
||||
|
||||
// Test: Loop detection - No loop when tool calls exist
|
||||
test('Loop detection - No loop when tool calls present', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
// Add 3 thinking blocks but with a tool call
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 1');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 2');
|
||||
|
||||
// Add a tool call
|
||||
acc.addToolCallDelta({
|
||||
index: 0,
|
||||
id: 'call_123',
|
||||
type: 'function',
|
||||
function: { name: 'read_file', arguments: '{"path": "test.js"}' }
|
||||
it('accepts config options', () => {
|
||||
const acc = new DeltaAccumulator({}, { loopDetectionThreshold: 5 });
|
||||
assert.strictEqual(acc.loopDetectionThreshold, 5);
|
||||
});
|
||||
});
|
||||
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 3');
|
||||
describe('Block management', () => {
|
||||
it('starts thinking block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
const block = acc.startBlock('thinking');
|
||||
assert.strictEqual(block.type, 'thinking');
|
||||
assert.strictEqual(block.index, 0);
|
||||
assert.strictEqual(block.started, true);
|
||||
assert.strictEqual(block.stopped, false);
|
||||
assert.strictEqual(acc.contentBlocks.length, 1);
|
||||
assert.strictEqual(acc.currentBlockIndex, 0);
|
||||
});
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert(!hasLoop, 'Should not detect loop when tool calls exist');
|
||||
assert(!acc.loopDetected, 'loopDetected flag should be false');
|
||||
});
|
||||
it('starts text block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
const block = acc.startBlock('text');
|
||||
assert.strictEqual(block.type, 'text');
|
||||
assert.strictEqual(block.index, 0);
|
||||
});
|
||||
|
||||
// Test: Loop detection - No loop with mixed block types
|
||||
test('Loop detection - No loop with mixed block types', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
it('adds delta to thinking block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Hello ');
|
||||
acc.addDelta('world');
|
||||
const block = acc.getCurrentBlock();
|
||||
assert.strictEqual(block.content, 'Hello world');
|
||||
assert.strictEqual(acc.thinkingBuffer, 'Hello world');
|
||||
});
|
||||
|
||||
// Add thinking, text, thinking pattern (not all consecutive thinking)
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 1');
|
||||
acc.startBlock('text');
|
||||
acc.addDelta('Some text');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 2');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 3');
|
||||
it('adds delta to text block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('text');
|
||||
acc.addDelta('Answer: ');
|
||||
acc.addDelta('42');
|
||||
const block = acc.getCurrentBlock();
|
||||
assert.strictEqual(block.content, 'Answer: 42');
|
||||
assert.strictEqual(acc.textBuffer, 'Answer: 42');
|
||||
});
|
||||
|
||||
// Last 3 blocks: text, thinking, thinking (not all thinking)
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert(!hasLoop, 'Should not detect loop when blocks are mixed');
|
||||
});
|
||||
it('handles multiple blocks (thinking → text)', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Analyzing...');
|
||||
acc.startBlock('text');
|
||||
acc.addDelta('The answer is 42');
|
||||
|
||||
// Test: Loop detection - Custom threshold
|
||||
test('Loop detection - Custom threshold (5 blocks)', () => {
|
||||
const acc = new DeltaAccumulator({}, { loopDetectionThreshold: 5 });
|
||||
assert.strictEqual(acc.contentBlocks.length, 2);
|
||||
assert.strictEqual(acc.currentBlockIndex, 1);
|
||||
assert.strictEqual(acc.contentBlocks[0].type, 'thinking');
|
||||
assert.strictEqual(acc.contentBlocks[1].type, 'text');
|
||||
assert.strictEqual(acc.contentBlocks[0].content, 'Analyzing...');
|
||||
assert.strictEqual(acc.contentBlocks[1].content, 'The answer is 42');
|
||||
});
|
||||
|
||||
// Add 4 thinking blocks (below custom threshold)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta(`Thinking ${i + 1}`);
|
||||
}
|
||||
it('stops current block', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Done');
|
||||
acc.stopCurrentBlock();
|
||||
const block = acc.getCurrentBlock();
|
||||
assert.strictEqual(block.stopped, true);
|
||||
});
|
||||
|
||||
let hasLoop = acc.checkForLoop();
|
||||
assert(!hasLoop, 'Should not detect loop with 4 blocks when threshold is 5');
|
||||
|
||||
// Add 5th thinking block
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 5');
|
||||
|
||||
hasLoop = acc.checkForLoop();
|
||||
assert(hasLoop, 'Should detect loop with 5 consecutive thinking blocks');
|
||||
});
|
||||
|
||||
// Test: Loop detection - Reset state
|
||||
test('Loop detection - Reset state', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
// Trigger loop detection
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.checkForLoop();
|
||||
|
||||
assert(acc.loopDetected, 'Loop should be detected');
|
||||
|
||||
// Reset
|
||||
acc.resetLoopDetection();
|
||||
|
||||
assert(!acc.loopDetected, 'Loop detection should be reset');
|
||||
|
||||
// ACTUAL BEHAVIOR: After reset, checkForLoop() re-evaluates the blocks
|
||||
// Since the same 3 thinking blocks still exist with no tool calls,
|
||||
// it does NOT detect loop again (because the condition already passed once)
|
||||
// This is CORRECT behavior - reset clears the flag, allowing re-evaluation
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert(hasLoop, 'Should re-detect loop with same pattern'); // Changed expectation
|
||||
});
|
||||
|
||||
// Test: Loop detection - Persistent after first detection
|
||||
test('Loop detection - Persistent after first detection', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
// Trigger loop
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.checkForLoop();
|
||||
|
||||
assert(acc.loopDetected, 'Loop should be detected');
|
||||
|
||||
// Add more blocks
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
|
||||
// Check again - should still return true
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert(hasLoop, 'Loop detection should persist');
|
||||
});
|
||||
|
||||
// Test: Loop detection - Tool call addition tracking
|
||||
test('Loop detection - Tool calls tracked correctly', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
// Add tool call deltas
|
||||
acc.addToolCallDelta({
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'test', arguments: '{"a":' }
|
||||
it('returns null when no blocks exist', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
const block = acc.getCurrentBlock();
|
||||
assert.strictEqual(block, null);
|
||||
});
|
||||
});
|
||||
|
||||
acc.addToolCallDelta({
|
||||
index: 0,
|
||||
function: { arguments: '1}' }
|
||||
describe('Usage statistics', () => {
|
||||
it('updates usage statistics', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.updateUsage({ prompt_tokens: 100, completion_tokens: 50 });
|
||||
assert.strictEqual(acc.inputTokens, 100);
|
||||
assert.strictEqual(acc.outputTokens, 50);
|
||||
});
|
||||
});
|
||||
|
||||
const toolCalls = acc.getToolCalls();
|
||||
assert(toolCalls.length === 1, 'Should have 1 tool call');
|
||||
assert(toolCalls[0].function.arguments === '{"a":1}', 'Arguments should accumulate');
|
||||
describe('Summary', () => {
|
||||
it('gets correct summary', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.model = 'glm-4.6';
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('test');
|
||||
acc.updateUsage({ prompt_tokens: 10, completion_tokens: 20 });
|
||||
|
||||
const summary = acc.getSummary();
|
||||
assert(summary.toolCallCount === 1, 'Summary should show 1 tool call');
|
||||
const summary = acc.getSummary();
|
||||
assert.strictEqual(summary.messageId, acc.messageId);
|
||||
assert.strictEqual(summary.model, 'glm-4.6');
|
||||
assert.strictEqual(summary.role, 'assistant');
|
||||
assert.strictEqual(summary.blockCount, 1);
|
||||
assert.strictEqual(summary.usage.input_tokens, 10);
|
||||
assert.strictEqual(summary.usage.output_tokens, 20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message lifecycle', () => {
|
||||
it('tracks lifecycle flags', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
assert.strictEqual(acc.messageStarted, false);
|
||||
acc.messageStarted = true;
|
||||
assert.strictEqual(acc.messageStarted, true);
|
||||
acc.finalized = true;
|
||||
assert.strictEqual(acc.finalized, true);
|
||||
});
|
||||
|
||||
it('tracks finish reason', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
assert.strictEqual(acc.finishReason, null);
|
||||
acc.finishReason = 'stop';
|
||||
assert.strictEqual(acc.finishReason, 'stop');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loop detection', () => {
|
||||
it('does not detect loop below threshold', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 1');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 2');
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, false);
|
||||
assert.strictEqual(acc.loopDetected, false);
|
||||
});
|
||||
|
||||
it('detects loop with 3 consecutive thinking blocks', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Planning step 1...');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Planning step 2...');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Planning step 3...');
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, true);
|
||||
assert.strictEqual(acc.loopDetected, true);
|
||||
|
||||
const summary = acc.getSummary();
|
||||
assert.strictEqual(summary.loopDetected, true);
|
||||
});
|
||||
|
||||
it('does not detect loop when tool calls present', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 1');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 2');
|
||||
|
||||
acc.addToolCallDelta({
|
||||
index: 0,
|
||||
id: 'call_123',
|
||||
type: 'function',
|
||||
function: { name: 'read_file', arguments: '{"path": "test.js"}' }
|
||||
});
|
||||
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 3');
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, false);
|
||||
assert.strictEqual(acc.loopDetected, false);
|
||||
});
|
||||
|
||||
it('does not detect loop with mixed block types', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 1');
|
||||
acc.startBlock('text');
|
||||
acc.addDelta('Some text');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 2');
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 3');
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, false);
|
||||
});
|
||||
|
||||
it('respects custom threshold', () => {
|
||||
const acc = new DeltaAccumulator({}, { loopDetectionThreshold: 5 });
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta(`Thinking ${i + 1}`);
|
||||
}
|
||||
|
||||
let hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, false);
|
||||
|
||||
acc.startBlock('thinking');
|
||||
acc.addDelta('Thinking 5');
|
||||
|
||||
hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, true);
|
||||
});
|
||||
|
||||
it('resets loop detection state', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.checkForLoop();
|
||||
|
||||
assert.strictEqual(acc.loopDetected, true);
|
||||
acc.resetLoopDetection();
|
||||
assert.strictEqual(acc.loopDetected, false);
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, true);
|
||||
});
|
||||
|
||||
it('persists after first detection', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
acc.checkForLoop();
|
||||
|
||||
assert.strictEqual(acc.loopDetected, true);
|
||||
|
||||
acc.startBlock('thinking');
|
||||
acc.startBlock('thinking');
|
||||
|
||||
const hasLoop = acc.checkForLoop();
|
||||
assert.strictEqual(hasLoop, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool call tracking', () => {
|
||||
it('tracks tool calls correctly', () => {
|
||||
const acc = new DeltaAccumulator();
|
||||
|
||||
acc.addToolCallDelta({
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'test', arguments: '{"a":' }
|
||||
});
|
||||
|
||||
acc.addToolCallDelta({
|
||||
index: 0,
|
||||
function: { arguments: '1}' }
|
||||
});
|
||||
|
||||
const toolCalls = acc.getToolCalls();
|
||||
assert.strictEqual(toolCalls.length, 1);
|
||||
assert.strictEqual(toolCalls[0].function.arguments, '{"a":1}');
|
||||
|
||||
const summary = acc.getSummary();
|
||||
assert.strictEqual(summary.toolCallCount, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log(`TESTS: ${passedTests} passed, ${failedTests} failed`);
|
||||
console.log('═══════════════════════════════════════');
|
||||
|
||||
if (failedTests > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const LocaleEnforcer = require('../../../dist/glmt/locale-enforcer').default;
|
||||
const { LocaleEnforcer } = require('../../../dist/glmt/locale-enforcer');
|
||||
|
||||
describe('LocaleEnforcer', () => {
|
||||
describe('Scenario 1: English prompt → English output', () => {
|
||||
|
||||
@@ -1,293 +1,174 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ReasoningEnforcer Unit Tests
|
||||
*
|
||||
* Test scenarios:
|
||||
* 1. Opt-in behavior (enabled vs disabled)
|
||||
* 2. System message injection
|
||||
* 3. User message fallback
|
||||
* 4. Effort level selection (low/medium/high/max)
|
||||
* 5. Message structure handling (string vs array)
|
||||
* 6. Edge cases (empty messages, no system/user)
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const ReasoningEnforcer = require('../../../dist/glmt/reasoning-enforcer').default;
|
||||
|
||||
/**
|
||||
* Simple test runner (no external dependencies)
|
||||
*/
|
||||
class TestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
}
|
||||
describe('ReasoningEnforcer', () => {
|
||||
describe('Opt-in behavior', () => {
|
||||
it('should NOT inject when disabled and thinking=false', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: false });
|
||||
const messages = [{ role: 'user', content: 'What is 2+2?' }];
|
||||
|
||||
test(name, fn) {
|
||||
this.tests.push({ name, fn });
|
||||
}
|
||||
const result = enforcer.injectInstruction(messages, { thinking: false });
|
||||
|
||||
async run() {
|
||||
console.log('\n=== ReasoningEnforcer Tests ===\n');
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.strictEqual(result[0].content, 'What is 2+2?');
|
||||
});
|
||||
|
||||
for (const { name, fn } of this.tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(`[OK] ${name}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.error(`[X] ${name}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
it('should inject when enabled=true', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'What is 2+2?' }];
|
||||
|
||||
console.log(`\n=== Results ===`);
|
||||
console.log(`Passed: ${this.passed}/${this.tests.length}`);
|
||||
console.log(`Failed: ${this.failed}/${this.tests.length}`);
|
||||
const result = enforcer.injectInstruction(messages, { thinking: false });
|
||||
|
||||
return this.failed === 0;
|
||||
}
|
||||
}
|
||||
assert.ok(result[0].content.includes('CRITICAL'));
|
||||
assert.ok(result[0].content.includes('<reasoning_content>'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Simple assertion helpers
|
||||
*/
|
||||
function assertEqual(actual, expected, message) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`${message || 'Assertion failed'}\n` +
|
||||
` Expected: ${JSON.stringify(expected)}\n` +
|
||||
` Actual: ${JSON.stringify(actual)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
it('should inject when thinking=true (even if enabled=false)', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: false });
|
||||
const messages = [{ role: 'user', content: 'What is 2+2?' }];
|
||||
|
||||
function assertTrue(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message || 'Expected condition to be true');
|
||||
}
|
||||
}
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
function assertIncludes(haystack, needle, message) {
|
||||
if (!haystack.includes(needle)) {
|
||||
throw new Error(
|
||||
`${message || 'String does not include expected substring'}\n` +
|
||||
` Expected substring: ${needle}\n` +
|
||||
` Actual string: ${haystack.substring(0, 100)}...`
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.ok(result[0].content.includes('CRITICAL'));
|
||||
});
|
||||
});
|
||||
|
||||
function assertDeepEqual(actual, expected, message) {
|
||||
const actualStr = JSON.stringify(actual);
|
||||
const expectedStr = JSON.stringify(expected);
|
||||
if (actualStr !== expectedStr) {
|
||||
throw new Error(
|
||||
`${message || 'Deep equality assertion failed'}\n` +
|
||||
` Expected: ${expectedStr}\n` +
|
||||
` Actual: ${actualStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
describe('System message injection', () => {
|
||||
it('should prepend to system message (string content)', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'Calculate 2+2' }
|
||||
];
|
||||
|
||||
// Create test runner
|
||||
const runner = new TestRunner();
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'medium' });
|
||||
|
||||
// Test 1: Opt-in behavior - disabled
|
||||
runner.test('should NOT inject when disabled and thinking=false', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: false });
|
||||
const messages = [
|
||||
{ role: 'user', content: 'What is 2+2?' }
|
||||
];
|
||||
assert.strictEqual(result.length, 2);
|
||||
assert.ok(result[0].content.startsWith('You are an expert reasoning model'));
|
||||
assert.ok(result[0].content.includes('You are a helpful assistant'));
|
||||
assert.strictEqual(result[1].content, 'Calculate 2+2');
|
||||
});
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: false });
|
||||
it('should prepend to system message (array content)', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: [{ type: 'text', text: 'You are a code assistant.' }]
|
||||
},
|
||||
{ role: 'user', content: 'Write a function' }
|
||||
];
|
||||
|
||||
assertEqual(result.length, 1);
|
||||
assertEqual(result[0].content, 'What is 2+2?');
|
||||
});
|
||||
|
||||
// Test 2: Opt-in behavior - enabled
|
||||
runner.test('should inject when enabled=true', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [
|
||||
{ role: 'user', content: 'What is 2+2?' }
|
||||
];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: false });
|
||||
|
||||
assertIncludes(result[0].content, 'CRITICAL');
|
||||
assertIncludes(result[0].content, '<reasoning_content>');
|
||||
});
|
||||
|
||||
// Test 3: Opt-in behavior - thinking=true
|
||||
runner.test('should inject when thinking=true (even if enabled=false)', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: false });
|
||||
const messages = [
|
||||
{ role: 'user', content: 'What is 2+2?' }
|
||||
];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assertIncludes(result[0].content, 'CRITICAL');
|
||||
});
|
||||
|
||||
// Test 4: System message injection - string content
|
||||
runner.test('should prepend to system message (string content)', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'Calculate 2+2' }
|
||||
];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'medium' });
|
||||
|
||||
assertEqual(result.length, 2);
|
||||
assertTrue(result[0].content.startsWith('You are an expert reasoning model'));
|
||||
assertIncludes(result[0].content, 'You are a helpful assistant');
|
||||
assertEqual(result[1].content, 'Calculate 2+2');
|
||||
});
|
||||
|
||||
// Test 5: System message injection - array content
|
||||
runner.test('should prepend to system message (array content)', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: [
|
||||
{ type: 'text', text: 'You are a code assistant.' }
|
||||
]
|
||||
},
|
||||
{ role: 'user', content: 'Write a function' }
|
||||
];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assertTrue(Array.isArray(result[0].content));
|
||||
assertEqual(result[0].content[0].type, 'text');
|
||||
assertIncludes(result[0].content[0].text, 'CRITICAL');
|
||||
assertEqual(result[0].content[1].text, 'You are a code assistant.');
|
||||
});
|
||||
|
||||
// Test 6: User message fallback
|
||||
runner.test('should prepend to first user message when no system message', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Explain quantum computing' }
|
||||
];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assertEqual(result.length, 1);
|
||||
assertIncludes(result[0].content, 'CRITICAL');
|
||||
assertIncludes(result[0].content, 'Explain quantum computing');
|
||||
});
|
||||
|
||||
// Test 7: Effort level - low
|
||||
runner.test('should use low prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'low' });
|
||||
|
||||
assertIncludes(result[0].content.toLowerCase(), 'brief analysis');
|
||||
});
|
||||
|
||||
// Test 8: Effort level - medium
|
||||
runner.test('should use medium prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'medium' });
|
||||
|
||||
assertIncludes(result[0].content.toLowerCase(), 'think step-by-step');
|
||||
});
|
||||
|
||||
// Test 9: Effort level - high
|
||||
runner.test('should use high prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'high' });
|
||||
|
||||
assertIncludes(result[0].content.toLowerCase(), 'think deeply and systematically');
|
||||
});
|
||||
|
||||
// Test 10: Effort level - max
|
||||
runner.test('should use max prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'max' });
|
||||
|
||||
assertIncludes(result[0].content.toLowerCase(), 'exhaustively from first principles');
|
||||
});
|
||||
|
||||
// Test 11: Default effort level
|
||||
runner.test('should default to medium effort if not specified', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assertIncludes(result[0].content, 'think step-by-step');
|
||||
});
|
||||
|
||||
// Test 12: Empty messages array
|
||||
runner.test('should handle empty messages array', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assertEqual(result.length, 0);
|
||||
});
|
||||
|
||||
// Test 13: No system or user role
|
||||
runner.test('should handle messages with no system or user role', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [
|
||||
{ role: 'assistant', content: 'Previous response' }
|
||||
];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assertEqual(result.length, 1);
|
||||
assertEqual(result[0].content, 'Previous response');
|
||||
});
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
// Test 14: Immutability
|
||||
runner.test('should not mutate original messages array', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const originalMessages = [
|
||||
{ role: 'user', content: 'Test prompt' }
|
||||
];
|
||||
const originalCopy = JSON.parse(JSON.stringify(originalMessages));
|
||||
assert.ok(Array.isArray(result[0].content));
|
||||
assert.strictEqual(result[0].content[0].type, 'text');
|
||||
assert.ok(result[0].content[0].text.includes('CRITICAL'));
|
||||
assert.strictEqual(result[0].content[1].text, 'You are a code assistant.');
|
||||
});
|
||||
});
|
||||
|
||||
enforcer.injectInstruction(originalMessages, { thinking: true });
|
||||
describe('User message fallback', () => {
|
||||
it('should prepend to first user message when no system message', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Explain quantum computing' }];
|
||||
|
||||
assertDeepEqual(originalMessages, originalCopy);
|
||||
});
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
// Test 15: Custom prompts
|
||||
runner.test('should handle custom prompts via constructor', () => {
|
||||
const customPrompts = {
|
||||
low: 'Custom low prompt',
|
||||
medium: 'Custom medium prompt',
|
||||
high: 'Custom high prompt',
|
||||
max: 'Custom max prompt'
|
||||
};
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true, prompts: customPrompts });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.ok(result[0].content.includes('CRITICAL'));
|
||||
assert.ok(result[0].content.includes('Explain quantum computing'));
|
||||
});
|
||||
});
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'low' });
|
||||
describe('Effort levels', () => {
|
||||
it('should use low prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
assertIncludes(result[0].content, 'Custom low prompt');
|
||||
});
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'low' });
|
||||
|
||||
// Run all tests
|
||||
runner.run().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
assert.ok(result[0].content.toLowerCase().includes('brief analysis'));
|
||||
});
|
||||
|
||||
it('should use medium prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'medium' });
|
||||
|
||||
assert.ok(result[0].content.toLowerCase().includes('think step-by-step'));
|
||||
});
|
||||
|
||||
it('should use high prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'high' });
|
||||
|
||||
assert.ok(result[0].content.toLowerCase().includes('think deeply and systematically'));
|
||||
});
|
||||
|
||||
it('should use max prompt template', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'max' });
|
||||
|
||||
assert.ok(result[0].content.toLowerCase().includes('exhaustively from first principles'));
|
||||
});
|
||||
|
||||
it('should default to medium effort if not specified', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assert.ok(result[0].content.includes('think step-by-step'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty messages array', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const result = enforcer.injectInstruction([], { thinking: true });
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
it('should handle messages with no system or user role', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const messages = [{ role: 'assistant', content: 'Previous response' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true });
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.strictEqual(result[0].content, 'Previous response');
|
||||
});
|
||||
|
||||
it('should not mutate original messages array', () => {
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true });
|
||||
const originalMessages = [{ role: 'user', content: 'Test prompt' }];
|
||||
const originalCopy = JSON.parse(JSON.stringify(originalMessages));
|
||||
|
||||
enforcer.injectInstruction(originalMessages, { thinking: true });
|
||||
|
||||
assert.deepStrictEqual(originalMessages, originalCopy);
|
||||
});
|
||||
|
||||
it('should handle custom prompts via constructor', () => {
|
||||
const customPrompts = {
|
||||
low: 'Custom low prompt',
|
||||
medium: 'Custom medium prompt',
|
||||
high: 'Custom high prompt',
|
||||
max: 'Custom max prompt'
|
||||
};
|
||||
const enforcer = new ReasoningEnforcer({ enabled: true, prompts: customPrompts });
|
||||
const messages = [{ role: 'user', content: 'Test' }];
|
||||
|
||||
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'low' });
|
||||
|
||||
assert.ok(result[0].content.includes('Custom low prompt'));
|
||||
});
|
||||
});
|
||||
});
|
||||
+103
-137
@@ -1,144 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const { SSEParser } = require('../../../dist/glmt/sse-parser');
|
||||
|
||||
const SSEParser = require('../../../dist/glmt/sse-parser').default;
|
||||
describe('SSEParser', () => {
|
||||
describe('Basic parsing', () => {
|
||||
it('parses single event', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"test": "value"}\n\n');
|
||||
assert.strictEqual(events.length, 1, `Expected 1 event, got ${events.length}`);
|
||||
assert.strictEqual(events[0].data.test, 'value');
|
||||
});
|
||||
|
||||
console.log('[TEST] SSEParser unit tests');
|
||||
console.log('');
|
||||
it('parses multiple events in one chunk', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"a": 1}\n\ndata: {"b": 2}\n\n');
|
||||
assert.strictEqual(events.length, 2, `Expected 2 events, got ${events.length}`);
|
||||
assert.strictEqual(events[0].data.a, 1);
|
||||
assert.strictEqual(events[1].data.b, 2);
|
||||
});
|
||||
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
it('handles event split across chunks', () => {
|
||||
const parser = new SSEParser();
|
||||
const events1 = parser.parse('data: {"test":');
|
||||
assert.strictEqual(events1.length, 0, 'Should not emit incomplete event');
|
||||
const events2 = parser.parse('"value"}\n\n');
|
||||
assert.strictEqual(events2.length, 1, `Expected 1 event after completion, got ${events2.length}`);
|
||||
assert.strictEqual(events2[0].data.test, 'value');
|
||||
});
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`[PASS] ${name}`);
|
||||
passedTests++;
|
||||
} catch (error) {
|
||||
console.log(`[FAIL] ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
failedTests++;
|
||||
}
|
||||
}
|
||||
it('handles empty lines between events', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"a": 1}\n\n\n\ndata: {"b": 2}\n\n');
|
||||
assert.strictEqual(events.length, 2, `Expected 2 events, got ${events.length}`);
|
||||
});
|
||||
});
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
}
|
||||
describe('[DONE] marker', () => {
|
||||
it('detects [DONE] marker', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: [DONE]\n\n');
|
||||
assert.strictEqual(events.length, 1, `Expected 1 event, got ${events.length}`);
|
||||
assert.strictEqual(events[0].event, 'done');
|
||||
assert.strictEqual(events[0].data, null);
|
||||
});
|
||||
|
||||
// Test: Single event
|
||||
test('Single event parsing', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"test": "value"}\n\n');
|
||||
assert(events.length === 1, `Expected 1 event, got ${events.length}`);
|
||||
assert(events[0].data.test === 'value', `Expected test=value, got ${events[0].data.test}`);
|
||||
it('handles mixed events with [DONE]', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"msg": "hello"}\n\ndata: [DONE]\n\n');
|
||||
assert.strictEqual(events.length, 2, `Expected 2 events, got ${events.length}`);
|
||||
assert.strictEqual(events[0].data.msg, 'hello');
|
||||
assert.strictEqual(events[1].event, 'done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('handles malformed JSON gracefully', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {invalid json}\n\ndata: {"valid": true}\n\n');
|
||||
assert.strictEqual(events.length, 1, `Expected 1 valid event, got ${events.length}`);
|
||||
assert.strictEqual(events[0].data.valid, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event fields', () => {
|
||||
it('parses event with ID field', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('id: 123\ndata: {"test": true}\n\n');
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].id, '123');
|
||||
assert.strictEqual(events[0].data.test, true);
|
||||
});
|
||||
|
||||
it('parses custom event type', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('event: custom\ndata: {"test": true}\n\n');
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].event, 'custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset functionality', () => {
|
||||
it('resets parser state', () => {
|
||||
const parser = new SSEParser();
|
||||
parser.parse('data: {"test": 1}\n\n');
|
||||
assert.strictEqual(parser.eventCount, 1);
|
||||
parser.reset();
|
||||
assert.strictEqual(parser.eventCount, 0);
|
||||
assert.strictEqual(parser.buffer, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Z.AI stream format', () => {
|
||||
it('parses real Z.AI stream format', () => {
|
||||
const parser = new SSEParser();
|
||||
const chunk = 'data: {"choices":[{"delta":{"role":"assistant","reasoning_content":"test"}}]}\n\n';
|
||||
const events = parser.parse(chunk);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].data.choices[0].delta.reasoning_content, 'test');
|
||||
});
|
||||
|
||||
it('parses multiple Z.AI deltas', () => {
|
||||
const parser = new SSEParser();
|
||||
const chunk = 'data: {"choices":[{"delta":{"reasoning_content":"chunk1"}}]}\n\ndata: {"choices":[{"delta":{"reasoning_content":"chunk2"}}]}\n\n';
|
||||
const events = parser.parse(chunk);
|
||||
assert.strictEqual(events.length, 2);
|
||||
assert.strictEqual(events[0].data.choices[0].delta.reasoning_content, 'chunk1');
|
||||
assert.strictEqual(events[1].data.choices[0].delta.reasoning_content, 'chunk2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test: Multiple events
|
||||
test('Multiple events in one chunk', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"a": 1}\n\ndata: {"b": 2}\n\n');
|
||||
assert(events.length === 2, `Expected 2 events, got ${events.length}`);
|
||||
assert(events[0].data.a === 1, 'First event data incorrect');
|
||||
assert(events[1].data.b === 2, 'Second event data incorrect');
|
||||
});
|
||||
|
||||
// Test: Split across chunks
|
||||
test('Event split across chunks', () => {
|
||||
const parser = new SSEParser();
|
||||
const events1 = parser.parse('data: {"test":');
|
||||
assert(events1.length === 0, 'Should not emit incomplete event');
|
||||
const events2 = parser.parse('"value"}\n\n');
|
||||
assert(events2.length === 1, `Expected 1 event after completion, got ${events2.length}`);
|
||||
assert(events2[0].data.test === 'value', 'Split event data incorrect');
|
||||
});
|
||||
|
||||
// Test: [DONE] marker
|
||||
test('[DONE] marker detection', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: [DONE]\n\n');
|
||||
assert(events.length === 1, `Expected 1 event, got ${events.length}`);
|
||||
assert(events[0].event === 'done', `Expected event=done, got ${events[0].event}`);
|
||||
assert(events[0].data === null, 'Expected null data for [DONE]');
|
||||
});
|
||||
|
||||
// Test: Mixed content and [DONE]
|
||||
test('Mixed events with [DONE]', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"msg": "hello"}\n\ndata: [DONE]\n\n');
|
||||
assert(events.length === 2, `Expected 2 events, got ${events.length}`);
|
||||
assert(events[0].data.msg === 'hello', 'First event data incorrect');
|
||||
assert(events[1].event === 'done', 'Second event should be done');
|
||||
});
|
||||
|
||||
// Test: Malformed JSON (should skip gracefully)
|
||||
test('Malformed JSON handling', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {invalid json}\n\ndata: {"valid": true}\n\n');
|
||||
// Should skip malformed, parse valid
|
||||
assert(events.length === 1, `Expected 1 valid event, got ${events.length}`);
|
||||
assert(events[0].data.valid === true, 'Valid event data incorrect');
|
||||
});
|
||||
|
||||
// Test: Empty lines handling
|
||||
test('Empty lines between events', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('data: {"a": 1}\n\n\n\ndata: {"b": 2}\n\n');
|
||||
assert(events.length === 2, `Expected 2 events, got ${events.length}`);
|
||||
});
|
||||
|
||||
// Test: Event with ID field
|
||||
test('Event with ID field', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('id: 123\ndata: {"test": true}\n\n');
|
||||
assert(events.length === 1, `Expected 1 event, got ${events.length}`);
|
||||
assert(events[0].id === '123', `Expected id=123, got ${events[0].id}`);
|
||||
assert(events[0].data.test === true, 'Event data incorrect');
|
||||
});
|
||||
|
||||
// Test: Custom event type
|
||||
test('Custom event type', () => {
|
||||
const parser = new SSEParser();
|
||||
const events = parser.parse('event: custom\ndata: {"test": true}\n\n');
|
||||
assert(events.length === 1, `Expected 1 event, got ${events.length}`);
|
||||
assert(events[0].event === 'custom', `Expected event=custom, got ${events[0].event}`);
|
||||
});
|
||||
|
||||
// Test: Reset functionality
|
||||
test('Parser reset', () => {
|
||||
const parser = new SSEParser();
|
||||
parser.parse('data: {"test": 1}\n\n');
|
||||
assert(parser.eventCount === 1, 'Event count should be 1');
|
||||
parser.reset();
|
||||
assert(parser.eventCount === 0, 'Event count should be 0 after reset');
|
||||
assert(parser.buffer === '', 'Buffer should be empty after reset');
|
||||
});
|
||||
|
||||
// Test: Real Z.AI stream format
|
||||
test('Real Z.AI stream format', () => {
|
||||
const parser = new SSEParser();
|
||||
const chunk = 'data: {"choices":[{"delta":{"role":"assistant","reasoning_content":"test"}}]}\n\n';
|
||||
const events = parser.parse(chunk);
|
||||
assert(events.length === 1, `Expected 1 event, got ${events.length}`);
|
||||
assert(events[0].data.choices[0].delta.reasoning_content === 'test', 'Z.AI format parsing incorrect');
|
||||
});
|
||||
|
||||
// Test: Multiple Z.AI deltas
|
||||
test('Multiple Z.AI deltas', () => {
|
||||
const parser = new SSEParser();
|
||||
const chunk = 'data: {"choices":[{"delta":{"reasoning_content":"chunk1"}}]}\n\ndata: {"choices":[{"delta":{"reasoning_content":"chunk2"}}]}\n\n';
|
||||
const events = parser.parse(chunk);
|
||||
assert(events.length === 2, `Expected 2 events, got ${events.length}`);
|
||||
assert(events[0].data.choices[0].delta.reasoning_content === 'chunk1', 'First delta incorrect');
|
||||
assert(events[1].data.choices[0].delta.reasoning_content === 'chunk2', 'Second delta incorrect');
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log(`TESTS: ${passedTests} passed, ${failedTests} failed`);
|
||||
console.log('═══════════════════════════════════════');
|
||||
|
||||
if (failedTests > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user