165 lines
6.3 KiB
TypeScript
165 lines
6.3 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import path from "node:path";
|
|
|
|
const { parseSSEToOpenAIResponse, extractSSEErrorMessage } =
|
|
await import("../../open-sse/handlers/sseParser.ts");
|
|
|
|
const GENERIC = "Invalid SSE response for non-streaming request";
|
|
|
|
// ─── PART 2: error-only SSE passthrough ───────────────────────────────────────
|
|
|
|
test("extractSSEErrorMessage surfaces the error message from an error-only SSE chunk", () => {
|
|
const rawSSE = [
|
|
'data: {"error":{"message":"Devin CLI not found on PATH"}}',
|
|
"",
|
|
"data: [DONE]",
|
|
"",
|
|
].join("\n");
|
|
|
|
const surfaced = extractSSEErrorMessage(rawSSE);
|
|
|
|
assert.ok(surfaced, "expected a surfaced error message, got null");
|
|
assert.ok(
|
|
surfaced.includes("Devin CLI not found on PATH"),
|
|
`expected surfaced message to contain the real Devin error, got: ${surfaced}`
|
|
);
|
|
assert.notEqual(surfaced, GENERIC);
|
|
});
|
|
|
|
test("extractSSEErrorMessage surfaces the real Devin spawn error shape (type+code)", () => {
|
|
const rawSSE = [
|
|
'data: {"error":{"message":"Devin CLI not found: devin. Install via https://cli.devin.ai or set CLI_DEVIN_BIN env var.","type":"devin_cli_error","code":"spawn_failed"}}',
|
|
"",
|
|
"data: [DONE]",
|
|
"",
|
|
].join("\n");
|
|
|
|
const surfaced = extractSSEErrorMessage(rawSSE);
|
|
|
|
assert.ok(surfaced);
|
|
assert.ok(surfaced.includes("Devin CLI not found"));
|
|
});
|
|
|
|
test("extractSSEErrorMessage accepts a top-level string error field", () => {
|
|
const rawSSE = ['data: {"error":"upstream blew up"}', "", "data: [DONE]"].join("\n");
|
|
|
|
const surfaced = extractSSEErrorMessage(rawSSE);
|
|
|
|
assert.ok(surfaced);
|
|
assert.ok(surfaced.includes("upstream blew up"));
|
|
});
|
|
|
|
test("extractSSEErrorMessage returns null when a valid choices chunk is present (no false positive)", () => {
|
|
const rawSSE = [
|
|
'data: {"id":"chatcmpl_ok","choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":"stop"}]}',
|
|
"data: [DONE]",
|
|
].join("\n");
|
|
|
|
assert.equal(extractSSEErrorMessage(rawSSE), null);
|
|
});
|
|
|
|
test("extractSSEErrorMessage returns null for a stream with no error and no choices", () => {
|
|
const rawSSE = ['data: {"foo":"bar"}', "data: [DONE]"].join("\n");
|
|
|
|
assert.equal(extractSSEErrorMessage(rawSSE), null);
|
|
});
|
|
|
|
test("extractSSEErrorMessage sanitizes stack-trace-like error messages (no `at /` leak)", () => {
|
|
// Genuine V8 stack shape: real newline between the message and the frames, so
|
|
// a JSON.stringify of a real Error.stack-style string round-trips with the
|
|
// newline intact (sanitizeErrorMessage drops everything after the first line).
|
|
const stacky =
|
|
"ENOENT: spawn devin\n at ChildProcess._handle.onexit (/home/me/app/open-sse/executors/devin-cli.ts:170:5)";
|
|
// A bare absolute source path on the surviving first line must also be redacted.
|
|
const bareInline = "Devin failed loading /home/me/app/open-sse/executors/devin-cli.ts module";
|
|
const rawSSE = [
|
|
`data: ${JSON.stringify({ error: { message: stacky } })}`,
|
|
"",
|
|
`data: ${JSON.stringify({ error: { message: bareInline } })}`,
|
|
"",
|
|
"data: [DONE]",
|
|
].join("\n");
|
|
|
|
const surfaced = extractSSEErrorMessage(rawSSE);
|
|
|
|
assert.ok(surfaced, "expected a sanitized error message");
|
|
// The first error-only chunk wins; its stack tail is dropped at the newline.
|
|
assert.equal(surfaced, "ENOENT: spawn devin");
|
|
assert.ok(!surfaced.includes("at /"), `surfaced message leaked a stack frame path: ${surfaced}`);
|
|
assert.ok(
|
|
!/\/[^\s]+\.ts/.test(surfaced),
|
|
`surfaced message leaked an absolute source path: ${surfaced}`
|
|
);
|
|
|
|
// A bare absolute source path on a single line is redacted to <path>.
|
|
const bareSurfaced = extractSSEErrorMessage(
|
|
[`data: ${JSON.stringify({ error: { message: bareInline } })}`, "", "data: [DONE]"].join("\n")
|
|
);
|
|
assert.ok(bareSurfaced);
|
|
assert.ok(
|
|
bareSurfaced.includes("<path>"),
|
|
`expected the bare absolute path to be redacted, got: ${bareSurfaced}`
|
|
);
|
|
assert.ok(!/\/[^\s]+\.ts/.test(bareSurfaced), `bare-path message leaked: ${bareSurfaced}`);
|
|
});
|
|
|
|
// ─── Regression: the normal valid-SSE parse path still works ───────────────────
|
|
|
|
test("parseSSEToOpenAIResponse still parses a normal valid SSE (no regression)", () => {
|
|
const rawSSE = [
|
|
'data: {"id":"chatcmpl_ok","model":"gpt-4o-mini","choices":[{"index":0,"delta":{"content":"hello"},"finish_reason":"stop"}]}',
|
|
"data: [DONE]",
|
|
].join("\n");
|
|
|
|
const parsed = parseSSEToOpenAIResponse(rawSSE, "fallback-model");
|
|
|
|
assert.ok(parsed);
|
|
assert.equal(parsed.choices[0].message.content, "hello");
|
|
});
|
|
|
|
test("parseSSEToOpenAIResponse still returns null for an error-only SSE (boundary owns the error path)", () => {
|
|
const rawSSE = ['data: {"error":{"message":"Devin CLI not found on PATH"}}', "data: [DONE]"].join(
|
|
"\n"
|
|
);
|
|
|
|
// The valid-SSE parser intentionally returns null here (no `choices`); the
|
|
// error surfacing is the job of extractSSEErrorMessage at the boundary.
|
|
assert.equal(parseSSEToOpenAIResponse(rawSSE, "fallback-model"), null);
|
|
});
|
|
|
|
// ─── PART 1: windsurf instruction text references the IDE command-palette flow ─
|
|
|
|
test("PART 1: windsurf authHint references the `Windsurf: Provide Auth Token` command", () => {
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
// After the providers.ts oauth-constants split, the windsurf authHint moved to
|
|
// src/shared/constants/providers/oauth.ts.
|
|
const providers = readFileSync(
|
|
path.join(here, "../../src/shared/constants/providers/oauth.ts"),
|
|
"utf8"
|
|
);
|
|
|
|
// The windsurf authHint must lead with the IDE command-palette flow.
|
|
assert.match(
|
|
providers,
|
|
/Windsurf: Provide Auth Token/,
|
|
"providers.ts windsurf authHint should reference the `Windsurf: Provide Auth Token` command"
|
|
);
|
|
});
|
|
|
|
test("PART 1: oauth route 410 errors reference the IDE command-palette flow", () => {
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const route = readFileSync(
|
|
path.join(here, "../../src/app/api/oauth/[provider]/[action]/route.ts"),
|
|
"utf8"
|
|
);
|
|
|
|
assert.match(
|
|
route,
|
|
/Windsurf: Provide Auth Token/,
|
|
"oauth route should direct users to the `Windsurf: Provide Auth Token` command"
|
|
);
|
|
});
|