fix(web): stop O2 AI chat opening via the ungated Ctrl+B shortcut on OSS (#14165)

Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
This commit is contained in:
Shrinath Rao
2026-09-07 05:53:58 +00:00
committed by GitHub
co-authored by ktx-vaidehi ktx-vaidehi
parent 0e557a11f9
commit 630ecf41f3
4 changed files with 146 additions and 8 deletions
+18
View File
@@ -113,6 +113,7 @@ vi.mock("@/composables/contextProviders", () => ({
// Component import must come after all vi.mock() declarations.
import O2AIChat from "./O2AIChat.vue";
import { getManager, resetManager } from "@/lib/vue-shortcut-manager";
// ── Stub definitions ─────────────────────────────────────────────────────────
@@ -241,6 +242,23 @@ describe("O2AIChat", () => {
});
});
describe("Ctrl+B ownership", () => {
beforeEach(() => resetManager());
afterEach(() => resetManager());
// A second ctrl+b here overwrites MainLayout's gated aiChatToggle (same key + scope).
it("should not register ctrl+b, leaving the gated global binding in place", () => {
wrapper = mountO2AIChat({ isOpen: false });
const keys = getManager()!
.getAll()
.map((s) => s.key);
expect(keys).not.toContain("ctrl+b");
expect(keys).not.toContain("meta+b");
expect(keys).toContain("escape");
});
});
describe("addNewChat", () => {
beforeEach(() => {
wrapper = mountO2AIChat({ isOpen: true });
-7
View File
@@ -3536,13 +3536,6 @@ export default defineComponent({
}
},
},
{
id: "aiChatExpand",
key: "ctrl+b",
keyForMac: "meta+b",
description: t("shortcuts.actions.aiChatExpand"),
handler: toggleExpand,
},
]);
const addNewChat = () => {
@@ -0,0 +1,122 @@
// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mount, VueWrapper, flushPromises } from "@vue/test-utils";
import { defineComponent, h } from "vue";
import store from "@/test/unit/helpers/store";
import i18n from "@/locales";
import router from "@/test/unit/helpers/router";
import { getManager, resetManager } from "@/lib/vue-shortcut-manager";
import config from "@/aws-exports";
vi.mock("@/services/config", () => ({
default: { get_config_full: vi.fn(() => new Promise(() => {})) },
}));
vi.mock("@openobserve/browser-rum", () => ({
openobserveRum: { setUser: vi.fn(), startView: vi.fn() },
}));
import MainLayout from "./MainLayout.vue";
const StubDiv = defineComponent({ setup: () => () => h("div") });
function mountMainLayout() {
return mount(MainLayout, {
global: {
plugins: [store, i18n, router],
stubs: {
AppHeader: StubDiv,
ONavbar: StubDiv,
O2AIChat: StubDiv,
WebinarBanner: StubDiv,
AnnouncementBanner: StubDiv,
ShortcutCheatsheet: StubDiv,
GetStarted: StubDiv,
CommunitySlackInvite: StubDiv,
ThemeSwitcher: StubDiv,
PredefinedThemes: StubDiv,
SlackIcon: StubDiv,
ManagementIcon: StubDiv,
ODialog: StubDiv,
"router-view": StubDiv,
},
},
});
}
describe("MainLayout — AI chat shortcut gate", () => {
let wrapper: VueWrapper | undefined;
const originalIsEnterprise = config.isEnterprise;
beforeEach(() => {
resetManager();
});
afterEach(() => {
wrapper?.unmount();
wrapper = undefined;
config.isEnterprise = originalIsEnterprise;
store.state.zoConfig.ai_enabled = false;
store.state.isAiChatEnabled = false;
resetManager();
});
const ctrlB = () => getManager()?.getById("aiChatToggle");
it("should not register ctrl+b on OSS builds", async () => {
config.isEnterprise = "false";
store.state.zoConfig.ai_enabled = true;
wrapper = mountMainLayout();
await flushPromises();
expect(ctrlB()).toBeUndefined();
});
it("should register ctrl+b on enterprise builds", async () => {
config.isEnterprise = "true";
wrapper = mountMainLayout();
await flushPromises();
expect(ctrlB()?.key).toBe("ctrl+b");
});
it("should not open the chat while ai_enabled is off", async () => {
config.isEnterprise = "true";
store.state.zoConfig.ai_enabled = false;
wrapper = mountMainLayout();
await router.push({ name: "logs" });
await flushPromises();
ctrlB()!.handler();
expect(store.state.isAiChatEnabled).toBe(false);
});
it("should open the chat when ai_enabled is on", async () => {
config.isEnterprise = "true";
store.state.zoConfig.ai_enabled = true;
wrapper = mountMainLayout();
await router.push({ name: "logs" });
await flushPromises();
ctrlB()!.handler();
expect(store.state.isAiChatEnabled).toBe(true);
});
});
+6 -1
View File
@@ -1248,6 +1248,8 @@ export default defineComponent({
};
const toggleAIChat = () => {
// ai_enabled arrives with the async /config response, so it can't gate registration.
if (!store.state.zoConfig.ai_enabled) return;
// On the home page, switch to the AI tab instead of opening the side panel
if (router.currentRoute.value.name === "home") {
window.dispatchEvent(new CustomEvent("o2:home-switch-tab", { detail: "ai" }));
@@ -1360,7 +1362,10 @@ export default defineComponent({
};
// ── Global shortcuts: AI Chat ─────────────────────────────────────────
useShortcuts([{ id: "aiChatToggle", handler: () => toggleAIChat() }]);
// O2 AI is enterprise-only: on OSS the Ctrl+B binding must not exist at all.
if (config.isEnterprise == "true") {
useShortcuts([{ id: "aiChatToggle", handler: () => toggleAIChat() }]);
}
return {
isDark,