Files
2026-07-13 13:39:12 +08:00

291 lines
12 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
newStreamCtx,
processFrame,
buildCursorUsage,
type StreamCtx,
} from "../../open-sse/executors/cursor";
// ─── Wire-format helpers (mirror the encoder's primitives) ─────────────────
function v(n: number): Buffer {
const out: number[] = [];
while (n > 0x7f) {
out.push((n & 0x7f) | 0x80);
n >>>= 7;
}
out.push(n);
return Buffer.from(out);
}
function tag(field: number, wireType: number): Buffer {
return v((field << 3) | wireType);
}
function lenPrefixed(field: number, payload: Buffer): Buffer {
return Buffer.concat([tag(field, 2), v(payload.length), payload]);
}
// AgentServerMessage { interaction_update (1): { text_delta (1): { text (1): str } } }
function buildTextDeltaPayload(text: string): Buffer {
const tdu = lenPrefixed(1, Buffer.from(text, "utf8"));
const iu = lenPrefixed(1, tdu);
return lenPrefixed(1, iu);
}
// AgentServerMessage { interaction_update (1): { turn_ended (14): {} } }
function buildTurnEndedPayload(): Buffer {
const iu = lenPrefixed(14, Buffer.alloc(0));
return lenPrefixed(1, iu);
}
// AgentServerMessage { interaction_update (1): { token_delta (8): { count (1): n } } }
function buildTokenDeltaPayload(tokens: number): Buffer {
const tokDelta = Buffer.concat([tag(1, 0), v(tokens)]);
const iu = lenPrefixed(8, tokDelta);
return lenPrefixed(1, iu);
}
// AgentServerMessage { interaction_update (1): { thinking_delta (4): { text (1): str } } }
function buildThinkingDeltaPayload(text: string): Buffer {
const tdu = lenPrefixed(1, Buffer.from(text, "utf8"));
const iu = lenPrefixed(4, tdu);
return lenPrefixed(1, iu);
}
// AgentServerMessage { kv_server_message (4): {...} } — empty body
function buildKvServerMessagePayload(): Buffer {
return lenPrefixed(4, Buffer.alloc(0));
}
// JSON error payload (Connect-RPC error envelope)
function buildJsonErrorPayload(): Buffer {
return Buffer.from(
JSON.stringify({
error: { message: "rate limited", code: "resource_exhausted" },
}),
"utf8"
);
}
// ─── Tests ─────────────────────────────────────────────────────────────────
test("newStreamCtx initializes with empty state", () => {
const ctx = newStreamCtx("auto", () => {});
assert.equal(ctx.totalText, "");
assert.equal(ctx.tokenDelta, 0);
assert.equal(ctx.endReason, null);
assert.equal(ctx.emittedRoleChunk, false);
assert.equal(ctx.midStreamError, null);
assert.equal(ctx.model, "auto");
assert.match(ctx.responseId, /^chatcmpl-cursor-/);
});
test("processFrame emits role+content chunks for text deltas", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
processFrame(buildTextDeltaPayload("hello"), ctx, new Set());
assert.equal(emitted.length, 2, "role chunk then content chunk");
// First chunk: role
const first = JSON.parse(emitted[0].replace(/^data: /, "").trim());
assert.equal(first.choices[0].delta.role, "assistant");
// Second chunk: content
const second = JSON.parse(emitted[1].replace(/^data: /, "").trim());
assert.equal(second.choices[0].delta.content, "hello");
assert.equal(ctx.totalText, "hello");
assert.equal(ctx.receivedText, true);
});
test("processFrame skips role chunk on subsequent text deltas", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
processFrame(buildTextDeltaPayload("hello "), ctx, new Set());
processFrame(buildTextDeltaPayload("world"), ctx, new Set());
assert.equal(emitted.length, 3, "role + 2 content chunks");
assert.equal(ctx.totalText, "hello world");
});
test("processFrame sets endReason on turn_ended", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTurnEndedPayload(), ctx, new Set());
assert.equal(ctx.endReason, "turn_ended");
});
test("processFrame accumulates token_delta", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTokenDeltaPayload(42), ctx, new Set());
processFrame(buildTokenDeltaPayload(13), ctx, new Set());
assert.equal(ctx.tokenDelta, 55);
});
test("processFrame sets endReason on kv_server_message after text", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("hi"), ctx, new Set());
processFrame(buildKvServerMessagePayload(), ctx, new Set());
assert.equal(ctx.endReason, "kv_after_text");
assert.equal(ctx.kvAfterTextSeen, true);
});
test("buildCursorUsage degrades to prompt-only counts for an empty response", () => {
// emitUsage now always emits on the success path (OpenAI streaming contract),
// relying on buildCursorUsage producing a valid usage object even when the
// model returned no text/thinking/token_delta.
const ctx = newStreamCtx("auto", () => {});
const usage = buildCursorUsage(ctx, {
messages: [{ role: "user", content: "hi" }],
});
assert.equal(typeof usage.prompt_tokens, "number");
assert.equal(usage.completion_tokens, 0);
assert.equal(usage.total_tokens, usage.prompt_tokens);
assert.equal(usage.estimated, true);
});
test("processFrame ignores kv_server_message before text (no end signal yet)", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildKvServerMessagePayload(), ctx, new Set());
assert.equal(ctx.endReason, null);
assert.equal(ctx.kvAfterTextSeen, false);
});
test("processFrame captures mid-stream JSON error", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildJsonErrorPayload(), ctx, new Set());
assert.equal(ctx.endReason, "server_end");
assert.ok(ctx.midStreamError);
assert.match(ctx.midStreamError!.message, /rate limited/);
});
test("processFrame JSON error after text terminates without overwriting content", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("partial"), ctx, new Set());
processFrame(buildJsonErrorPayload(), ctx, new Set());
assert.equal(ctx.endReason, "server_end");
assert.equal(ctx.midStreamError, null, "no error overlay when text already streamed");
assert.equal(ctx.totalText, "partial");
});
test("processFrame doesn't ack same exec_id twice", () => {
// Simulate request_context appearing twice — only the first should ack.
// Phase 6 dedup is keyed by kind+execId+execMsgId so request_context and
// mcp_args sharing an empty execId don't collide.
const ctx = newStreamCtx("auto", () => {});
const acked = new Set<string>();
// Build an exec_request_context payload:
// ASM { exec_server_message (2): ESM { id (1): 1, exec_id (15): "x", request_context_args (10): {} } }
function buildRequestContext(execId: string): Buffer {
const esm = Buffer.concat([
Buffer.concat([tag(1, 0), v(1)]),
lenPrefixed(15, Buffer.from(execId, "utf8")),
lenPrefixed(10, Buffer.alloc(0)),
]);
return lenPrefixed(2, esm);
}
processFrame(buildRequestContext("x"), ctx, acked);
assert.ok(acked.has("exec_request_context:x:1"));
// Second call doesn't error or change state
processFrame(buildRequestContext("x"), ctx, acked);
assert.equal(acked.size, 1);
});
test("StreamCtx custom emit is invoked with full SSE-formatted strings", () => {
const captured: string[] = [];
const ctx = newStreamCtx("auto", (s) => captured.push(s));
processFrame(buildTextDeltaPayload("ok"), ctx, new Set());
// Each emit ends with \n\n and starts with "data: "
for (const s of captured) {
assert.match(s, /^data: /);
assert.ok(s.endsWith("\n\n"));
}
});
// ─── Thinking-delta capture and reasoning emission ─────────────────────────
test("processFrame accumulates thinking_delta into thinkingText and emits reasoning_content", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
processFrame(buildThinkingDeltaPayload("step 1: "), ctx, new Set());
processFrame(buildThinkingDeltaPayload("compute"), ctx, new Set());
assert.equal(ctx.thinkingText, "step 1: compute");
assert.equal(ctx.receivedText, true);
// role chunk + 2 reasoning_content chunks
assert.equal(emitted.length, 3);
const chunks = emitted.map((s) => JSON.parse(s.replace(/^data: /, "").trim()));
assert.equal(chunks[0].choices[0].delta.role, "assistant");
assert.equal(chunks[1].choices[0].delta.reasoning_content, "step 1: ");
assert.equal(chunks[2].choices[0].delta.reasoning_content, "compute");
});
test("processFrame on empty thinking text is a no-op (no role chunk, no emit)", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
// Build an empty-text thinking delta: thinking_delta(4) wraps an empty TDU
const iu = lenPrefixed(4, lenPrefixed(1, Buffer.from("", "utf8")));
const payload = lenPrefixed(1, iu);
processFrame(payload, ctx, new Set());
assert.equal(ctx.thinkingText, "");
assert.equal(ctx.emittedRoleChunk, false);
assert.equal(emitted.length, 0);
});
test("newStreamCtx initializes thinkingText to empty", () => {
const ctx = newStreamCtx("auto", () => {});
assert.equal(ctx.thinkingText, "");
});
// ─── Usage construction (buildCursorUsage) ─────────────────────────────────
const SAMPLE_BODY = { messages: [{ role: "user" as const, content: "hello world" }] };
test("buildCursorUsage uses cursor's real tokenDelta for completion_tokens", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("answer"), ctx, new Set());
processFrame(buildTokenDeltaPayload(42), ctx, new Set());
processFrame(buildTokenDeltaPayload(13), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, number>;
// 55 tokens + buffer added by addBufferToUsage (currently +5 per util convention)
assert.ok(usage.completion_tokens >= 55, `expected ≥55, got ${usage.completion_tokens}`);
assert.ok(usage.prompt_tokens > 0);
assert.equal(usage.total_tokens, usage.prompt_tokens + usage.completion_tokens);
});
test("buildCursorUsage falls back to estimate when tokenDelta is zero", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("abc"), ctx, new Set());
// No token_delta frames sent
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, number>;
assert.ok(usage.completion_tokens > 0);
assert.equal(usage.total_tokens, usage.prompt_tokens + usage.completion_tokens);
assert.equal((usage as Record<string, unknown>).estimated, true);
});
test("buildCursorUsage emits completion_tokens_details.reasoning_tokens when thinking seen", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildThinkingDeltaPayload("let me think about this carefully"), ctx, new Set());
processFrame(buildTextDeltaPayload("done"), ctx, new Set());
processFrame(buildTokenDeltaPayload(20), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, unknown>;
const details = usage.completion_tokens_details as Record<string, number> | undefined;
assert.ok(details, "completion_tokens_details should be present");
assert.ok(details!.reasoning_tokens > 0, "reasoning_tokens should be >0");
});
test("buildCursorUsage omits completion_tokens_details when no thinking", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("just text"), ctx, new Set());
processFrame(buildTokenDeltaPayload(10), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, unknown>;
assert.equal(usage.completion_tokens_details, undefined);
});
test("buildCursorUsage never emits cache fields (cursor doesn't expose them)", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildThinkingDeltaPayload("thinking"), ctx, new Set());
processFrame(buildTextDeltaPayload("text"), ctx, new Set());
processFrame(buildTokenDeltaPayload(20), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, unknown>;
assert.equal(usage.cached_tokens, undefined);
assert.equal(usage.cache_read_input_tokens, undefined);
assert.equal(usage.cache_creation_input_tokens, undefined);
});