Files
OmniRoute/tests/unit/m365-connection-4042.test.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

145 lines
5.9 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
// #4042 — M365 Copilot (individual) connection helpers: credential resolution,
// WS URL building, token redaction, and prompt flattening. Pure functions, no
// live socket — the round-trip is the separate Rule #18 validation gate.
import {
M365_INDIVIDUAL_DEFAULTS,
newChatSessionId,
resolveConnectionParams,
buildWsUrl,
redactWsUrl,
buildPrompt,
} from "../../open-sse/executors/copilot-m365-connection.ts";
// ── Credential resolution ────────────────────────────────────────────────
test("resolveConnectionParams errors when the access_token is missing", () => {
const r = resolveConnectionParams(undefined);
assert.ok("error" in r);
assert.match((r as { error: string }).error, /access_token/i);
});
test("resolveConnectionParams errors when the Chathub path is missing", () => {
const r = resolveConnectionParams({ apiKey: "tok" });
assert.ok("error" in r);
assert.match((r as { error: string }).error, /Chathub path/i);
});
test("resolveConnectionParams reads token from apiKey and path from providerSpecificData", () => {
const r = resolveConnectionParams({
apiKey: "opaque-jwe-token",
providerSpecificData: { chathubPath: "user-oid@tenant-id" },
});
assert.ok(!("error" in r));
const p = r as { host: string; chathubPath: string; accessToken: string };
assert.equal(p.accessToken, "opaque-jwe-token");
assert.equal(p.chathubPath, "user-oid@tenant-id");
assert.equal(p.host, M365_INDIVIDUAL_DEFAULTS.host);
});
test("resolveConnectionParams accepts access_token in providerSpecificData and a custom host", () => {
const r = resolveConnectionParams({
providerSpecificData: {
access_token: "tok2",
userTenant: "u@t",
host: "substrate.svc.cloud.microsoft",
},
});
assert.ok(!("error" in r));
const p = r as { host: string; chathubPath: string; accessToken: string };
assert.equal(p.accessToken, "tok2");
assert.equal(p.chathubPath, "u@t");
assert.equal(p.host, "substrate.svc.cloud.microsoft");
});
test("resolveConnectionParams parses the pasted OmniRoute credential line", () => {
const r = resolveConnectionParams({
apiKey: "access_token=tok3; chathubPath=redacted-account@redacted-tenant",
});
assert.ok(!("error" in r));
const p = r as { chathubPath: string; accessToken: string };
assert.equal(p.accessToken, "tok3");
assert.equal(p.chathubPath, "redacted-account@redacted-tenant");
});
test("resolveConnectionParams parses a pasted Chathub WebSocket URL", () => {
const r = resolveConnectionParams({
apiKey:
"wss://substrate.office.com/m365Copilot/Chathub/redacted-account%40redacted-tenant?access_token=tok4&source=officeweb",
});
assert.ok(!("error" in r));
const p = r as { chathubPath: string; accessToken: string };
assert.equal(p.accessToken, "tok4");
assert.equal(p.chathubPath, "redacted-account@redacted-tenant");
});
// ── WS URL building ──────────────────────────────────────────────────────
test("buildWsUrl targets the substrate Chathub with the individual-tier query", () => {
const url = buildWsUrl({ host: "substrate.office.com", chathubPath: "u@t", accessToken: "TOK" });
assert.ok(url.startsWith("wss://substrate.office.com/m365Copilot/Chathub/u@t?"));
const qs = new URLSearchParams(url.split("?")[1]);
assert.equal(qs.get("licenseType"), "Starter");
assert.equal(qs.get("agent"), "web");
assert.equal(qs.get("scenario"), "OfficeWebPaidConsumerCopilot");
assert.equal(qs.get("source"), "officeweb");
assert.ok(qs.get("variants")?.includes("feature.bizchatfluxv3"));
assert.equal(qs.get("access_token"), "TOK");
// chatsessionid == XRoutingParameterSessionKey == clientrequestid (same value)
const sid = qs.get("chatsessionid");
assert.ok(sid && /^[0-9a-f]{32}$/.test(sid));
assert.equal(qs.get("XRoutingParameterSessionKey"), sid);
assert.equal(qs.get("clientrequestid"), sid);
});
test("redactWsUrl strips the access_token so the URL is safe to log", () => {
const url = buildWsUrl({
host: "substrate.office.com",
chathubPath: "u@t",
accessToken: "SECRET",
});
const redacted = redactWsUrl(url);
assert.ok(!redacted.includes("SECRET"), "token must not survive redaction");
assert.match(redacted, /access_token=REDACTED/);
});
test("newChatSessionId is 32 lowercase hex chars", () => {
const id = newChatSessionId();
assert.match(id, /^[0-9a-f]{32}$/);
assert.notEqual(newChatSessionId(), id);
});
// ── Prompt flattening ────────────────────────────────────────────────────
// Updated for full-history flattening (tool-call support follow-up to #10732):
// the prompt now carries the WHOLE bracketed history so multi-turn agent loops
// keep assistant turns and tool results, instead of the last user message only.
test("buildPrompt flattens the full message history in order", () => {
const prompt = buildPrompt({
messages: [
{ role: "user", content: "first" },
{ role: "assistant", content: "hi there" },
{ role: "user", content: "second" },
],
});
assert.ok(prompt.includes("[user]\nfirst"));
assert.ok(prompt.includes("[assistant]\nhi there"));
assert.ok(prompt.includes("[user]\nsecond"));
});
test("buildPrompt keeps system instructions first, bracketed", () => {
const prompt = buildPrompt({
messages: [
{ role: "system", content: "Be terse." },
{ role: "user", content: "hi" },
],
});
// The no-tool path prefixes the answer-in-full instruction, then the history.
assert.match(prompt, /^Please answer the following request in full\./);
assert.match(prompt, /\[system\]\nBe terse\./);
assert.ok(prompt.includes("[user]\nhi"));
});