Files
OmniRoute/tests/unit/9780-namespace-identity-pivot.test.ts
Diego Rodrigues de Sa e Souza a102a2d773 maint: final follow-up cherry-pick #9783 (#9904)
* fix(deps): bump transitive deps for 6 Dependabot + remaining audit vulns on main

Same overrides as #9464 (ip-address, hono, fast-uri, socket.io-parser, undici)
applied directly to main. Also covers brace-expansion (scoped), js-yaml v4 copies,
and mermaid.

npm audit: 6→0 vulnerabilities.
Closes Dependabot #161-#166.

* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190)

Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13
(with monaco-editor scoped override). Closes Dependabot #189, #190.

Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge —
awaiting Dependabot re-scan.

npm audit → 0 vulnerabilities.

* fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks)

_tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing
slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential
_tasks symlink can slip in via git add -A and, once pulled, checkout materializes
it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks
ignores the symlink too, preventing re-capture.

* fix(translator): keep Responses namespace identity across the hub-and-spoke pivot

Step 1 of the pivot (openai-responses -> openai) flattens namespace sub-tools
to a qualified wire name (#8295) and records the `{namespace, name}` pair on a
non-enumerable `_toolNameMap`. Step 2 (openai -> target) returns a brand-new
object, so the property was dropped for every non-OpenAI target. chatCore then
handed `null` to the #7936 response seam and namespace sub-tool calls reached
the client under their flattened name, which Codex rejects with
`unsupported call: <name>` — the symptom #7936 was opened to fix.

Copying `_toolNameMap` through is not viable: openai-to-claude and
openai-to-gemini publish their own `Map<string, string>` alias map on that same
property during step 2, so it carries two incompatible types. This adds a
dedicated `_namespaceToolIdentityMap`, propagated by translateRequest across
the pivot; chatCore prefers it and falls back to `_toolNameMap` for the
non-pivot producers. Both keys are stripped from the cliproxyapi wire body.

Fixes #9780

* fix(chat): reduce file size

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(chat): reduce combined file size

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(chat): reduce combined file size

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: VXNCXNX <vincent@preuve.ai>
2026-08-09 09:52:23 -03:00

156 lines
5.6 KiB
TypeScript

// #9780 — the Responses namespace identity map must survive the hub-and-spoke
// pivot in translator/index.ts. Step 1 flattens namespace sub-tools (#8295) and
// records `{namespace, name}`; step 2 returns a new object and used to drop it,
// leaving the #7936 seam with null and Codex rejecting `unsupported call`.
// A naive copy-through is not an option: openai-to-claude/gemini publish their
// own alias map on `_toolNameMap`, hence the dedicated channel asserted here.
import test from "node:test";
import assert from "node:assert/strict";
await import("../../open-sse/translator/bootstrap.ts");
const { translateRequest, initState } = await import("../../open-sse/translator/index.ts");
const { openaiToOpenAIResponsesResponse } = await import(
"../../open-sse/translator/response/openai-responses.ts"
);
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
type NamespaceIdentity = { namespace: string; name: string };
const NAMESPACE_REQUEST = {
model: "any-model",
instructions: "coding agent",
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "go" }] }],
tools: [
{
type: "namespace",
name: "functions",
tools: [
{
name: "exec",
description: "Run a shell command",
parameters: {
type: "object",
properties: { cmd: { type: "string" } },
required: ["cmd"],
},
},
],
},
],
};
function pivot(targetFormat: string): Record<string, unknown> {
return translateRequest(
"openai-responses",
targetFormat,
"any-model",
structuredClone(NAMESPACE_REQUEST),
true,
null,
null,
null
) as Record<string, unknown>;
}
function identityOf(body: Record<string, unknown>) {
const map = body._namespaceToolIdentityMap;
assert.ok(map instanceof Map, "expected a _namespaceToolIdentityMap after the pivot");
return map as Map<string, NamespaceIdentity>;
}
test("#9780: namespace identity survives the openai-responses -> kiro pivot", () => {
const identity = identityOf(pivot("kiro"));
assert.equal(identity.size, 1);
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
});
test("#9780: namespace identity survives the openai-responses -> cursor pivot", () => {
const identity = identityOf(pivot("cursor"));
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
});
// Regression guard: these two appeared to "keep" a map before the fix, but it
// was the alias map.
for (const target of ["claude", "gemini"]) {
test(`#9780: ${target} pivot keeps its alias map AND the namespace identity`, () => {
const body = pivot(target);
const identity = identityOf(body);
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
// The alias channel must be untouched: string values, not identities.
const aliases = body._toolNameMap;
assert.ok(aliases instanceof Map, `${target} must still publish its alias map`);
for (const value of (aliases as Map<string, unknown>).values()) {
assert.equal(typeof value, "string", `${target} alias values must stay strings`);
}
});
}
// Same-format requests are never flattened, so an absent map is correct here.
test("#9780: same-format openai-responses request is not flattened at all", () => {
const body = pivot("openai-responses");
const tools = body.tools as Array<Record<string, unknown>>;
assert.equal(tools[0].type, "namespace");
assert.equal((tools[0].tools as Array<{ name: string }>)[0].name, "exec");
assert.equal(body._namespaceToolIdentityMap, undefined);
});
test("#9780: the identity channel is non-enumerable and never serializes", () => {
const body = pivot("kiro");
assert.ok(body._namespaceToolIdentityMap instanceof Map);
assert.equal(
Object.prototype.propertyIsEnumerable.call(body, "_namespaceToolIdentityMap"),
false
);
assert.equal("_namespaceToolIdentityMap" in JSON.parse(JSON.stringify(body)), false);
});
// End-to-end: request pivot + response seam, i.e. what the Codex adjudicator
// actually receives. Before the fix every target emitted `functions__exec` with
// no namespace, which is the reported `unsupported call`.
for (const target of ["kiro", "cursor", "claude", "gemini"]) {
test(`#9780: ${target} round-trip returns the declared name and its namespace`, () => {
const body = pivot(target);
const state = initState(FORMATS.OPENAI_RESPONSES) as Record<string, unknown>;
state.requestToolIdentityMap = body._namespaceToolIdentityMap;
// The upstream echoes the flattened wire name (#8295).
const events = openaiToOpenAIResponsesResponse(
{
id: "chatcmpl-9780",
model: "any-model",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_9780",
type: "function",
function: { name: "functions__exec", arguments: '{"cmd":"git status"}' },
},
],
},
finish_reason: "tool_calls",
},
],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
},
state
) as Array<{ event: string; data: { item?: NamespaceIdentity } }>;
const added = events.find((e) => e.event === "response.output_item.added")?.data.item;
assert.ok(added, "expected response.output_item.added");
assert.deepEqual(
{ name: added.name, namespace: added.namespace },
{ name: "exec", namespace: "functions" }
);
});
}