mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
copilot-m365-web enterprise (work) tier support (#6334) (net +1/-0, tests OK). Integrated into release/v3.8.46.
This commit is contained in:
committed by
GitHub
parent
62b1bc9d05
commit
f5147d00f9
@@ -8,6 +8,7 @@
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** copilot-m365-web **enterprise / work tier** support ([#6334](https://github.com/diegosouzapw/OmniRoute/issues/6334)) — mirrors the EDU-tier pattern (#6210): `M365ConnectionParams` gains an `agent` field, a new opt-in `M365_ENTERPRISE_OVERRIDES` preset (`agent=work`, `scenario=officeweb`, `licenseType=Premium`) applies via `providerSpecificData.tier="enterprise"` (alias `"work"`), and `agent` is also overridable directly via `providerSpecificData.agent`. `buildWsUrl` was hardcoding `agent="web"` (the one enterprise-distinguishing param with no override path), so a Premium work account handshook then returned an empty stream. The individual and EDU paths are untouched. Kilo's dup flag vs #6210 (EDU tier) was a false positive — different tier. Regression guard: `tests/unit/copilot-m365-enterprise-6334.test.ts` (7). End-to-end confirmation on a real Premium work account is a live-VPS validation follow-up (Hard Rule #18). (thanks @Forcerecon)
|
||||
- **feat(api):** standardized, provider-agnostic **`effort` + `thinking` request params** ([#6241](https://github.com/diegosouzapw/OmniRoute/issues/6241)) — a thin standardization layer over the existing mature per-provider reasoning plumbing (no provider mapper touched). `providerChatCompletionSchema` gains a canonical `effort` (reusing the shared `none/low/medium/high/xhigh` vocabulary — the UI tiers `extra`/`max` collapse onto `xhigh`) and a boolean `thinking`. A pure `normalizeReasoningRequest` (wired once in `src/sse/handlers/chat.ts`, before any reasoning field is read) folds them onto the fields the translators already consume (`reasoning_effort` / `reasoning.effort` / `thinking`), so they fan out to Anthropic / Gemini / xAI / Responses — an explicit client `reasoning_effort` / object-shaped `thinking` always wins (backward-compatible). `/models` additively exposes `supportsThinking` + `effort_tiers` so the frontend can render the toggles (UI component is a follow-up). Regression guard: `tests/unit/effort-thinking-standardization-6241.test.ts` (12). (thanks @Iammilansoni, @shabeer)
|
||||
- **feat(combo):** new **`pipeline` (sequential) combo strategy** ([#6297](https://github.com/diegosouzapw/OmniRoute/issues/6297)) — the 18th routing strategy runs targets **in order**, threading each step's output into the next step's input, with an optional per-step `prompt` (system instruction); only the final step's response is returned. Distinct from `fusion` (parallel fan-out + judge). Implemented as a self-contained `open-sse/services/pipeline.ts` (sibling to `fusion.ts`), dispatched from `combo.ts`; the step list reuses `combo.models` order and reads an optional `prompt` off each target (backward-compatible — ignored by every other strategy). Intermediate steps run non-streaming with tools stripped (complete prose to thread forward); the final step keeps the client's `stream` flag + tools. A failing/empty/unparseable intermediate step fails the whole pipeline explicitly via a sanitized error (never silently swallowed). Kilo's dup flag vs #563 was a false positive (that's model→chain selection; this is a sequential chain). Regression guard: `tests/unit/combo-pipeline-strategy.test.ts` (5). (thanks @ofekbetzalel)
|
||||
- **feat(ci):** `check:test-masking` now flags **inline-reimplemented prod conditions** ([#6348](https://github.com/diegosouzapw/OmniRoute/issues/6348)) — a new **report-only** subcheck (v2, 6A.10 family) catches the wrong-shape contract test: a test that recomputes the condition under test inline instead of importing/exercising the real function (the #6216 class, where `=== 500` → `>= 500` stayed green because the test re-implemented the branch). For each added/modified test file it warns when the file textually duplicates a ≥3-token conditional from a production file touched in the same PR **and** does not import the symbol/module owning it, via a pure, fixture-tested `findReimplementedConditions()` with an allowlist mirroring `assertReductionAllowlist`. Report-only for now (does not fail the gate) — to be promoted to blocking after a triage cycle. Regression guard: `tests/unit/check-test-masking.test.ts` (45).
|
||||
|
||||
@@ -35,6 +35,19 @@ export const M365_EDU_OVERRIDES = {
|
||||
licenseType: "Starter",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Enterprise / "work" (Microsoft 365 Copilot for work) tier overrides (#6334). Enterprise
|
||||
* tenants ride the `agent="work"` BizChat surface with the `officeweb` scenario and a
|
||||
* Premium license. Opt-in via `providerSpecificData.tier="enterprise"` (alias `"work"`) so
|
||||
* the individual and EDU paths are unchanged. A raw `providerSpecificData.agent` override is
|
||||
* also honored for tenants that need a different agent value.
|
||||
*/
|
||||
export const M365_ENTERPRISE_OVERRIDES = {
|
||||
agent: "work",
|
||||
scenario: "officeweb",
|
||||
licenseType: "Premium",
|
||||
} as const;
|
||||
|
||||
export const M365_DEFAULT_VARIANTS = [
|
||||
"EnableMcpServerWidgets",
|
||||
"feature.EnableMcpServerWidgets",
|
||||
@@ -94,6 +107,7 @@ export interface M365ConnectionParams {
|
||||
scenario?: string;
|
||||
isEdu?: string;
|
||||
licenseType?: string;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
/** A new 32-hex chat session id (== XRoutingParameterSessionKey == clientrequestid). */
|
||||
@@ -174,16 +188,18 @@ export function resolveConnectionParams(
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve tier overrides (opt-in). `tier="edu"|"included"` applies the EDU overrides;
|
||||
* individual fields (`scenario`/`isEdu`/`licenseType`) can also be overridden directly
|
||||
* via providerSpecificData. Unset fields fall back to the individual defaults in
|
||||
* buildWsUrl. (#6210)
|
||||
* Resolve tier overrides (opt-in). `tier="edu"|"included"` applies the EDU overrides and
|
||||
* `tier="enterprise"|"work"` applies the enterprise/work overrides; individual fields
|
||||
* (`scenario`/`isEdu`/`licenseType`/`agent`) can also be overridden directly via
|
||||
* providerSpecificData. Unset fields fall back to the individual defaults in buildWsUrl.
|
||||
* (#6210, #6334)
|
||||
*/
|
||||
function resolveTierOverrides(
|
||||
psd: JsonRecord
|
||||
): Pick<M365ConnectionParams, "scenario" | "isEdu" | "licenseType"> {
|
||||
): Pick<M365ConnectionParams, "scenario" | "isEdu" | "licenseType" | "agent"> {
|
||||
const tier = typeof psd.tier === "string" ? psd.tier.toLowerCase() : "";
|
||||
const isEduTier = tier === "edu" || tier === "included";
|
||||
const isEnterpriseTier = tier === "enterprise" || tier === "work";
|
||||
const psdIsEdu =
|
||||
(typeof psd.isEdu === "string" && psd.isEdu) ||
|
||||
(typeof psd.isEdu === "boolean" && String(psd.isEdu)) ||
|
||||
@@ -191,11 +207,16 @@ function resolveTierOverrides(
|
||||
return {
|
||||
scenario:
|
||||
(typeof psd.scenario === "string" && psd.scenario) ||
|
||||
(isEduTier ? M365_EDU_OVERRIDES.scenario : undefined),
|
||||
(isEduTier ? M365_EDU_OVERRIDES.scenario : undefined) ||
|
||||
(isEnterpriseTier ? M365_ENTERPRISE_OVERRIDES.scenario : undefined),
|
||||
isEdu: psdIsEdu || (isEduTier ? M365_EDU_OVERRIDES.isEdu : undefined),
|
||||
licenseType:
|
||||
(typeof psd.licenseType === "string" && psd.licenseType) ||
|
||||
(isEduTier ? M365_EDU_OVERRIDES.licenseType : undefined),
|
||||
(isEduTier ? M365_EDU_OVERRIDES.licenseType : undefined) ||
|
||||
(isEnterpriseTier ? M365_ENTERPRISE_OVERRIDES.licenseType : undefined),
|
||||
agent:
|
||||
(typeof psd.agent === "string" && psd.agent) ||
|
||||
(isEnterpriseTier ? M365_ENTERPRISE_OVERRIDES.agent : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,7 +240,7 @@ export function buildWsUrl(params: M365ConnectionParams): string {
|
||||
agentHost: M365_INDIVIDUAL_DEFAULTS.agentHost,
|
||||
licenseType: params.licenseType ?? M365_INDIVIDUAL_DEFAULTS.licenseType,
|
||||
isEdu: params.isEdu ?? "false",
|
||||
agent: M365_INDIVIDUAL_DEFAULTS.agent,
|
||||
agent: params.agent ?? M365_INDIVIDUAL_DEFAULTS.agent,
|
||||
scenario: params.scenario ?? M365_INDIVIDUAL_DEFAULTS.scenario,
|
||||
});
|
||||
return `wss://${params.host}/m365Copilot/Chathub/${params.chathubPath}?${query.toString()}`;
|
||||
|
||||
@@ -51,6 +51,11 @@ import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
|
||||
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
|
||||
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
|
||||
import { assignEditApiKeyProviderSpecificData } from "./connectionProviderSpecificData";
|
||||
import {
|
||||
isM365TierCapableProvider,
|
||||
normalizeM365TierValue,
|
||||
type M365TierValue,
|
||||
} from "./m365Tier";
|
||||
import QuotaScrapingFields, { EMPTY_QUOTA_SCRAPING_FIELDS } from "./QuotaScrapingFields";
|
||||
|
||||
export interface EditConnectionModalConnection {
|
||||
@@ -132,6 +137,7 @@ export default function EditConnectionModal({
|
||||
passthroughModels: connection?.providerSpecificData?.passthroughModels === true,
|
||||
disableCooling: connection?.providerSpecificData?.disableCooling === true,
|
||||
importFreeModelsOnly: connection?.providerSpecificData?.importFreeModelsOnly === true,
|
||||
m365Tier: normalizeM365TierValue(connection?.providerSpecificData?.tier) as M365TierValue,
|
||||
});
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState(null);
|
||||
@@ -183,6 +189,7 @@ export default function EditConnectionModal({
|
||||
const localProviderMetadata = getLocalProviderMetadata(provider);
|
||||
const isLocalSelfHostedProvider = !!localProviderMetadata;
|
||||
const isGooglePse = provider === "google-pse-search";
|
||||
const isM365TierCapable = isM365TierCapableProvider(provider);
|
||||
const webSessionCredential = getWebSessionCredentialRequirement(provider);
|
||||
const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none";
|
||||
const isWebSessionCredential = !!webSessionCredential && webSessionCredential.kind !== "none";
|
||||
@@ -307,6 +314,7 @@ export default function EditConnectionModal({
|
||||
passthroughModels: connection?.providerSpecificData?.passthroughModels === true,
|
||||
disableCooling: connection?.providerSpecificData?.disableCooling === true,
|
||||
importFreeModelsOnly: connection?.providerSpecificData?.importFreeModelsOnly === true,
|
||||
m365Tier: normalizeM365TierValue(connection.providerSpecificData?.tier) as M365TierValue,
|
||||
});
|
||||
const existing = connection.providerSpecificData?.extraApiKeys;
|
||||
setExtraApiKeys(Array.isArray(existing) ? existing : []);
|
||||
@@ -325,7 +333,10 @@ export default function EditConnectionModal({
|
||||
setApiKeyHealth(health || {});
|
||||
setNewExtraKey("");
|
||||
setOpenRouterPreset(existingOpenRouterPreset);
|
||||
setShowAdvanced(!!existingCustomUserAgent);
|
||||
setShowAdvanced(
|
||||
!!existingCustomUserAgent ||
|
||||
normalizeM365TierValue(connection.providerSpecificData?.tier) !== ""
|
||||
);
|
||||
setTestResult(null);
|
||||
setValidationResult(null);
|
||||
setSaveError(null);
|
||||
@@ -876,6 +887,21 @@ export default function EditConnectionModal({
|
||||
placeholder="my-app/1.0"
|
||||
hint={t("customUserAgentHint")}
|
||||
/>
|
||||
{isM365TierCapable && (
|
||||
<Select
|
||||
label={t("m365TierLabel")}
|
||||
value={formData.m365Tier ?? ""}
|
||||
options={[
|
||||
{ value: "", label: t("m365TierIndividualOption") },
|
||||
{ value: "edu", label: t("m365TierEduOption") },
|
||||
{ value: "enterprise", label: t("m365TierEnterpriseOption") },
|
||||
]}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, m365Tier: e.target.value as M365TierValue })
|
||||
}
|
||||
hint={t("m365TierHint")}
|
||||
/>
|
||||
)}
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={formData.passthroughModels}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
assignCcCompatibleRequestDefaults,
|
||||
mergeCcCompatibleRequestDefaults,
|
||||
} from "./ccCompatibleRequestDefaults";
|
||||
import { applyM365Tier, isM365TierCapableProvider, type M365TierValue } from "./m365Tier";
|
||||
import {
|
||||
assignQuotaScrapingProviderData,
|
||||
type QuotaScrapingFieldValues,
|
||||
@@ -19,6 +20,7 @@ type FormData = QuotaScrapingFieldValues & {
|
||||
cx: string;
|
||||
excludedModels: string;
|
||||
importFreeModelsOnly: boolean;
|
||||
m365Tier?: M365TierValue;
|
||||
passthroughModels: boolean;
|
||||
region: string;
|
||||
routingTags: string;
|
||||
@@ -117,6 +119,7 @@ export function assignEditApiKeyProviderSpecificData(options: {
|
||||
o.target.accountId = o.formData.accountId.trim();
|
||||
}
|
||||
if (o.isAntigravityFamily) o.target.projectId = o.trimmedCloudCodeProjectId || null;
|
||||
if (isM365TierCapableProvider(o.provider)) applyM365Tier(o.target, o.formData.m365Tier ?? "");
|
||||
if (o.isCcCompatible) {
|
||||
o.target.requestDefaults = mergeCcCompatibleRequestDefaults(
|
||||
o.target.requestDefaults,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* M365 Copilot (BizChat) tier selector helpers (#6334).
|
||||
*
|
||||
* The `copilot-m365-web` executor (`open-sse/executors/copilot-m365-connection.ts`)
|
||||
* reads `providerSpecificData.tier` to pick the BizChat surface: unset/`individual`
|
||||
* uses the consumer defaults, `edu` (alias `included`) applies the education overrides,
|
||||
* and `enterprise` (alias `work`) applies the Microsoft 365 Copilot for-work overrides.
|
||||
*
|
||||
* These pure helpers gate the Advanced-Settings tier dropdown to tier-capable
|
||||
* providers and normalize the stored value <-> the dropdown value so the UI can be
|
||||
* unit-tested without a DOM.
|
||||
*/
|
||||
|
||||
/** Providers that expose the tier selector in connection Advanced Settings. */
|
||||
export const M365_TIER_CAPABLE_PROVIDERS = new Set<string>(["copilot-m365-web"]);
|
||||
|
||||
/** Dropdown value: "" is Individual (default / unset tier). */
|
||||
export type M365TierValue = "" | "edu" | "enterprise";
|
||||
|
||||
export function isM365TierCapableProvider(provider?: string | null): boolean {
|
||||
return !!provider && M365_TIER_CAPABLE_PROVIDERS.has(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a stored `providerSpecificData.tier` into the dropdown value.
|
||||
* Mirrors the executor's alias handling (`work`->enterprise, `included`->edu) so a
|
||||
* connection saved with an alias round-trips to the right option. Anything else
|
||||
* (unset, null, "individual", unknown) maps to "" (Individual).
|
||||
*/
|
||||
export function normalizeM365TierValue(raw: unknown): M365TierValue {
|
||||
const tier = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||
if (tier === "edu" || tier === "included") return "edu";
|
||||
if (tier === "enterprise" || tier === "work") return "enterprise";
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the selected tier onto a `providerSpecificData` target.
|
||||
*
|
||||
* `edu`/`enterprise` write the canonical tier string. Individual ("") writes
|
||||
* `null` — the PUT route merges `{ ...existing, ...incoming }`, so an omitted /
|
||||
* `undefined` key would keep a previously-saved tier; an explicit `null` overrides
|
||||
* it and the executor treats a non-string tier as Individual.
|
||||
*/
|
||||
export function applyM365Tier(target: Record<string, unknown>, tier: M365TierValue): void {
|
||||
if (tier === "edu" || tier === "enterprise") {
|
||||
target.tier = tier;
|
||||
} else {
|
||||
target.tier = null;
|
||||
}
|
||||
}
|
||||
@@ -4638,6 +4638,11 @@
|
||||
"localProviderBaseUrlHint": "Local Provider Base Url Hint",
|
||||
"localProviders": "Local Providers",
|
||||
"maxConcurrentWholeNumberError": "Max Concurrent Whole Number Error",
|
||||
"m365TierLabel": "Copilot tier",
|
||||
"m365TierHint": "Choose which Microsoft 365 Copilot surface this connection uses. Individual is the default consumer BizChat; Education and Enterprise (work) opt into their tenant surfaces.",
|
||||
"m365TierIndividualOption": "Individual (default)",
|
||||
"m365TierEduOption": "Education",
|
||||
"m365TierEnterpriseOption": "Enterprise / Work",
|
||||
"museSparkWebCookieHint": "Muse Spark Web Cookie Hint",
|
||||
"museSparkWebCookiePlaceholder": "Muse Spark Web Cookie Placeholder",
|
||||
"oauth": "Oauth",
|
||||
|
||||
129
tests/unit/copilot-m365-enterprise-6334.test.ts
Normal file
129
tests/unit/copilot-m365-enterprise-6334.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Feature test for #6334 — copilot-m365-web enterprise ("work") tier support.
|
||||
*
|
||||
* The individual path hardcoded `agent="web"`. Microsoft 365 Copilot for work rides the
|
||||
* `agent="work"` BizChat surface with `scenario="officeweb"` + a `Premium` license. This is
|
||||
* opt-in via `providerSpecificData.tier="enterprise"` (alias `"work"`), mirroring the EDU
|
||||
* tier (#6210). A raw `providerSpecificData.agent` override is also honored. The individual
|
||||
* and EDU tuples must remain unchanged.
|
||||
*
|
||||
* The live round-trip against a real M365 enterprise tenant is the separate Rule #18
|
||||
* validation and is NOT possible in this environment — documented as a live-validation
|
||||
* follow-up.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
buildWsUrl,
|
||||
resolveConnectionParams,
|
||||
M365_INDIVIDUAL_DEFAULTS,
|
||||
M365_ENTERPRISE_OVERRIDES,
|
||||
} from "../../open-sse/executors/copilot-m365-connection.ts";
|
||||
|
||||
const BASE_PARAMS = {
|
||||
host: "substrate.office.com",
|
||||
chathubPath: "user-oid@tenant-id",
|
||||
accessToken: "tok",
|
||||
};
|
||||
|
||||
// ── buildWsUrl: enterprise tuple ────────────────────────────────────────────
|
||||
|
||||
test("buildWsUrl: enterprise params emit agent=work + scenario=officeweb + Premium license [#6334]", () => {
|
||||
const url = new URL(
|
||||
buildWsUrl({
|
||||
...BASE_PARAMS,
|
||||
agent: M365_ENTERPRISE_OVERRIDES.agent,
|
||||
scenario: M365_ENTERPRISE_OVERRIDES.scenario,
|
||||
licenseType: M365_ENTERPRISE_OVERRIDES.licenseType,
|
||||
})
|
||||
);
|
||||
assert.equal(url.searchParams.get("agent"), "work");
|
||||
assert.equal(url.searchParams.get("scenario"), "officeweb");
|
||||
assert.equal(url.searchParams.get("licenseType"), "Premium");
|
||||
});
|
||||
|
||||
test("buildWsUrl: individual (default) tier is unchanged — agent=web [#6334]", () => {
|
||||
const url = new URL(buildWsUrl(BASE_PARAMS));
|
||||
assert.equal(url.searchParams.get("agent"), M365_INDIVIDUAL_DEFAULTS.agent);
|
||||
assert.equal(url.searchParams.get("agent"), "web");
|
||||
assert.equal(url.searchParams.get("scenario"), M365_INDIVIDUAL_DEFAULTS.scenario);
|
||||
assert.equal(url.searchParams.get("isEdu"), "false");
|
||||
});
|
||||
|
||||
// ── resolveConnectionParams: opt-in tier resolution ─────────────────────────
|
||||
|
||||
test("resolveConnectionParams: tier='enterprise' selects the enterprise/work tuple [#6334]", () => {
|
||||
const params = resolveConnectionParams({
|
||||
apiKey: "access_token=tok",
|
||||
providerSpecificData: { chathubPath: "user@tenant", tier: "enterprise" },
|
||||
} as never);
|
||||
assert.ok(!("error" in params), "should resolve without error");
|
||||
if (!("error" in params)) {
|
||||
assert.equal(params.agent, "work");
|
||||
assert.equal(params.scenario, "officeweb");
|
||||
assert.equal(params.licenseType, "Premium");
|
||||
const url = new URL(buildWsUrl(params));
|
||||
assert.equal(url.searchParams.get("agent"), "work");
|
||||
assert.equal(url.searchParams.get("scenario"), "officeweb");
|
||||
assert.equal(url.searchParams.get("licenseType"), "Premium");
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveConnectionParams: tier='work' is an alias for the enterprise tuple [#6334]", () => {
|
||||
const params = resolveConnectionParams({
|
||||
apiKey: "access_token=tok",
|
||||
providerSpecificData: { chathubPath: "user@tenant", tier: "work" },
|
||||
} as never);
|
||||
assert.ok(!("error" in params));
|
||||
if (!("error" in params)) {
|
||||
assert.equal(params.agent, "work");
|
||||
assert.equal(params.scenario, "officeweb");
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveConnectionParams: raw providerSpecificData.agent override flows into the URL [#6334]", () => {
|
||||
const params = resolveConnectionParams({
|
||||
apiKey: "access_token=tok",
|
||||
providerSpecificData: { chathubPath: "user@tenant", agent: "work" },
|
||||
} as never);
|
||||
assert.ok(!("error" in params));
|
||||
if (!("error" in params)) {
|
||||
assert.equal(params.agent, "work");
|
||||
const url = new URL(buildWsUrl(params));
|
||||
assert.equal(url.searchParams.get("agent"), "work");
|
||||
// No tier → scenario/licenseType fall back to individual defaults.
|
||||
assert.equal(url.searchParams.get("scenario"), M365_INDIVIDUAL_DEFAULTS.scenario);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Regressions: individual + EDU paths untouched ───────────────────────────
|
||||
|
||||
test("resolveConnectionParams: no tier keeps the individual default agent (web) [#6334]", () => {
|
||||
const params = resolveConnectionParams({
|
||||
apiKey: "access_token=tok",
|
||||
providerSpecificData: { chathubPath: "user@tenant" },
|
||||
} as never);
|
||||
assert.ok(!("error" in params));
|
||||
if (!("error" in params)) {
|
||||
// agent unset → buildWsUrl falls back to the individual default.
|
||||
assert.equal(params.agent, undefined);
|
||||
const url = new URL(buildWsUrl(params));
|
||||
assert.equal(url.searchParams.get("agent"), "web");
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveConnectionParams: tier='edu' tuple is unchanged and stays on agent=web [#6334]", () => {
|
||||
const params = resolveConnectionParams({
|
||||
apiKey: "access_token=tok",
|
||||
providerSpecificData: { chathubPath: "user@tenant", tier: "edu" },
|
||||
} as never);
|
||||
assert.ok(!("error" in params));
|
||||
if (!("error" in params)) {
|
||||
assert.equal(params.scenario, "OfficeWebIncludedCopilot");
|
||||
assert.equal(params.isEdu, "true");
|
||||
// EDU tier does not touch agent → individual default in the URL.
|
||||
assert.equal(params.agent, undefined);
|
||||
const url = new URL(buildWsUrl(params));
|
||||
assert.equal(url.searchParams.get("agent"), "web");
|
||||
}
|
||||
});
|
||||
61
tests/unit/m365-tier-selector-6334.test.ts
Normal file
61
tests/unit/m365-tier-selector-6334.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
applyM365Tier,
|
||||
isM365TierCapableProvider,
|
||||
normalizeM365TierValue,
|
||||
} from "../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/m365Tier.ts";
|
||||
|
||||
// #6334 — the connection Advanced-Settings tier dropdown must map its value to
|
||||
// providerSpecificData.tier the same way the copilot-m365-web executor reads it,
|
||||
// and selecting Individual must clear a previously-saved tier.
|
||||
|
||||
test("isM365TierCapableProvider only matches copilot-m365-web", () => {
|
||||
assert.equal(isM365TierCapableProvider("copilot-m365-web"), true);
|
||||
assert.equal(isM365TierCapableProvider("copilot-web"), false);
|
||||
assert.equal(isM365TierCapableProvider("openai"), false);
|
||||
assert.equal(isM365TierCapableProvider(""), false);
|
||||
assert.equal(isM365TierCapableProvider(undefined), false);
|
||||
assert.equal(isM365TierCapableProvider(null), false);
|
||||
});
|
||||
|
||||
test("normalizeM365TierValue maps stored tier (and aliases) to the dropdown value", () => {
|
||||
// Individual / unset
|
||||
assert.equal(normalizeM365TierValue(undefined), "");
|
||||
assert.equal(normalizeM365TierValue(null), "");
|
||||
assert.equal(normalizeM365TierValue(""), "");
|
||||
assert.equal(normalizeM365TierValue("individual"), "");
|
||||
assert.equal(normalizeM365TierValue("unknown-tier"), "");
|
||||
// Education (+ "included" alias, case-insensitive)
|
||||
assert.equal(normalizeM365TierValue("edu"), "edu");
|
||||
assert.equal(normalizeM365TierValue("EDU"), "edu");
|
||||
assert.equal(normalizeM365TierValue("included"), "edu");
|
||||
// Enterprise (+ "work" alias)
|
||||
assert.equal(normalizeM365TierValue("enterprise"), "enterprise");
|
||||
assert.equal(normalizeM365TierValue("work"), "enterprise");
|
||||
assert.equal(normalizeM365TierValue(" Work "), "enterprise");
|
||||
});
|
||||
|
||||
test("applyM365Tier writes the canonical tier for edu/enterprise", () => {
|
||||
const eduTarget: Record<string, unknown> = {};
|
||||
applyM365Tier(eduTarget, "edu");
|
||||
assert.equal(eduTarget.tier, "edu");
|
||||
|
||||
const entTarget: Record<string, unknown> = {};
|
||||
applyM365Tier(entTarget, "enterprise");
|
||||
assert.equal(entTarget.tier, "enterprise");
|
||||
});
|
||||
|
||||
test("applyM365Tier clears a previously-saved tier when Individual is selected", () => {
|
||||
// Simulates the edit flow: target starts as a copy of existing providerSpecificData
|
||||
// that already carries tier="enterprise". Individual must OVERRIDE it (null), not
|
||||
// merely omit the key — the PUT route merges { ...existing, ...incoming }, so an
|
||||
// omitted/undefined key would keep the stale value.
|
||||
const target: Record<string, unknown> = { tier: "enterprise", customUserAgent: "x" };
|
||||
applyM365Tier(target, "");
|
||||
assert.equal(target.tier, null);
|
||||
assert.equal(target.customUserAgent, "x");
|
||||
// And it round-trips back to Individual on reload.
|
||||
assert.equal(normalizeM365TierValue(target.tier), "");
|
||||
});
|
||||
Reference in New Issue
Block a user