chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { GithubExecutor } from "../../open-sse/executors/github.ts";
|
||||
|
||||
// GitHub Copilot's /chat/completions endpoint rejects a conversation that ends with an
|
||||
// assistant message: "This model does not support assistant message prefill. The
|
||||
// conversation must end with a user message." Anthropic clients (e.g. newest Claude
|
||||
// Desktop) send a trailing assistant turn as a prefill seed — the Anthropic API honors
|
||||
// it, but Copilot 400s. GithubExecutor.dropTrailingAssistantPrefill() strips the
|
||||
// trailing assistant message(s) before dispatch, scoped to the github executor only.
|
||||
// Port of 9router#2143 (author: Manuel <baslr@users.noreply.github.com>).
|
||||
|
||||
test("dropTrailingAssistantPrefill drops a single trailing assistant message", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const messages = [
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "assistant", content: "Here is the answer:" },
|
||||
];
|
||||
|
||||
const out = executor.dropTrailingAssistantPrefill(messages);
|
||||
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].role, "user");
|
||||
});
|
||||
|
||||
test("dropTrailingAssistantPrefill drops multiple consecutive trailing assistant messages", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const messages = [
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "assistant", content: "one" },
|
||||
{ role: "assistant", content: "two" },
|
||||
];
|
||||
|
||||
const out = executor.dropTrailingAssistantPrefill(messages);
|
||||
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].role, "user");
|
||||
});
|
||||
|
||||
test("dropTrailingAssistantPrefill is a no-op (same reference) when the conversation ends with a user message", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const messages = [
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "assistant", content: "Hello" },
|
||||
{ role: "user", content: "More" },
|
||||
];
|
||||
|
||||
const out = executor.dropTrailingAssistantPrefill(messages);
|
||||
|
||||
assert.equal(out, messages, "must return the same array reference when nothing changes");
|
||||
assert.equal(out.length, 3);
|
||||
});
|
||||
|
||||
test("dropTrailingAssistantPrefill is a no-op when the conversation ends with a tool message", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const messages = [
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "assistant", content: null, tool_calls: [{ id: "x" }] },
|
||||
{ role: "tool", tool_call_id: "x", content: "result" },
|
||||
];
|
||||
|
||||
const out = executor.dropTrailingAssistantPrefill(messages);
|
||||
|
||||
assert.equal(out, messages, "must return the same array reference when nothing changes");
|
||||
assert.equal(out.length, 3);
|
||||
assert.equal(out[2].role, "tool");
|
||||
});
|
||||
|
||||
test("dropTrailingAssistantPrefill never empties an assistant-only conversation", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const messages = [{ role: "assistant", content: "only" }];
|
||||
|
||||
const out = executor.dropTrailingAssistantPrefill(messages);
|
||||
|
||||
assert.equal(out.length, 1, "must keep at least one message");
|
||||
assert.equal(out[0].role, "assistant");
|
||||
});
|
||||
|
||||
test("dropTrailingAssistantPrefill is null/empty safe", () => {
|
||||
const executor = new GithubExecutor();
|
||||
|
||||
assert.deepEqual(executor.dropTrailingAssistantPrefill([]), []);
|
||||
assert.equal(executor.dropTrailingAssistantPrefill(undefined), undefined);
|
||||
assert.equal(executor.dropTrailingAssistantPrefill(null), null);
|
||||
});
|
||||
|
||||
test("GithubExecutor.transformRequest drops the trailing assistant prefill end-to-end", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const body = {
|
||||
model: "claude-sonnet-4.6",
|
||||
messages: [
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "assistant", content: "Here is the answer:" },
|
||||
],
|
||||
};
|
||||
|
||||
const out = executor.transformRequest("claude-sonnet-4.6", body, false, {});
|
||||
|
||||
assert.equal(out.messages.length, 1);
|
||||
assert.equal(out.messages[0].role, "user");
|
||||
});
|
||||
|
||||
test("GithubExecutor.transformRequest leaves a user-terminated conversation untouched end-to-end", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const body = {
|
||||
model: "claude-sonnet-4.6",
|
||||
messages: [
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "assistant", content: "Hello" },
|
||||
{ role: "user", content: "More" },
|
||||
],
|
||||
};
|
||||
|
||||
const out = executor.transformRequest("claude-sonnet-4.6", body, false, {});
|
||||
|
||||
assert.equal(out.messages.length, 3);
|
||||
assert.equal(out.messages[2].role, "user");
|
||||
});
|
||||
Reference in New Issue
Block a user