mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
# Conflicts: # open-sse/executors/github.ts
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** lead Traffic Inspector with a purpose-first header that separates "what happened" from "how it happened", so beginners can read request outcomes without drowning in protocol detail ([#11283](https://github.com/diegosouzapw/OmniRoute/pull/11283))
|
||||
1
changelog.d/features/11286-essentials-sidebar-preset.md
Normal file
1
changelog.d/features/11286-essentials-sidebar-preset.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** add an Essentials sidebar preset that shows only the beginner core path (Home → Endpoints → API Keys → Providers → Health → Settings) while keeping Advanced tools reachable via Command Palette search ([#11286](https://github.com/diegosouzapw/OmniRoute/pull/11286))
|
||||
1
changelog.d/fixes/10815-kiro-social-multi-account.md
Normal file
1
changelog.d/fixes/10815-kiro-social-multi-account.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(oauth): stop treating the Kiro profile ARN as an account identity in `findKiroConnectionByIdentity()`, so a second Google/GitHub social login creates a new connection instead of overwriting the first — distinct Builder ID accounts share the same CodeWhisperer profile ARN, and the social token is not a JWT, so no e-mail was available to disambiguate them (#10815)
|
||||
@@ -1,3 +1,5 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
import {
|
||||
BaseExecutor,
|
||||
ExecuteInput,
|
||||
@@ -13,6 +15,11 @@ import {
|
||||
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
|
||||
import { stripUnsupportedParams } from "../translator/paramSupport.ts";
|
||||
|
||||
/** Correlation-id fallback for runtimes without crypto.randomUUID — still CSPRNG-backed. */
|
||||
function randomIdFallback(): string {
|
||||
return `${Date.now()}-${randomBytes(9).toString("hex")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Copilot credential refresh resolves to.
|
||||
*
|
||||
@@ -329,7 +336,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
...getGitHubCopilotChatHeaders(stream ? "text/event-stream" : "application/json", initiator),
|
||||
Authorization: `Bearer ${token}`,
|
||||
"x-request-id":
|
||||
crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
crypto.randomUUID?.() || randomIdFallback(),
|
||||
};
|
||||
|
||||
// Per-call / per-conversation / per-turn correlation ids the @github/copilot
|
||||
@@ -337,8 +344,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
// id (getGitHubCopilotMachineId) is stable per-install; these three are
|
||||
// fresh uuids. A Copilot-aware client may pin the session/task ids across a
|
||||
// conversation via its own headers — honor those when present, else mint.
|
||||
const genId = () =>
|
||||
crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const genId = () => crypto.randomUUID?.() || randomIdFallback();
|
||||
headers["x-interaction-id"] =
|
||||
this.readClientHeader(clientHeaders, "x-interaction-id") || genId();
|
||||
headers["x-client-session-id"] =
|
||||
|
||||
@@ -521,6 +521,7 @@ export default function SidebarTab() {
|
||||
|
||||
const presetLabels: Record<SidebarPresetId, string> = {
|
||||
all: getSettingsLabel("presetAll", "All"),
|
||||
essentials: getSettingsLabel("presetEssentials", "Essentials"),
|
||||
minimal: getSettingsLabel("presetMinimal", "Minimal"),
|
||||
developer: getSettingsLabel("presetDeveloper", "Developer"),
|
||||
admin: getSettingsLabel("presetAdmin", "Admin"),
|
||||
@@ -528,6 +529,10 @@ export default function SidebarTab() {
|
||||
|
||||
const presetDescriptions: Record<SidebarPresetId, string> = {
|
||||
all: getSettingsLabel("presetAllDesc", "Show everything"),
|
||||
essentials: getSettingsLabel(
|
||||
"presetEssentialsDesc",
|
||||
"Beginner path — Advanced tools stay searchable"
|
||||
),
|
||||
minimal: getSettingsLabel("presetMinimalDesc", "Core pages only"),
|
||||
developer: getSettingsLabel("presetDeveloperDesc", "Dev & proxy tools"),
|
||||
admin: getSettingsLabel("presetAdminDesc", "Monitoring & audit"),
|
||||
|
||||
@@ -15,7 +15,15 @@ import { HistoricSessionBanner } from "./components/session/HistoricSessionBanne
|
||||
|
||||
const BUFFER_MAX = 1000;
|
||||
|
||||
export function TrafficInspectorPageClient() {
|
||||
export function TrafficInspectorPageClient({
|
||||
title,
|
||||
subtitle,
|
||||
purpose,
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
purpose?: string;
|
||||
} = {}) {
|
||||
const [containerHeight, setContainerHeight] = useState(600);
|
||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [selectedRequest, setSelectedRequest] = useState<InterceptedRequest | null>(null);
|
||||
@@ -91,6 +99,18 @@ export function TrafficInspectorPageClient() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{title && (
|
||||
<div className="shrink-0 px-4 pt-4 pb-2">
|
||||
<h1 className="text-2xl font-bold text-text-main">{title}</h1>
|
||||
{subtitle && (
|
||||
<p className="text-sm text-text-muted mt-1 max-w-2xl">{subtitle}</p>
|
||||
)}
|
||||
{purpose && (
|
||||
<p className="text-xs text-text-muted mt-2 max-w-2xl italic">{purpose}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Capture modes toolbar */}
|
||||
<div className="shrink-0 px-4 pt-4 pb-2">
|
||||
<CaptureModesToolbar customHostCount={0} />
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function generateMetadata() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function TrafficInspectorPage() {
|
||||
return <TrafficInspectorPageClient />;
|
||||
export default async function TrafficInspectorPage() {
|
||||
const t = await getTranslations("sidebar");
|
||||
return <TrafficInspectorPageClient title={t("trafficInspector")} subtitle={t("trafficInspectorSubtitle")} purpose={t("trafficInspectorPurpose")} />;
|
||||
}
|
||||
|
||||
@@ -1272,7 +1272,8 @@
|
||||
"agentBridge": "Agent Bridge",
|
||||
"agentBridgeSubtitle": "Intercept IDE agent traffic",
|
||||
"trafficInspector": "Traffic Inspector",
|
||||
"trafficInspectorSubtitle": "Monitor LLM calls + debug any HTTPS traffic",
|
||||
"trafficInspectorSubtitle": "Inspect request and response traffic from your apps",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client.",
|
||||
"cliCode": "CLI Code",
|
||||
"cliCodeSubtitle": "Code tools pointing to OmniRoute",
|
||||
"cliAgents": "CLI Agents",
|
||||
@@ -6707,6 +6708,18 @@
|
||||
"sidebarVisibility": "Hide sidebar items",
|
||||
"sidebarVisibilityDesc": "Hide any sidebar navigation entry to reduce visual clutter without disabling any features",
|
||||
"sidebarVisibilityHint": "Any sidebar section is hidden automatically when all of its entries are hidden",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable.",
|
||||
"hideHealthLogs": "Hide Health Check Logs",
|
||||
"hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console",
|
||||
"themeAccent": "Theme color",
|
||||
|
||||
@@ -30,6 +30,27 @@ function providerData(connection: KiroConnectionLike): Record<string, unknown> {
|
||||
: {};
|
||||
}
|
||||
|
||||
/** True when the identity carries something that identifies the ACCOUNT (not the profile). */
|
||||
function hasAccountIdentifier(identity: KiroConnectionIdentity): boolean {
|
||||
return Boolean(folded(identity.email) || trimmed(identity.clientId));
|
||||
}
|
||||
|
||||
/** True when a shared field is present on both sides and disagrees — different accounts. */
|
||||
function contradictsAccount(
|
||||
connection: KiroConnectionLike,
|
||||
identity: KiroConnectionIdentity
|
||||
): boolean {
|
||||
const email = folded(identity.email);
|
||||
const existingEmail = folded(connection.email);
|
||||
if (email && existingEmail && email !== existingEmail) return true;
|
||||
|
||||
const clientId = trimmed(identity.clientId);
|
||||
const existingClientId = trimmed(providerData(connection).clientId);
|
||||
if (clientId && existingClientId && clientId !== existingClientId) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Find an existing Kiro account without comparing OAuth tokens or API keys. */
|
||||
export function findKiroConnectionByIdentity(
|
||||
connections: KiroConnectionLike[],
|
||||
@@ -45,7 +66,14 @@ export function findKiroConnectionByIdentity(
|
||||
const match = candidates.find(
|
||||
(connection) => trimmed(providerData(connection).profileArn) === profileArn
|
||||
);
|
||||
if (match) return match;
|
||||
// A profile ARN identifies the CodeWhisperer PROFILE, not the account: distinct
|
||||
// Builder ID accounts (Google/GitHub social login) share the same ARN. Accepting it
|
||||
// as identity made a second social login overwrite the first connection (#10815).
|
||||
// Only trust the ARN when the incoming identity carries an account-level identifier
|
||||
// that does not contradict the stored one.
|
||||
if (match && hasAccountIdentifier(identity) && !contradictsAccount(match, identity)) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
const clientId = trimmed(identity.clientId);
|
||||
|
||||
@@ -6,8 +6,11 @@ import { useTranslations } from "next-intl";
|
||||
import {
|
||||
SIDEBAR_SECTIONS,
|
||||
HIDDEN_SIDEBAR_ITEMS_SETTING_KEY,
|
||||
SIDEBAR_PRESET_KEY,
|
||||
ESSENTIALS_ADVANCED_TOOL_IDS,
|
||||
normalizeHiddenSidebarItems,
|
||||
resolveRuntimeSidebarSections,
|
||||
type HideableSidebarItemId,
|
||||
type SidebarItemDefinition,
|
||||
type SidebarSectionChild,
|
||||
} from "@/shared/constants/sidebarVisibility";
|
||||
@@ -61,6 +64,7 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [hiddenItems, setHiddenItems] = useState<Set<string>>(new Set());
|
||||
const [activePreset, setActivePreset] = useState<string | null>(null);
|
||||
const [radarAdminUrl, setRadarAdminUrl] = useState<unknown>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,6 +75,9 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
setHiddenItems(
|
||||
new Set(normalizeHiddenSidebarItems(data?.[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]))
|
||||
);
|
||||
setActivePreset(
|
||||
typeof data?.[SIDEBAR_PRESET_KEY] === "string" ? data[SIDEBAR_PRESET_KEY] : null
|
||||
);
|
||||
setRadarAdminUrl(data?.radarAdminUrl ?? null);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -104,7 +111,13 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
if (isSidebarGroup(child)) {
|
||||
const subgroupLabel = safeTranslate(child.titleKey, child.titleFallback);
|
||||
return child.items
|
||||
.filter((item) => !hiddenItems.has(item.id))
|
||||
.filter((item) => {
|
||||
if (!hiddenItems.has(item.id)) return true;
|
||||
return (
|
||||
activePreset === "essentials" &&
|
||||
ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId)
|
||||
);
|
||||
})
|
||||
.map<PaletteItem>((item) => ({
|
||||
id: item.id,
|
||||
href: item.href,
|
||||
@@ -121,7 +134,12 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
}));
|
||||
}
|
||||
const item = child as SidebarItemDefinition;
|
||||
if (hiddenItems.has(item.id)) return [];
|
||||
if (hiddenItems.has(item.id)) {
|
||||
const keepForEssentials =
|
||||
activePreset === "essentials" &&
|
||||
ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId);
|
||||
if (!keepForEssentials) return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: item.id,
|
||||
|
||||
@@ -202,6 +202,36 @@ export const SIDEBAR_ITEM_ORDER_KEY = "sidebarItemOrder";
|
||||
export const SIDEBAR_PRESET_KEY = "sidebarActivePreset";
|
||||
export const SIDEBAR_SETTINGS_UPDATED_EVENT = "omniroute:settings-updated";
|
||||
|
||||
/** Beginner Essentials: core path only. Advanced tools stay reachable via search. */
|
||||
const ESSENTIALS_SHOWN: ReadonlySet<HideableSidebarItemId> = new Set([
|
||||
"home",
|
||||
"endpoints",
|
||||
"api-manager",
|
||||
"providers",
|
||||
"health",
|
||||
"settings-general",
|
||||
"settings-sidebar",
|
||||
]);
|
||||
|
||||
/** Hidden in Essentials sidebar but kept searchable in Command Palette. */
|
||||
export const ESSENTIALS_ADVANCED_TOOL_IDS: ReadonlySet<HideableSidebarItemId> = new Set([
|
||||
"playground",
|
||||
"logs",
|
||||
"batch",
|
||||
"translator",
|
||||
"combos",
|
||||
"quota",
|
||||
"analytics",
|
||||
"costs",
|
||||
"cache",
|
||||
"runtime",
|
||||
"resilience-connections",
|
||||
"mcp",
|
||||
"a2a",
|
||||
"memory",
|
||||
"skills",
|
||||
]);
|
||||
|
||||
const MINIMAL_SHOWN: ReadonlySet<HideableSidebarItemId> = new Set([
|
||||
"home",
|
||||
"endpoints",
|
||||
@@ -297,6 +327,7 @@ function buildHiddenList(shown: ReadonlySet<HideableSidebarItemId>): HideableSid
|
||||
|
||||
export const SIDEBAR_PRESETS: readonly SidebarPresetDefinition[] = [
|
||||
{ id: "all", icon: "select_all", hiddenItems: [] },
|
||||
{ id: "essentials", icon: "star", hiddenItems: buildHiddenList(ESSENTIALS_SHOWN) },
|
||||
{ id: "minimal", icon: "minimize", hiddenItems: buildHiddenList(MINIMAL_SHOWN) },
|
||||
{ id: "developer", icon: "code", hiddenItems: buildHiddenList(DEVELOPER_SHOWN) },
|
||||
{ id: "admin", icon: "admin_panel_settings", hiddenItems: buildHiddenList(ADMIN_SHOWN) },
|
||||
|
||||
@@ -174,7 +174,7 @@ export interface SidebarSectionDefinition {
|
||||
defaultPinned?: boolean;
|
||||
}
|
||||
|
||||
export type SidebarPresetId = "all" | "minimal" | "developer" | "admin";
|
||||
export type SidebarPresetId = "all" | "essentials" | "minimal" | "developer" | "admin";
|
||||
|
||||
export interface SidebarPresetDefinition {
|
||||
id: SidebarPresetId;
|
||||
|
||||
@@ -199,7 +199,10 @@ export const updateSettingsSchema = z.object({
|
||||
.array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]]))
|
||||
.optional(),
|
||||
sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(),
|
||||
sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(),
|
||||
sidebarActivePreset: z
|
||||
.enum(["all", "essentials", "minimal", "developer", "admin"])
|
||||
.nullable()
|
||||
.optional(),
|
||||
comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),
|
||||
codexServiceTier: z
|
||||
.object({
|
||||
|
||||
@@ -23,8 +23,11 @@ export function unescapeWindowsShellArg(arg) {
|
||||
s = s.replace(/\^(.)/g, "$1").replace(/\^(.)/g, "$1");
|
||||
// 2. drop the wrapping quotes added by the CRT argv layer
|
||||
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) s = s.slice(1, -1);
|
||||
// 3. undo the doubled backslashes and the escaped embedded quotes
|
||||
s = s.replace(/\\\\/g, "\\").replace(/\\"/g, '"');
|
||||
// 3. undo the doubled backslashes and the escaped embedded quotes in a single
|
||||
// left-to-right pass — two sequential global replaces would let the first
|
||||
// pass's output feed the second (e.g. an escaped-backslash-then-quote
|
||||
// sequence could be misread), which is exactly what js/double-escaping flags.
|
||||
s = s.replace(/\\\\|\\"/g, (m) => (m === "\\\\" ? "\\" : '"'));
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,3 +79,49 @@ test("findKiroConnectionByIdentity never overwrites a different authentication t
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
// #10815 — a profile ARN identifies the CodeWhisperer profile, not the account: two
|
||||
// distinct social (Google/GitHub) Builder ID accounts share the same ARN, so matching
|
||||
// on it alone made the second login overwrite the first connection.
|
||||
const SHARED_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:1:profile/SHARED";
|
||||
|
||||
const firstSocialAccount = {
|
||||
id: "social-account-1",
|
||||
authType: "oauth",
|
||||
name: null,
|
||||
email: null,
|
||||
providerSpecificData: {
|
||||
profileArn: SHARED_PROFILE_ARN,
|
||||
authMethod: "imported",
|
||||
provider: "Github",
|
||||
},
|
||||
};
|
||||
|
||||
test("findKiroConnectionByIdentity does not match a shared profile ARN without an account identifier", () => {
|
||||
const match = findKiroConnectionByIdentity([firstSocialAccount], {
|
||||
authType: "oauth",
|
||||
profileArn: SHARED_PROFILE_ARN,
|
||||
email: null,
|
||||
});
|
||||
assert.equal(match, null);
|
||||
});
|
||||
|
||||
test("findKiroConnectionByIdentity treats diverging emails on a shared profile ARN as distinct accounts", () => {
|
||||
const stored = { ...firstSocialAccount, id: "social-a", email: "a@example.com" };
|
||||
const match = findKiroConnectionByIdentity([stored], {
|
||||
authType: "oauth",
|
||||
profileArn: SHARED_PROFILE_ARN,
|
||||
email: "b@example.com",
|
||||
});
|
||||
assert.equal(match, null);
|
||||
});
|
||||
|
||||
test("findKiroConnectionByIdentity still matches the same account on a shared profile ARN", () => {
|
||||
const stored = { ...firstSocialAccount, id: "social-a", email: "a@example.com" };
|
||||
const match = findKiroConnectionByIdentity([stored], {
|
||||
authType: "oauth",
|
||||
profileArn: SHARED_PROFILE_ARN,
|
||||
email: "a@example.com",
|
||||
});
|
||||
assert.equal(match?.id, "social-a");
|
||||
});
|
||||
|
||||
@@ -89,9 +89,10 @@ test("applyItemOrder ignores unknown IDs in order list", () => {
|
||||
|
||||
// ─── SIDEBAR_PRESETS ──────────────────────────────────────────────────────────
|
||||
|
||||
test("SIDEBAR_PRESETS contains all four preset IDs", () => {
|
||||
test("SIDEBAR_PRESETS contains all five preset IDs", () => {
|
||||
const ids = SIDEBAR_PRESETS.map((p) => p.id);
|
||||
assert.ok(ids.includes("all"), "expected 'all' preset");
|
||||
assert.ok(ids.includes("essentials"), "expected 'essentials' preset");
|
||||
assert.ok(ids.includes("minimal"), "expected 'minimal' preset");
|
||||
assert.ok(ids.includes("developer"), "expected 'developer' preset");
|
||||
assert.ok(ids.includes("admin"), "expected 'admin' preset");
|
||||
@@ -112,6 +113,30 @@ test("SIDEBAR_PRESETS 'all' preset has no hidden items", () => {
|
||||
assert.deepEqual(allPreset.hiddenItems, []);
|
||||
});
|
||||
|
||||
test("SIDEBAR_PRESETS includes essentials as the beginner path", () => {
|
||||
assert.equal(SIDEBAR_PRESETS.length, 5);
|
||||
assert.deepEqual(
|
||||
SIDEBAR_PRESETS.map((p) => p.id),
|
||||
["all", "essentials", "minimal", "developer", "admin"]
|
||||
);
|
||||
const essentials = SIDEBAR_PRESETS.find((p) => p.id === "essentials");
|
||||
assert.ok(essentials, "expected 'essentials' preset to exist");
|
||||
const hidden = new Set(essentials.hiddenItems);
|
||||
for (const id of [
|
||||
"home",
|
||||
"endpoints",
|
||||
"api-manager",
|
||||
"providers",
|
||||
"health",
|
||||
"settings-general",
|
||||
"settings-sidebar",
|
||||
]) {
|
||||
assert.equal(hidden.has(id as never), false, `${id} should stay visible in essentials`);
|
||||
}
|
||||
assert.equal(hidden.has("playground"), true);
|
||||
assert.equal(hidden.has("logs"), true);
|
||||
});
|
||||
|
||||
test("SIDEBAR_PRESETS non-all presets have at least one hidden item", () => {
|
||||
for (const preset of SIDEBAR_PRESETS.filter((p) => p.id !== "all")) {
|
||||
assert.ok(preset.hiddenItems.length > 0, `Preset '${preset.id}' should hide at least one item`);
|
||||
|
||||
45
tests/unit/sidebar-essentials-static.test.ts
Normal file
45
tests/unit/sidebar-essentials-static.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
test("essentials preset is registered in sidebar visibility types and presets", () => {
|
||||
const types = fs.readFileSync(
|
||||
path.join(repoRoot, "src/shared/constants/sidebarVisibility/types.ts"),
|
||||
"utf8"
|
||||
);
|
||||
const visibility = fs.readFileSync(
|
||||
path.join(repoRoot, "src/shared/constants/sidebarVisibility.ts"),
|
||||
"utf8"
|
||||
);
|
||||
const schema = fs.readFileSync(
|
||||
path.join(repoRoot, "src/shared/validation/settingsSchemas.ts"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert.match(types, /"essentials"/);
|
||||
assert.match(visibility, /id:\s*"essentials"/);
|
||||
assert.match(visibility, /ESSENTIALS_ADVANCED_TOOL_IDS/);
|
||||
assert.match(schema, /"essentials"/);
|
||||
});
|
||||
|
||||
test("command palette keeps essentials advanced tools searchable", () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(repoRoot, "src/shared/components/CommandPalette.tsx"),
|
||||
"utf8"
|
||||
);
|
||||
assert.match(source, /ESSENTIALS_ADVANCED_TOOL_IDS/);
|
||||
assert.match(source, /activePreset === "essentials"/);
|
||||
});
|
||||
|
||||
test("essentials i18n keys exist in en.json", () => {
|
||||
const en = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, "src/i18n/messages/en.json"), "utf8")
|
||||
) as { settings: Record<string, string> };
|
||||
assert.equal(en.settings.presetEssentials, "Essentials");
|
||||
assert.match(en.settings.presetEssentialsDesc, /Beginner path/i);
|
||||
assert.match(en.settings.presetEssentialsDesc, /searchable/i);
|
||||
});
|
||||
44
tests/unit/traffic-inspector-beginner-header.test.ts
Normal file
44
tests/unit/traffic-inspector-beginner-header.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const pagePath = path.join(
|
||||
repoRoot,
|
||||
"src/app/(dashboard)/dashboard/tools/traffic-inspector/page.tsx"
|
||||
);
|
||||
const clientPath = path.join(
|
||||
repoRoot,
|
||||
"src/app/(dashboard)/dashboard/tools/traffic-inspector/TrafficInspectorPageClient.tsx"
|
||||
);
|
||||
const enPath = path.join(repoRoot, "src/i18n/messages/en.json");
|
||||
|
||||
test("Traffic Inspector page passes translated title, subtitle, and purpose", () => {
|
||||
const pageSource = fs.readFileSync(pagePath, "utf8");
|
||||
assert.match(pageSource, /title=\{t\("trafficInspector"\)\}/);
|
||||
assert.match(pageSource, /subtitle=\{t\("trafficInspectorSubtitle"\)\}/);
|
||||
assert.match(pageSource, /purpose=\{t\("trafficInspectorPurpose"\)\}/);
|
||||
});
|
||||
|
||||
test("Traffic Inspector client renders purpose-first header when props are provided", () => {
|
||||
const clientSource = fs.readFileSync(clientPath, "utf8");
|
||||
assert.match(clientSource, /title\s*&&/);
|
||||
assert.match(clientSource, /subtitle\s*&&/);
|
||||
assert.match(clientSource, /purpose\s*&&/);
|
||||
});
|
||||
|
||||
test("Traffic Inspector beginner i18n keys exist in en.json", () => {
|
||||
const en = JSON.parse(fs.readFileSync(enPath, "utf8"));
|
||||
assert.equal(en.sidebar.trafficInspector, "Traffic Inspector");
|
||||
assert.equal(
|
||||
en.sidebar.trafficInspectorSubtitle,
|
||||
"Inspect request and response traffic from your apps"
|
||||
);
|
||||
assert.equal(
|
||||
typeof en.sidebar.trafficInspectorPurpose,
|
||||
"string"
|
||||
);
|
||||
assert.ok(en.sidebar.trafficInspectorPurpose.length > 20);
|
||||
});
|
||||
Reference in New Issue
Block a user