mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-18 12:13:58 +00:00
feat(glmt): add streaming with real-time thinking blocks
- reorganize bin/ into auth/, glmt/, management/, utils/ - add budget calculator and locale enforcer - enhance test coverage with unit/integration separation
This commit is contained in:
1 parent
80f9cc644e
commit
5fae92ac07
51 files changed
+3768
-823
No files matched your search
@@ -0,0 +1,184 @@
|
||||
# CCS COMPREHENSIVE TEST SUITE - Master Orchestrator (PowerShell)
|
||||
# Runs all test suites in the correct order
|
||||
# Maintains backward compatibility with existing usage
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
# Get the directory where this script is located
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Yellow
|
||||
Write-Host "CCS COMPREHENSIVE TEST SUITE" -ForegroundColor Yellow
|
||||
Write-Host "========================================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "This master test suite runs:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Native Windows tests (PowerShell installation)" -ForegroundColor Gray
|
||||
Write-Host " 2. npm package tests (if Node.js available)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# Track overall results
|
||||
$OverallPass = 0
|
||||
$OverallFail = 0
|
||||
$OverallTotal = 0
|
||||
|
||||
function Test-Case {
|
||||
param(
|
||||
[string]$Name,
|
||||
[scriptblock]$Test,
|
||||
[string]$ExpectedBehavior
|
||||
)
|
||||
|
||||
$script:TotalTests++
|
||||
Write-Host ""
|
||||
Write-Host "[$script:TotalTests] $Name" -ForegroundColor Cyan
|
||||
Write-Host " Expected: $ExpectedBehavior" -ForegroundColor Gray
|
||||
|
||||
try {
|
||||
$result = & $Test
|
||||
if ($result) {
|
||||
Write-Host " Result: PASS" -ForegroundColor Green
|
||||
$script:PassCount++
|
||||
return $true
|
||||
} else {
|
||||
Write-Host " Result: FAIL" -ForegroundColor Red
|
||||
$script:FailCount++
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Host " Result: FAIL" -ForegroundColor Red
|
||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
$script:FailCount++
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# Function to run a test suite
|
||||
function Run-TestSuite {
|
||||
param(
|
||||
[string]$SuiteName,
|
||||
[scriptblock]$SuiteCommand
|
||||
)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "===== Running $SuiteName =====" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# Reset counters
|
||||
$script:PassCount = 0
|
||||
$script:FailCount = 0
|
||||
$script:TotalTests = 0
|
||||
|
||||
try {
|
||||
& $SuiteCommand
|
||||
$suitePassed = $script:PassCount
|
||||
$suiteFailed = $script:FailCount
|
||||
$suiteTotal = $script:TotalTests
|
||||
|
||||
Write-Host "$SuiteName completed successfully" -ForegroundColor Green
|
||||
Write-Host "Tests: $suitePassed/$suiteTotal passed" -ForegroundColor Cyan
|
||||
|
||||
# Add to overall totals
|
||||
$script:OverallPass += $suitePassed
|
||||
$script:OverallFail += $suiteFailed
|
||||
$script:OverallTotal += $suiteTotal
|
||||
|
||||
return $suiteFailed -eq 0
|
||||
} catch {
|
||||
Write-Host "$SuiteName failed with exception" -ForegroundColor Red
|
||||
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
$script:OverallFail++
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# Test Suite 1: Native Windows Tests
|
||||
$WindowsTestPath = Join-Path $ScriptDir "native\windows\edge-cases.ps1"
|
||||
if (Test-Path $WindowsTestPath) {
|
||||
if (Run-TestSuite "Native Windows Tests" { & $WindowsTestPath }) {
|
||||
Write-Host "✓ Native Windows tests passed" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠ Native Windows tests had failures" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠ Native Windows tests not found, skipping" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Test Suite 2: npm Package Tests
|
||||
$NodeCommand = Get-Command node -ErrorAction SilentlyContinue
|
||||
$PackageJsonPath = Join-Path $ScriptDir "..\package.json"
|
||||
|
||||
if ($NodeCommand -and (Test-Path $PackageJsonPath)) {
|
||||
Write-Host ""
|
||||
Write-Host "===== Running npm Package Tests =====" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
try {
|
||||
Push-Location (Split-Path -Parent $PackageJsonPath)
|
||||
$npmResult = npm run test:npm 2>&1
|
||||
$npmExitCode = $LASTEXITCODE
|
||||
Pop-Location
|
||||
|
||||
if ($npmExitCode -eq 0) {
|
||||
Write-Host "✓ npm package tests passed" -ForegroundColor Green
|
||||
Write-Host "npm tests completed successfully" -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host "⚠ npm package tests had failures" -ForegroundColor Yellow
|
||||
Write-Host "npm output:" -ForegroundColor Gray
|
||||
Write-Host $npmResult -ForegroundColor Gray
|
||||
$script:OverallFail++
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠ npm package tests had errors" -ForegroundColor Yellow
|
||||
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
$script:OverallFail++
|
||||
}
|
||||
} else {
|
||||
if (-not $NodeCommand) {
|
||||
Write-Host "⚠ Node.js not found, skipping npm tests" -ForegroundColor Yellow
|
||||
Write-Host " Install Node.js to run npm package tests" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "⚠ package.json not found, skipping npm tests" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Final Summary
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Yellow
|
||||
Write-Host "FINAL TEST RESULTS" -ForegroundColor Yellow
|
||||
Write-Host "========================================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
if ($OverallTotal -gt 0) {
|
||||
Write-Host "Total Tests: $OverallTotal" -ForegroundColor Cyan
|
||||
Write-Host "Passed: $OverallPass" -ForegroundColor Green
|
||||
|
||||
if ($OverallFail -eq 0) {
|
||||
Write-Host "Failed: $OverallFail" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Failed: $OverallFail" -ForegroundColor Red
|
||||
}
|
||||
|
||||
$SuccessRate = if ($OverallTotal -gt 0) { [math]::Round(($OverallPass / $OverallTotal) * 100, 2) } else { 0 }
|
||||
Write-Host "Success Rate: $SuccessRate%" -ForegroundColor $(if ($SuccessRate -ge 90) { "Green" } elseif ($SuccessRate -ge 70) { "Yellow" } else { "Red" })
|
||||
Write-Host ""
|
||||
|
||||
if ($OverallFail -eq 0) {
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "ALL TESTS PASSED!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "CCS is ready for production use!" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "========================================" -ForegroundColor Yellow
|
||||
Write-Host "SOME TESTS FAILED" -ForegroundColor Yellow
|
||||
Write-Host "========================================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "Review failed tests above for details" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "No tests were executed" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# CCS COMPREHENSIVE TEST SUITE - Master Orchestrator
|
||||
# Runs all test suites in the correct order
|
||||
# Maintains backward compatibility with existing usage
|
||||
|
||||
set -e
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared utilities
|
||||
source "$SCRIPT_DIR/shared/helpers.sh"
|
||||
|
||||
# Print test header
|
||||
print_test_header "CCS COMPREHENSIVE TEST SUITE"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}This master test suite runs:${NC}"
|
||||
echo -e "${GRAY} 1. Native Unix tests (traditional installation)${NC}"
|
||||
echo -e "${GRAY} 2. npm package tests (Node.js framework)${NC}"
|
||||
echo ""
|
||||
|
||||
# Track overall results
|
||||
OVERALL_PASS=0
|
||||
OVERALL_FAIL=0
|
||||
OVERALL_TOTAL=0
|
||||
|
||||
# Function to run a test suite and capture results
|
||||
run_test_suite() {
|
||||
local suite_name="$1"
|
||||
local suite_command="$2"
|
||||
|
||||
echo ""
|
||||
print_section_header "Running $suite_name"
|
||||
echo ""
|
||||
|
||||
# Reset counters for this suite
|
||||
reset_test_counters
|
||||
|
||||
# Run the test suite and capture output
|
||||
if bash -c "$suite_command"; then
|
||||
# Suite passed
|
||||
local suite_passed=$PASS_COUNT
|
||||
local suite_failed=$FAIL_COUNT
|
||||
local suite_total=$TOTAL_TESTS
|
||||
|
||||
echo -e "${GREEN}$suite_name completed successfully${NC}"
|
||||
echo -e "${CYAN}Tests: $suite_passed/$suite_total passed${NC}"
|
||||
|
||||
# Add to overall totals
|
||||
((OVERALL_PASS += suite_passed))
|
||||
((OVERALL_FAIL += suite_failed))
|
||||
((OVERALL_TOTAL += suite_total))
|
||||
|
||||
return 0
|
||||
else
|
||||
# Suite failed
|
||||
local suite_passed=$PASS_COUNT
|
||||
local suite_failed=$FAIL_COUNT
|
||||
local suite_total=$TOTAL_TESTS
|
||||
|
||||
echo -e "${RED}$suite_name failed${NC}"
|
||||
echo -e "${YELLOW}Tests: $suite_passed/$suite_total passed, $suite_failed failed${NC}"
|
||||
|
||||
# Add to overall totals
|
||||
((OVERALL_PASS += suite_passed))
|
||||
((OVERALL_FAIL += suite_failed))
|
||||
((OVERALL_TOTAL += suite_total))
|
||||
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test Suite 1: Native Unix Tests
|
||||
if [[ -f "$SCRIPT_DIR/native/unix/edge-cases.sh" ]]; then
|
||||
if run_test_suite "Native Unix Tests" "cd '$SCRIPT_DIR' && bash native/unix/edge-cases.sh"; then
|
||||
echo -e "${GREEN}✓ Native Unix tests passed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Native Unix tests had failures${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Native Unix tests not found, skipping${NC}"
|
||||
fi
|
||||
|
||||
# Test Suite 2: npm Package Tests
|
||||
if command -v node &> /dev/null; then
|
||||
if [[ -f "$SCRIPT_DIR/../package.json" ]]; then
|
||||
echo ""
|
||||
print_section_header "Running npm Package Tests"
|
||||
echo ""
|
||||
|
||||
# Reset counters for npm tests
|
||||
reset_test_counters
|
||||
|
||||
# Change to the project root directory and run npm tests
|
||||
if cd "$SCRIPT_DIR/.." && npm run test:npm 2>/dev/null; then
|
||||
echo -e "${GREEN}✓ npm package tests passed${NC}"
|
||||
|
||||
# Note: npm tests don't use our counter system, so we'll estimate
|
||||
# We could parse npm test output, but for now just acknowledge success
|
||||
echo -e "${CYAN}npm tests completed successfully${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ npm package tests had failures or could not run${NC}"
|
||||
((OVERALL_FAIL++)) # Count as at least one failure
|
||||
fi
|
||||
|
||||
# Return to tests directory
|
||||
cd "$SCRIPT_DIR"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ package.json not found, skipping npm tests${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Node.js not found, skipping npm tests${NC}"
|
||||
echo -e "${GRAY} Install Node.js to run npm package tests${NC}"
|
||||
fi
|
||||
|
||||
# Final Summary
|
||||
echo ""
|
||||
print_test_header "FINAL TEST RESULTS"
|
||||
|
||||
# Update final counters with actual totals
|
||||
TOTAL_TESTS=$OVERALL_TOTAL
|
||||
PASS_COUNT=$OVERALL_PASS
|
||||
FAIL_COUNT=$OVERALL_FAIL
|
||||
|
||||
if [[ $TOTAL_TESTS -gt 0 ]]; then
|
||||
print_test_summary
|
||||
else
|
||||
echo -e "${YELLOW}No tests were executed${NC}"
|
||||
fi
|
||||
|
||||
print_final_result
|
||||
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# GLMT Integration Test Suite
|
||||
# Tests proxy startup, configuration, and basic functionality
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CCS_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "=== GLMT Integration Test Suite ==="
|
||||
echo ""
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
test_pass() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
((PASSED++))
|
||||
}
|
||||
|
||||
test_fail() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
((FAILED++))
|
||||
}
|
||||
|
||||
test_info() {
|
||||
echo -e "${YELLOW}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
# Test 1: Check GLMT profile exists in config
|
||||
echo "Test 1: GLMT profile configuration"
|
||||
if grep -q '"glmt"' ~/.ccs/config.json; then
|
||||
test_pass "GLMT profile found in config.json"
|
||||
else
|
||||
test_fail "GLMT profile NOT found in config.json"
|
||||
fi
|
||||
|
||||
# Test 2: Check GLMT settings file exists
|
||||
echo "Test 2: GLMT settings file"
|
||||
if [ -f ~/.ccs/glmt.settings.json ]; then
|
||||
test_pass "GLMT settings file exists"
|
||||
|
||||
# Check if API key is configured
|
||||
if grep -q "YOUR_GLM_API_KEY_HERE" ~/.ccs/glmt.settings.json; then
|
||||
test_info "API key not configured (still has placeholder)"
|
||||
else
|
||||
test_pass "API key configured"
|
||||
fi
|
||||
else
|
||||
test_fail "GLMT settings file NOT found"
|
||||
fi
|
||||
|
||||
# Test 3: Check transformer module
|
||||
echo "Test 3: Transformer module"
|
||||
if [ -f "$CCS_DIR/bin/glmt-transformer.js" ]; then
|
||||
test_pass "Transformer module exists"
|
||||
|
||||
# Test syntax
|
||||
if node --check "$CCS_DIR/bin/glmt-transformer.js" 2>/dev/null; then
|
||||
test_pass "Transformer syntax valid"
|
||||
else
|
||||
test_fail "Transformer syntax invalid"
|
||||
fi
|
||||
else
|
||||
test_fail "Transformer module NOT found"
|
||||
fi
|
||||
|
||||
# Test 4: Check proxy module
|
||||
echo "Test 4: Proxy module"
|
||||
if [ -f "$CCS_DIR/bin/glmt-proxy.js" ]; then
|
||||
test_pass "Proxy module exists"
|
||||
|
||||
# Test syntax
|
||||
if node --check "$CCS_DIR/bin/glmt-proxy.js" 2>/dev/null; then
|
||||
test_pass "Proxy syntax valid"
|
||||
else
|
||||
test_fail "Proxy syntax invalid"
|
||||
fi
|
||||
else
|
||||
test_fail "Proxy module NOT found"
|
||||
fi
|
||||
|
||||
# Test 5: Test proxy startup
|
||||
echo "Test 5: Proxy startup test"
|
||||
test_info "Starting proxy in background..."
|
||||
|
||||
# Start proxy
|
||||
node "$CCS_DIR/bin/glmt-proxy.js" &
|
||||
PROXY_PID=$!
|
||||
|
||||
# Wait for PROXY_READY signal (with timeout)
|
||||
TIMEOUT=5
|
||||
PORT=""
|
||||
for i in $(seq 1 $TIMEOUT); do
|
||||
if ps -p $PROXY_PID > /dev/null 2>&1; then
|
||||
sleep 0.2
|
||||
# Check if proxy outputted anything (we can't easily capture it in background)
|
||||
if [ $i -eq $TIMEOUT ]; then
|
||||
test_fail "Proxy started but PROXY_READY signal not captured (this is OK - proxy works)"
|
||||
PORT="unknown"
|
||||
fi
|
||||
else
|
||||
test_fail "Proxy failed to start or exited immediately"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ps -p $PROXY_PID > /dev/null 2>&1; then
|
||||
test_pass "Proxy process is running (PID: $PROXY_PID)"
|
||||
|
||||
# Kill proxy
|
||||
kill $PROXY_PID 2>/dev/null || true
|
||||
sleep 0.5
|
||||
|
||||
if ! ps -p $PROXY_PID > /dev/null 2>&1; then
|
||||
test_pass "Proxy terminated gracefully"
|
||||
else
|
||||
test_fail "Proxy did not terminate (killing forcefully)"
|
||||
kill -9 $PROXY_PID 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 6: Unit tests
|
||||
echo "Test 6: Unit tests"
|
||||
if [ -f "$CCS_DIR/tests/glmt-transformer.test.js" ]; then
|
||||
test_info "Running transformer unit tests..."
|
||||
if node "$CCS_DIR/tests/glmt-transformer.test.js" | grep -q "Passed: 12/12"; then
|
||||
test_pass "All 12 unit tests passed"
|
||||
else
|
||||
test_fail "Some unit tests failed"
|
||||
fi
|
||||
else
|
||||
test_fail "Unit test file NOT found"
|
||||
fi
|
||||
|
||||
# Test 7: Help text
|
||||
echo "Test 7: Help text verification"
|
||||
if ccs --help | grep -q "ccs glmt"; then
|
||||
test_pass "GLMT appears in help text"
|
||||
else
|
||||
test_fail "GLMT NOT in help text"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=== Test Summary ==="
|
||||
echo -e "Passed: ${GREEN}$PASSED${NC}"
|
||||
echo -e "Failed: ${RED}$FAILED${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Test with Claude Code: ccs glmt \"What is 2+2?\""
|
||||
echo " 2. Test thinking tags: ccs glmt \"<Thinking:On> Explain recursion\""
|
||||
echo " 3. Check thinking blocks appear in Claude Code UI"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed. Please review above.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,202 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# CCS v3.2.0 - Symlink Chain Validation Test (Windows)
|
||||
# Tests double symlink chain: instance → shared → claude
|
||||
|
||||
$TestDir = "$env:TEMP\ccs-symlink-test-$PID"
|
||||
$FakeClaude = "$TestDir\fake-claude"
|
||||
$FakeShared = "$TestDir\fake-shared"
|
||||
$FakeInstance = "$TestDir\fake-instance"
|
||||
|
||||
function Write-TestHeader {
|
||||
Write-Host "╔════════════════════════════════════════╗"
|
||||
Write-Host "║ CCS v3.2.0 Symlink Chain Test ║"
|
||||
Write-Host "╚════════════════════════════════════════╝"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Test-SymlinkChain {
|
||||
Write-Host "[i] Test 1: Basic symlink chain"
|
||||
|
||||
New-Item -ItemType Directory -Path "$FakeClaude\commands" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$FakeClaude\skills" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$FakeClaude\agents" -Force | Out-Null
|
||||
|
||||
"# Test Command" | Out-File "$FakeClaude\commands\test-cmd.md"
|
||||
"# Test Skill" | Out-File "$FakeClaude\skills\test-skill.md"
|
||||
"# Test Agent" | Out-File "$FakeClaude\agents\test-agent.md"
|
||||
|
||||
New-Item -ItemType Directory -Path $FakeShared -Force | Out-Null
|
||||
|
||||
try {
|
||||
# Test with symlinks (requires Developer Mode)
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeShared\commands" -Target "$FakeClaude\commands" -Force | Out-Null
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeShared\skills" -Target "$FakeClaude\skills" -Force | Out-Null
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeShared\agents" -Target "$FakeClaude\agents" -Force | Out-Null
|
||||
|
||||
New-Item -ItemType Directory -Path $FakeInstance -Force | Out-Null
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeInstance\commands" -Target "$FakeShared\commands" -Force | Out-Null
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeInstance\skills" -Target "$FakeShared\skills" -Force | Out-Null
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeInstance\agents" -Target "$FakeShared\agents" -Force | Out-Null
|
||||
|
||||
if ((Test-Path "$FakeInstance\commands\test-cmd.md") -and
|
||||
(Test-Path "$FakeInstance\skills\test-skill.md") -and
|
||||
(Test-Path "$FakeInstance\agents\test-agent.md")) {
|
||||
Write-Host "[OK] Symlink chain works (Developer Mode enabled)" -ForegroundColor Green
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[!] Symlink failed - Developer Mode required" -ForegroundColor Yellow
|
||||
Write-Host "[i] Testing copy fallback..." -ForegroundColor Yellow
|
||||
|
||||
# Test copy fallback
|
||||
Copy-Item -Path "$FakeClaude\commands" -Destination "$FakeShared\commands" -Recurse -Force
|
||||
Copy-Item -Path "$FakeClaude\skills" -Destination "$FakeShared\skills" -Recurse -Force
|
||||
Copy-Item -Path "$FakeClaude\agents" -Destination "$FakeShared\agents" -Recurse -Force
|
||||
|
||||
Copy-Item -Path "$FakeShared\commands" -Destination "$FakeInstance\commands" -Recurse -Force
|
||||
Copy-Item -Path "$FakeShared\skills" -Destination "$FakeInstance\skills" -Recurse -Force
|
||||
Copy-Item -Path "$FakeShared\agents" -Destination "$FakeInstance\agents" -Recurse -Force
|
||||
|
||||
if ((Test-Path "$FakeInstance\commands\test-cmd.md") -and
|
||||
(Test-Path "$FakeInstance\skills\test-skill.md") -and
|
||||
(Test-Path "$FakeInstance\agents\test-agent.md")) {
|
||||
Write-Host "[OK] Copy fallback works" -ForegroundColor Green
|
||||
return $false # Symlinks not available
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[X] Both symlink and copy failed" -ForegroundColor Red
|
||||
throw "Test failed"
|
||||
}
|
||||
|
||||
function Test-CircularDetection {
|
||||
Write-Host "[i] Test 2: Circular symlink detection"
|
||||
|
||||
$CircularTest = "$TestDir\circular-test"
|
||||
New-Item -ItemType Directory -Path "$CircularTest\shared" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$CircularTest\claude" -Force | Out-Null
|
||||
|
||||
try {
|
||||
# Create circular symlink: shared → claude → shared
|
||||
New-Item -ItemType SymbolicLink -Path "$CircularTest\claude\commands" -Target "$CircularTest\shared" -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
$item = Get-Item "$CircularTest\claude\commands" -ErrorAction SilentlyContinue
|
||||
if ($item -and $item.LinkType -eq "SymbolicLink") {
|
||||
$target = $item.Target
|
||||
if ($target -like "*\shared*") {
|
||||
Write-Host "[OK] Circular symlink detected correctly" -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Expected if symlinks not available
|
||||
Write-Host "[i] Circular detection skipped (symlinks not available)" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[X] Circular symlink detection failed" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Test-Performance {
|
||||
Write-Host "[i] Test 3: Performance measurement"
|
||||
|
||||
$PerfTest = "$TestDir\perf-test"
|
||||
New-Item -ItemType Directory -Path "$PerfTest\source" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$PerfTest\target" -Force | Out-Null
|
||||
|
||||
try {
|
||||
$start = Get-Date
|
||||
for ($i = 1; $i -le 100; $i++) {
|
||||
New-Item -ItemType SymbolicLink -Path "$PerfTest\target\link-$i" -Target "$PerfTest\source" -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$end = Get-Date
|
||||
$duration = ($end - $start).TotalMilliseconds
|
||||
$avg = [math]::Round($duration / 100, 2)
|
||||
Write-Host "[OK] Avg symlink creation: ${avg}ms (target: <1ms)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[i] Performance test skipped (symlinks not available)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
function Test-BrokenSymlink {
|
||||
Write-Host "[i] Test 4: Broken symlink detection"
|
||||
|
||||
$BrokenTest = "$TestDir\broken-test"
|
||||
New-Item -ItemType Directory -Path "$BrokenTest\instance" -Force | Out-Null
|
||||
|
||||
try {
|
||||
New-Item -ItemType SymbolicLink -Path "$BrokenTest\instance\commands" -Target "$BrokenTest\nonexistent" -Force -ErrorAction Stop | Out-Null
|
||||
|
||||
$item = Get-Item "$BrokenTest\instance\commands" -ErrorAction SilentlyContinue
|
||||
if ($item -and $item.LinkType -eq "SymbolicLink" -and !(Test-Path "$BrokenTest\instance\commands")) {
|
||||
Write-Host "[OK] Broken symlink detected correctly" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[X] Broken symlink detection failed" -ForegroundColor Red
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[i] Broken symlink test skipped (symlinks not available)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
function Test-LiveUpdates {
|
||||
Write-Host "[i] Test 5: Live updates through chain"
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path "$FakeClaude\commands" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $FakeShared -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $FakeInstance -Force | Out-Null
|
||||
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeShared\commands" -Target "$FakeClaude\commands" -Force -ErrorAction Stop | Out-Null
|
||||
New-Item -ItemType SymbolicLink -Path "$FakeInstance\commands" -Target "$FakeShared\commands" -Force -ErrorAction Stop | Out-Null
|
||||
|
||||
# Write to source
|
||||
"# Original" | Out-File "$FakeClaude\commands\live-test.md"
|
||||
|
||||
# Verify accessible from instance
|
||||
if (Test-Path "$FakeInstance\commands\live-test.md") {
|
||||
$content = Get-Content "$FakeInstance\commands\live-test.md" -Raw
|
||||
if ($content -match "# Original") {
|
||||
Write-Host "[OK] Live updates work through chain" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[X] Content mismatch" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Write-Host "[X] File not accessible through chain" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
# Update source
|
||||
"# Updated" | Out-File "$FakeClaude\commands\live-test.md"
|
||||
|
||||
# Verify update visible from instance
|
||||
$content = Get-Content "$FakeInstance\commands\live-test.md" -Raw
|
||||
if ($content -match "# Updated") {
|
||||
Write-Host "[OK] Live updates reflected instantly" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[X] Update not reflected" -ForegroundColor Red
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[i] Live updates test skipped (symlinks not available)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Write-TestHeader
|
||||
Write-Host "Running validation tests..."
|
||||
Write-Host ""
|
||||
|
||||
Test-SymlinkChain
|
||||
Test-CircularDetection
|
||||
Test-Performance
|
||||
Test-BrokenSymlink
|
||||
Test-LiveUpdates
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "╔════════════════════════════════════════╗"
|
||||
Write-Host "║ All Tests Passed ✓ ║"
|
||||
Write-Host "╚════════════════════════════════════════╝"
|
||||
} finally {
|
||||
Remove-Item $TestDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Executable
+173
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# CCS v3.2.0 - Symlink Chain Validation Test
|
||||
# Tests double symlink chain: instance → shared → claude
|
||||
|
||||
TEST_DIR="/tmp/ccs-symlink-test-$$"
|
||||
FAKE_CLAUDE="$TEST_DIR/fake-claude"
|
||||
FAKE_SHARED="$TEST_DIR/fake-shared"
|
||||
FAKE_INSTANCE="$TEST_DIR/fake-instance"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "╔════════════════════════════════════════╗"
|
||||
echo "║ CCS v3.2.0 Symlink Chain Test ║"
|
||||
echo "╚════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Test 1: Basic symlink chain
|
||||
test_symlink_chain() {
|
||||
echo "[i] Test 1: Basic symlink chain"
|
||||
|
||||
mkdir -p "$FAKE_CLAUDE/commands"
|
||||
mkdir -p "$FAKE_CLAUDE/skills"
|
||||
mkdir -p "$FAKE_CLAUDE/agents"
|
||||
|
||||
echo "# Test Command" > "$FAKE_CLAUDE/commands/test-cmd.md"
|
||||
echo "# Test Skill" > "$FAKE_CLAUDE/skills/test-skill.md"
|
||||
echo "# Test Agent" > "$FAKE_CLAUDE/agents/test-agent.md"
|
||||
|
||||
mkdir -p "$FAKE_SHARED"
|
||||
ln -s "$FAKE_CLAUDE/commands" "$FAKE_SHARED/commands"
|
||||
ln -s "$FAKE_CLAUDE/skills" "$FAKE_SHARED/skills"
|
||||
ln -s "$FAKE_CLAUDE/agents" "$FAKE_SHARED/agents"
|
||||
|
||||
mkdir -p "$FAKE_INSTANCE"
|
||||
ln -s "$FAKE_SHARED/commands" "$FAKE_INSTANCE/commands"
|
||||
ln -s "$FAKE_SHARED/skills" "$FAKE_INSTANCE/skills"
|
||||
ln -s "$FAKE_SHARED/agents" "$FAKE_INSTANCE/agents"
|
||||
|
||||
# Verify chain works for all directories
|
||||
if [[ -f "$FAKE_INSTANCE/commands/test-cmd.md" ]] && \
|
||||
[[ -f "$FAKE_INSTANCE/skills/test-skill.md" ]] && \
|
||||
[[ -f "$FAKE_INSTANCE/agents/test-agent.md" ]]; then
|
||||
echo "[OK] Symlink chain works for all directories"
|
||||
else
|
||||
echo "[X] Symlink chain broken"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 2: Circular symlink detection
|
||||
test_circular_detection() {
|
||||
echo "[i] Test 2: Circular symlink detection"
|
||||
|
||||
# Simulate circular symlink scenario
|
||||
mkdir -p "$TEST_DIR/circular-test/shared"
|
||||
mkdir -p "$TEST_DIR/circular-test/claude"
|
||||
|
||||
# Create circular symlink: shared → claude → shared
|
||||
ln -s "$TEST_DIR/circular-test/shared" "$TEST_DIR/circular-test/claude/commands"
|
||||
|
||||
# Detect if target is already symlink pointing back
|
||||
if [[ -L "$TEST_DIR/circular-test/claude/commands" ]]; then
|
||||
target=$(readlink "$TEST_DIR/circular-test/claude/commands")
|
||||
if [[ "$target" == *"/shared"* ]]; then
|
||||
echo "[OK] Circular symlink detected correctly"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[X] Circular symlink detection failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Test 3: Performance measurement
|
||||
test_performance() {
|
||||
echo "[i] Test 3: Performance measurement"
|
||||
|
||||
mkdir -p "$TEST_DIR/perf-test/source"
|
||||
mkdir -p "$TEST_DIR/perf-test/target"
|
||||
|
||||
start=$(date +%s%N 2>/dev/null || echo "0")
|
||||
for i in {1..100}; do
|
||||
ln -sf "$TEST_DIR/perf-test/source" "$TEST_DIR/perf-test/target/link-$i"
|
||||
done
|
||||
end=$(date +%s%N 2>/dev/null || echo "0")
|
||||
|
||||
if [[ "$start" != "0" ]] && [[ "$end" != "0" ]]; then
|
||||
duration=$(( (end - start) / 1000000 )) # Convert to ms
|
||||
avg=$(( duration / 100 ))
|
||||
echo "[OK] Avg symlink creation: ${avg}ms (target: <1ms)"
|
||||
else
|
||||
echo "[i] Performance timing not available on this platform"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 4: Broken symlink detection
|
||||
test_broken_symlink() {
|
||||
echo "[i] Test 4: Broken symlink detection"
|
||||
|
||||
mkdir -p "$TEST_DIR/broken-test/instance"
|
||||
ln -s "$TEST_DIR/broken-test/nonexistent" "$TEST_DIR/broken-test/instance/commands"
|
||||
|
||||
if [[ -L "$TEST_DIR/broken-test/instance/commands" ]] && \
|
||||
[[ ! -e "$TEST_DIR/broken-test/instance/commands" ]]; then
|
||||
echo "[OK] Broken symlink detected correctly"
|
||||
else
|
||||
echo "[X] Broken symlink detection failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 5: Live updates through chain
|
||||
test_live_updates() {
|
||||
echo "[i] Test 5: Live updates through chain"
|
||||
|
||||
# Clean test directories
|
||||
rm -rf "$TEST_DIR/live-test"
|
||||
mkdir -p "$TEST_DIR/live-test/claude/commands"
|
||||
mkdir -p "$TEST_DIR/live-test/shared"
|
||||
mkdir -p "$TEST_DIR/live-test/instance"
|
||||
|
||||
ln -s "$TEST_DIR/live-test/claude/commands" "$TEST_DIR/live-test/shared/commands"
|
||||
ln -s "$TEST_DIR/live-test/shared/commands" "$TEST_DIR/live-test/instance/commands"
|
||||
|
||||
# Write to source
|
||||
echo "# Original" > "$TEST_DIR/live-test/claude/commands/live-test.md"
|
||||
|
||||
# Verify accessible from instance
|
||||
if [[ -f "$TEST_DIR/live-test/instance/commands/live-test.md" ]]; then
|
||||
content=$(cat "$TEST_DIR/live-test/instance/commands/live-test.md")
|
||||
if [[ "$content" == "# Original" ]]; then
|
||||
echo "[OK] Live updates work through chain"
|
||||
else
|
||||
echo "[X] Content mismatch"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "[X] File not accessible through chain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Update source
|
||||
echo "# Updated" > "$TEST_DIR/live-test/claude/commands/live-test.md"
|
||||
|
||||
# Verify update visible from instance
|
||||
content=$(cat "$TEST_DIR/live-test/instance/commands/live-test.md")
|
||||
if [[ "$content" == "# Updated" ]]; then
|
||||
echo "[OK] Live updates reflected instantly"
|
||||
else
|
||||
echo "[X] Update not reflected"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run all tests
|
||||
echo "Running validation tests..."
|
||||
echo ""
|
||||
|
||||
test_symlink_chain
|
||||
test_circular_detection
|
||||
test_performance
|
||||
test_broken_symlink
|
||||
test_live_updates
|
||||
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════╗"
|
||||
echo "║ All Tests Passed ✓ ║"
|
||||
echo "╚════════════════════════════════════════╝"
|
||||
Executable
+632
@@ -0,0 +1,632 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const GlmtTransformer = require('../bin/glmt-transformer');
|
||||
const DeltaAccumulator = require('../bin/delta-accumulator');
|
||||
|
||||
/**
|
||||
* Token Counting Validation Tests
|
||||
*
|
||||
* Verifies:
|
||||
* 1. message_delta includes both input_tokens and output_tokens
|
||||
* 2. Token counts with simple prompts (no tools)
|
||||
* 3. Token counts with tool calls
|
||||
* 4. Token counts with thinking blocks + tools
|
||||
* 5. Deferred finalization waits for usage data
|
||||
* 6. Finalization happens when BOTH finish_reason AND usage are received
|
||||
* 7. Graceful degradation if usage never arrives
|
||||
* 8. No regressions in existing features
|
||||
*/
|
||||
|
||||
class TestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
}
|
||||
|
||||
test(name, fn) {
|
||||
this.tests.push({ name, fn });
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('\n=== Token Counting Validation Tests ===\n');
|
||||
|
||||
for (const { name, fn } of this.tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(`✓ ${name}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.error(`✗ ${name}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.error(` Stack: ${error.stack.split('\n').slice(1, 3).join('\n')}`);
|
||||
}
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n=== Results ===`);
|
||||
console.log(`Passed: ${this.passed}/${this.tests.length}`);
|
||||
console.log(`Failed: ${this.failed}/${this.tests.length}`);
|
||||
|
||||
return this.failed === 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertTrue(value, message) {
|
||||
if (!value) {
|
||||
throw new Error(message || 'Expected true');
|
||||
}
|
||||
}
|
||||
|
||||
function assertExists(value, message) {
|
||||
if (value === undefined || value === null) {
|
||||
throw new Error(message || 'Value should exist');
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeepEqual(actual, expected, message) {
|
||||
const actualStr = JSON.stringify(actual);
|
||||
const expectedStr = JSON.stringify(expected);
|
||||
if (actualStr !== expectedStr) {
|
||||
throw new Error(
|
||||
`${message || 'Deep equality failed'}\n` +
|
||||
` Expected: ${expectedStr}\n` +
|
||||
` Actual: ${actualStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const runner = new TestRunner();
|
||||
|
||||
// ========================================
|
||||
// Test 1: message_delta includes input_tokens and output_tokens
|
||||
// ========================================
|
||||
runner.test('message_delta includes both input_tokens and output_tokens', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
|
||||
// Simulate usage data
|
||||
accumulator.updateUsage({
|
||||
prompt_tokens: 150,
|
||||
completion_tokens: 75
|
||||
});
|
||||
accumulator.finishReason = 'stop';
|
||||
|
||||
const events = transformer.finalizeDelta(accumulator);
|
||||
|
||||
// Find message_delta event
|
||||
const messageDelta = events.find(e => e.event === 'message_delta');
|
||||
assertExists(messageDelta, 'message_delta event should exist');
|
||||
assertExists(messageDelta.data.usage, 'usage should exist in message_delta');
|
||||
assertEqual(messageDelta.data.usage.input_tokens, 150, 'input_tokens should be 150');
|
||||
assertEqual(messageDelta.data.usage.output_tokens, 75, 'output_tokens should be 75');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 2: Token counting with simple prompts (no tools)
|
||||
// ========================================
|
||||
runner.test('token counting with simple prompt (no tools)', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const openaiResponse = {
|
||||
id: 'chatcmpl-123',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Simple response'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 5,
|
||||
total_tokens: 15
|
||||
}
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
assertExists(result.usage, 'usage should exist');
|
||||
assertEqual(result.usage.input_tokens, 10, 'input_tokens should be 10');
|
||||
assertEqual(result.usage.output_tokens, 5, 'output_tokens should be 5');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 3: Token counting with tool calls
|
||||
// ========================================
|
||||
runner.test('token counting with tool calls', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const openaiResponse = {
|
||||
id: 'chatcmpl-456',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
arguments: '{"location":"London"}'
|
||||
}
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 25,
|
||||
total_tokens: 75
|
||||
}
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
assertExists(result.usage, 'usage should exist');
|
||||
assertEqual(result.usage.input_tokens, 50, 'input_tokens should be 50');
|
||||
assertEqual(result.usage.output_tokens, 25, 'output_tokens should be 25');
|
||||
assertEqual(result.stop_reason, 'tool_use', 'stop_reason should be tool_use');
|
||||
assertTrue(result.content.some(b => b.type === 'tool_use'), 'should have tool_use block');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 4: Token counting with thinking blocks + tools
|
||||
// ========================================
|
||||
runner.test('token counting with thinking blocks and tool calls', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const openaiResponse = {
|
||||
id: 'chatcmpl-789',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'Let me analyze this request...',
|
||||
content: 'I need to call a tool',
|
||||
tool_calls: [{
|
||||
id: 'call_2',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'calculate',
|
||||
arguments: '{"expression":"2+2"}'
|
||||
}
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 100,
|
||||
completion_tokens: 80,
|
||||
total_tokens: 180
|
||||
}
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
assertExists(result.usage, 'usage should exist');
|
||||
assertEqual(result.usage.input_tokens, 100, 'input_tokens should be 100');
|
||||
assertEqual(result.usage.output_tokens, 80, 'output_tokens should be 80');
|
||||
assertTrue(result.content.some(b => b.type === 'thinking'), 'should have thinking block');
|
||||
assertTrue(result.content.some(b => b.type === 'tool_use'), 'should have tool_use block');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 5: Deferred finalization waits for usage data
|
||||
// ========================================
|
||||
runner.test('deferred finalization waits for usage data', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
|
||||
// Simulate finish_reason arriving first
|
||||
accumulator.finishReason = 'stop';
|
||||
accumulator.messageStarted = true;
|
||||
|
||||
// Usage hasn't arrived yet - should NOT finalize
|
||||
assertEqual(accumulator.usageReceived, false, 'usageReceived should be false initially');
|
||||
|
||||
// Simulate transformDelta with finish_reason but no usage
|
||||
const openaiEvent1 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {},
|
||||
finish_reason: 'stop'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const events1 = transformer.transformDelta(openaiEvent1, accumulator);
|
||||
|
||||
// Should NOT have message_stop event yet
|
||||
const hasMessageStop1 = events1.some(e => e.event === 'message_stop');
|
||||
assertEqual(hasMessageStop1, false, 'should NOT finalize without usage');
|
||||
assertEqual(accumulator.finalized, false, 'accumulator should NOT be finalized');
|
||||
|
||||
// Now usage arrives
|
||||
const openaiEvent2 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
usage: {
|
||||
prompt_tokens: 200,
|
||||
completion_tokens: 100
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const events2 = transformer.transformDelta(openaiEvent2, accumulator);
|
||||
|
||||
// Should NOW finalize since we have both finish_reason AND usage
|
||||
const hasMessageStop2 = events2.some(e => e.event === 'message_stop');
|
||||
assertEqual(hasMessageStop2, true, 'should finalize when usage arrives');
|
||||
assertEqual(accumulator.finalized, true, 'accumulator should be finalized');
|
||||
assertEqual(accumulator.usageReceived, true, 'usageReceived should be true');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 6: Finalization happens when BOTH finish_reason AND usage received
|
||||
// ========================================
|
||||
runner.test('finalization waits for BOTH finish_reason AND usage', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
accumulator.messageStarted = true;
|
||||
|
||||
// Test case A: Usage arrives first
|
||||
const openaiEvent1 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
usage: {
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 30
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const events1 = transformer.transformDelta(openaiEvent1, accumulator);
|
||||
assertEqual(accumulator.usageReceived, true, 'usage should be received');
|
||||
assertEqual(accumulator.finalized, false, 'should NOT finalize with only usage');
|
||||
|
||||
// Test case B: finish_reason arrives second
|
||||
const openaiEvent2 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {},
|
||||
finish_reason: 'stop'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const events2 = transformer.transformDelta(openaiEvent2, accumulator);
|
||||
assertEqual(accumulator.finishReason, 'stop', 'finish_reason should be set');
|
||||
assertEqual(accumulator.finalized, true, 'should finalize when both present');
|
||||
|
||||
const messageDelta = events2.find(e => e.event === 'message_delta');
|
||||
assertExists(messageDelta, 'message_delta should exist');
|
||||
assertEqual(messageDelta.data.usage.input_tokens, 50, 'input_tokens in message_delta');
|
||||
assertEqual(messageDelta.data.usage.output_tokens, 30, 'output_tokens in message_delta');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 7: Graceful degradation if usage never arrives
|
||||
// ========================================
|
||||
runner.test('graceful degradation when usage never arrives', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
accumulator.messageStarted = true;
|
||||
|
||||
// finish_reason arrives
|
||||
accumulator.finishReason = 'stop';
|
||||
|
||||
// Simulate [DONE] event without usage
|
||||
const doneEvent = {
|
||||
event: 'done'
|
||||
};
|
||||
|
||||
const events = transformer.transformDelta(doneEvent, accumulator);
|
||||
|
||||
// Should finalize with zero tokens (graceful degradation)
|
||||
assertEqual(accumulator.finalized, true, 'should finalize on [DONE]');
|
||||
const messageDelta = events.find(e => e.event === 'message_delta');
|
||||
assertExists(messageDelta, 'message_delta should exist');
|
||||
assertEqual(messageDelta.data.usage.input_tokens, 0, 'input_tokens should be 0');
|
||||
assertEqual(messageDelta.data.usage.output_tokens, 0, 'output_tokens should be 0');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 8: No regression - thinking blocks still work
|
||||
// ========================================
|
||||
runner.test('no regression: thinking blocks still work', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
|
||||
// Start message
|
||||
const event1 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
delta: { role: 'assistant' }
|
||||
}]
|
||||
}
|
||||
};
|
||||
transformer.transformDelta(event1, accumulator);
|
||||
|
||||
// Thinking delta
|
||||
const event2 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {
|
||||
reasoning_content: 'Analyzing the problem...'
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
const events2 = transformer.transformDelta(event2, accumulator);
|
||||
|
||||
// Check thinking block was created
|
||||
const hasThinkingStart = events2.some(e =>
|
||||
e.event === 'content_block_start' &&
|
||||
e.data.content_block.type === 'thinking'
|
||||
);
|
||||
assertEqual(hasThinkingStart, true, 'thinking block should start');
|
||||
|
||||
const hasThinkingDelta = events2.some(e =>
|
||||
e.event === 'content_block_delta' &&
|
||||
e.data.delta.type === 'thinking_delta'
|
||||
);
|
||||
assertEqual(hasThinkingDelta, true, 'thinking delta should be emitted');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 9: No regression - tool calls execute correctly
|
||||
// ========================================
|
||||
runner.test('no regression: tool calls execute correctly', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
accumulator.messageStarted = true;
|
||||
|
||||
// Tool call delta
|
||||
const event = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: 'call_abc',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search',
|
||||
arguments: '{"q":"test"}'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const events = transformer.transformDelta(event, accumulator);
|
||||
|
||||
const toolUseStart = events.find(e =>
|
||||
e.event === 'content_block_start' &&
|
||||
e.data.content_block.type === 'tool_use'
|
||||
);
|
||||
assertExists(toolUseStart, 'tool_use block should start');
|
||||
assertEqual(toolUseStart.data.content_block.name, 'search', 'tool name should be search');
|
||||
|
||||
const inputJsonDelta = events.find(e =>
|
||||
e.event === 'content_block_delta' &&
|
||||
e.data.delta.type === 'input_json_delta'
|
||||
);
|
||||
assertExists(inputJsonDelta, 'input_json_delta should be emitted');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 10: No regression - streaming still works
|
||||
// ========================================
|
||||
runner.test('no regression: streaming still works', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
|
||||
// Message start
|
||||
const event1 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
model: 'GLM-4.6',
|
||||
choices: [{ delta: { role: 'assistant' } }]
|
||||
}
|
||||
};
|
||||
const events1 = transformer.transformDelta(event1, accumulator);
|
||||
assertTrue(events1.some(e => e.event === 'message_start'), 'message_start event');
|
||||
|
||||
// Text delta
|
||||
const event2 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { content: 'Hello' } }]
|
||||
}
|
||||
};
|
||||
const events2 = transformer.transformDelta(event2, accumulator);
|
||||
assertTrue(events2.some(e => e.event === 'content_block_start'), 'content_block_start');
|
||||
assertTrue(events2.some(e => e.event === 'content_block_delta'), 'content_block_delta');
|
||||
|
||||
// More text
|
||||
const event3 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { content: ' world' } }]
|
||||
}
|
||||
};
|
||||
const events3 = transformer.transformDelta(event3, accumulator);
|
||||
const textDelta = events3.find(e => e.data?.delta?.type === 'text_delta');
|
||||
assertExists(textDelta, 'text_delta should exist');
|
||||
assertEqual(textDelta.data.delta.text, ' world', 'delta text should be " world"');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 11: No regression - buffered mode still works
|
||||
// ========================================
|
||||
runner.test('no regression: buffered mode (non-streaming) works', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
|
||||
const openaiResponse = {
|
||||
id: 'chatcmpl-buffered',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'Thinking step by step...',
|
||||
content: 'Final answer'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 20,
|
||||
completion_tokens: 15,
|
||||
total_tokens: 35
|
||||
}
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
assertEqual(result.type, 'message', 'type should be message');
|
||||
assertEqual(result.role, 'assistant', 'role should be assistant');
|
||||
assertTrue(result.content.some(b => b.type === 'thinking'), 'has thinking');
|
||||
assertTrue(result.content.some(b => b.type === 'text'), 'has text');
|
||||
assertEqual(result.usage.input_tokens, 20, 'input_tokens');
|
||||
assertEqual(result.usage.output_tokens, 15, 'output_tokens');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 12: usageReceived flag is set correctly
|
||||
// ========================================
|
||||
runner.test('usageReceived flag is set when usage data arrives', () => {
|
||||
const accumulator = new DeltaAccumulator();
|
||||
|
||||
assertEqual(accumulator.usageReceived, false, 'initial value should be false');
|
||||
|
||||
accumulator.updateUsage({
|
||||
prompt_tokens: 100,
|
||||
completion_tokens: 50
|
||||
});
|
||||
|
||||
assertEqual(accumulator.usageReceived, true, 'should be true after updateUsage');
|
||||
assertEqual(accumulator.inputTokens, 100, 'inputTokens should be 100');
|
||||
assertEqual(accumulator.outputTokens, 50, 'outputTokens should be 50');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 13: Double finalization protection
|
||||
// ========================================
|
||||
runner.test('double finalization protection works', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
accumulator.messageStarted = true;
|
||||
accumulator.finishReason = 'stop';
|
||||
accumulator.updateUsage({ prompt_tokens: 10, completion_tokens: 5 });
|
||||
|
||||
// First finalization
|
||||
const events1 = transformer.finalizeDelta(accumulator);
|
||||
assertTrue(events1.length > 0, 'should return events on first finalization');
|
||||
assertEqual(accumulator.finalized, true, 'should be finalized');
|
||||
|
||||
// Second finalization attempt
|
||||
const events2 = transformer.finalizeDelta(accumulator);
|
||||
assertEqual(events2.length, 0, 'should return empty array on second call');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Test 14: Token counts in streaming with thinking + text + tools
|
||||
// ========================================
|
||||
runner.test('streaming: token counts with thinking + text + tools', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const accumulator = new DeltaAccumulator();
|
||||
|
||||
// Message start
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
model: 'GLM-4.6',
|
||||
choices: [{ delta: { role: 'assistant' } }]
|
||||
}
|
||||
}, accumulator);
|
||||
|
||||
// Thinking
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { reasoning_content: 'Thinking...' } }]
|
||||
}
|
||||
}, accumulator);
|
||||
|
||||
// Text
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { content: 'Answer' } }]
|
||||
}
|
||||
}, accumulator);
|
||||
|
||||
// Tool call
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'tool', arguments: '{}' }
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, accumulator);
|
||||
|
||||
// Usage arrives
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
usage: { prompt_tokens: 300, completion_tokens: 200 }
|
||||
}
|
||||
}, accumulator);
|
||||
|
||||
// finish_reason arrives
|
||||
const finalEvents = transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: {}, finish_reason: 'tool_calls' }]
|
||||
}
|
||||
}, accumulator);
|
||||
|
||||
// Verify message_delta has correct tokens
|
||||
const messageDelta = finalEvents.find(e => e.event === 'message_delta');
|
||||
assertExists(messageDelta, 'message_delta should exist');
|
||||
assertEqual(messageDelta.data.usage.input_tokens, 300, 'input_tokens should be 300');
|
||||
assertEqual(messageDelta.data.usage.output_tokens, 200, 'output_tokens should be 200');
|
||||
assertEqual(messageDelta.data.delta.stop_reason, 'tool_use', 'stop_reason should be tool_use');
|
||||
});
|
||||
|
||||
// Run all tests
|
||||
runner.run().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
}).catch(error => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+230
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const API_KEY = process.env.Z_AI_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
|
||||
const MODEL = 'GLM-4.6';
|
||||
|
||||
if (!API_KEY || API_KEY === 'your-api-key-here') {
|
||||
console.error('[ERROR] Z.AI API key not found');
|
||||
console.error('[INFO] Set Z_AI_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable');
|
||||
console.error('[INFO] Example: export Z_AI_API_KEY=your-key-here');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Test request with reasoning
|
||||
const requestBody = JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Solve this math problem step by step: What is 27 * 453? Show your reasoning process.'
|
||||
}
|
||||
],
|
||||
stream: true,
|
||||
reasoning: true,
|
||||
reasoning_effort: 'medium',
|
||||
max_tokens: 4096,
|
||||
do_sample: true
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'api.z.ai',
|
||||
port: 443,
|
||||
path: '/api/coding/paas/v4/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`,
|
||||
'Content-Length': Buffer.byteLength(requestBody),
|
||||
'User-Agent': 'CCS-GLMT-StreamingTest/1.0'
|
||||
}
|
||||
};
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
console.log('Z.AI STREAMING BEHAVIOR TEST');
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
console.log('[TEST] Initiating streaming request to Z.AI');
|
||||
console.log('[TEST] Model:', MODEL);
|
||||
console.log('[TEST] Stream: true');
|
||||
console.log('[TEST] Reasoning: true');
|
||||
console.log('[TEST] Reasoning Effort: medium');
|
||||
console.log('[TEST] API Key:', API_KEY.substring(0, 10) + '...');
|
||||
console.log('');
|
||||
|
||||
const startTime = Date.now();
|
||||
let firstByteTime = null;
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
console.log('[HTTP] Status:', res.statusCode);
|
||||
console.log('[HTTP] Content-Type:', res.headers['content-type']);
|
||||
console.log('');
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
let errorBody = '';
|
||||
res.on('data', chunk => errorBody += chunk);
|
||||
res.on('end', () => {
|
||||
console.error('[ERROR] Request failed:', res.statusCode, res.statusMessage);
|
||||
console.error('[ERROR] Response:', errorBody);
|
||||
process.exit(1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
console.log('SSE EVENTS (analyzing streaming behavior)');
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
console.log('');
|
||||
|
||||
let buffer = '';
|
||||
let eventCount = 0;
|
||||
let reasoningEventCount = 0;
|
||||
let reasoningInDelta = false;
|
||||
let reasoningInMessage = false;
|
||||
let totalReasoningChunks = [];
|
||||
let textEventCount = 0;
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
if (!firstByteTime) {
|
||||
firstByteTime = Date.now();
|
||||
console.log('[TIMING] Time to first byte:', firstByteTime - startTime, 'ms');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
buffer += chunk.toString();
|
||||
const lines = buffer.split('\n');
|
||||
|
||||
// Keep incomplete line in buffer
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('data: ')) {
|
||||
eventCount++;
|
||||
const data = line.substring(6);
|
||||
|
||||
if (data === '[DONE]') {
|
||||
console.log(`[EVENT ${eventCount}] [DONE]`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const choice = parsed.choices?.[0];
|
||||
const delta = choice?.delta;
|
||||
const message = choice?.message;
|
||||
|
||||
// Check for reasoning_content in delta
|
||||
if (delta?.reasoning_content) {
|
||||
reasoningInDelta = true;
|
||||
reasoningEventCount++;
|
||||
totalReasoningChunks.push(delta.reasoning_content);
|
||||
|
||||
console.log(`[EVENT ${eventCount}] *** REASONING IN DELTA ***`);
|
||||
console.log(' Delta keys:', Object.keys(delta).join(', '));
|
||||
console.log(' Reasoning chunk length:', delta.reasoning_content.length);
|
||||
console.log(' Reasoning chunk preview:', JSON.stringify(delta.reasoning_content.substring(0, 80)));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Check for reasoning_content in message (complete)
|
||||
if (message?.reasoning_content) {
|
||||
reasoningInMessage = true;
|
||||
console.log(`[EVENT ${eventCount}] *** REASONING IN MESSAGE (COMPLETE) ***`);
|
||||
console.log(' Message keys:', Object.keys(message).join(', '));
|
||||
console.log(' Reasoning total length:', message.reasoning_content.length);
|
||||
console.log(' Reasoning preview:', message.reasoning_content.substring(0, 100).replace(/\n/g, ' '));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Check for text content in delta
|
||||
if (delta?.content) {
|
||||
textEventCount++;
|
||||
if (textEventCount <= 3 || eventCount % 10 === 0) {
|
||||
console.log(`[EVENT ${eventCount}] Text delta`);
|
||||
console.log(' Content:', JSON.stringify(delta.content));
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
// Show finish_reason
|
||||
if (choice?.finish_reason) {
|
||||
console.log(`[EVENT ${eventCount}] Finish reason: ${choice.finish_reason}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Show usage stats
|
||||
if (parsed.usage) {
|
||||
console.log(`[EVENT ${eventCount}] Usage:`, JSON.stringify(parsed.usage));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(`[EVENT ${eventCount}] [PARSE ERROR]`, e.message);
|
||||
console.log(' Raw:', data.substring(0, 100));
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
console.log('TEST RESULTS');
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log('[TIMING]');
|
||||
console.log(' Time to first byte:', firstByteTime - startTime, 'ms');
|
||||
console.log(' Total duration:', totalTime, 'ms');
|
||||
console.log('');
|
||||
console.log('[EVENTS]');
|
||||
console.log(' Total events:', eventCount);
|
||||
console.log(' Text content events:', textEventCount);
|
||||
console.log(' Reasoning events:', reasoningEventCount);
|
||||
console.log('');
|
||||
console.log('[REASONING BEHAVIOR]');
|
||||
console.log(' Reasoning in delta (incremental):', reasoningInDelta);
|
||||
console.log(' Reasoning in message (complete):', reasoningInMessage);
|
||||
if (totalReasoningChunks.length > 0) {
|
||||
const totalReasoningLength = totalReasoningChunks.join('').length;
|
||||
console.log(' Total reasoning chunks:', totalReasoningChunks.length);
|
||||
console.log(' Total reasoning length:', totalReasoningLength);
|
||||
}
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
console.log('SCENARIO DETERMINATION');
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
|
||||
if (reasoningInDelta) {
|
||||
console.log('');
|
||||
console.log('[✓] SCENARIO A: Incremental Streaming');
|
||||
console.log(' reasoning_content appears in delta objects');
|
||||
console.log(' Multiple events contain reasoning chunks');
|
||||
console.log(' RECOMMENDATION: Proceed with full streaming implementation');
|
||||
} else if (reasoningInMessage) {
|
||||
console.log('');
|
||||
console.log('[!] SCENARIO B: Complete at End');
|
||||
console.log(' reasoning_content only in final message object');
|
||||
console.log(' No intermediate reasoning chunks');
|
||||
console.log(' RECOMMENDATION: Hybrid streaming (text streams, reasoning buffered)');
|
||||
} else {
|
||||
console.log('');
|
||||
console.log('[X] SCENARIO C: No Streaming Support');
|
||||
console.log(' No reasoning_content in streaming mode');
|
||||
console.log(' RECOMMENDATION: Keep buffered mode only');
|
||||
}
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('[ERROR]', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.write(requestBody);
|
||||
req.end();
|
||||
Reference in new issue
Block a user