Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  head.js   Sprache: JAVA

 

/* Any copyright is dedicated to the Public Domain.
 * https://creativecommons.org/publicdomain/zero/1.0/ */


"use strict";

ChromeUtils.defineESModuleGetters(this, {
  AIWindow:
    "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
  AIWindowUI:
    "moz-src:///browser/components/aiwindow/ui/modules/AIWindowUI.sys.mjs",
  AIWindowAccountAuth:
    "moz-src:///browser/components/aiwindow/ui/modules/AIWindowAccountAuth.sys.mjs",
  Chat: "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs",
  ChatConversation:
    "moz-src:///browser/components/aiwindow/ui/modules/ChatConversation.sys.mjs",
  getModelForChoice:
    "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs",
  IntentClassifier:
    "moz-src:///browser/components/aiwindow/models/IntentClassifier.sys.mjs",
  openAIEngine: "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs",
  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
  SessionWindowUI: "resource:///modules/sessionstore/SessionWindowUI.sys.mjs",
  sinon: "resource://testing-common/Sinon.sys.mjs",
});

const { _setLoadPromptForTesting } = ChromeUtils.importESModule(
  "moz-src:///browser/components/aiwindow/ui/modules/ChatConversation.sys.mjs"
);

/**
 * @import { SmartbarAction } from "chrome://browser/content/aiwindow/components/input-cta/input-cta.mjs"
 */


async function modelFor(choiceId) {
  return (await getModelForChoice(choiceId)).model;
}

const AIWINDOW_URL = "chrome://browser/content/aiwindow/aiWindow.html";
const FIRSTRUN_URL = "chrome://browser/content/aiwindow/firstrun.html";

let gIntentEngineStub;
let gRemoteClientStub;

// Minimal RS records returned by the global getRemoteClient stub.
// Version numbers must match FEATURE_MAJOR_VERSIONS in models/Utils.sys.mjs.
const MOCK_RS_RECORDS = [
  ["chat", 7],
  ["title-generation", 1],
  ["conversation-starters-sidebar-system", 1],
  ["conversation-suggestions-sidebar-starter", 2],
  ["conversation-suggestions-followup", 1],
  ["conversation-suggestions-assistant-limitations", 1],
  ["conversation-suggestions-memories", 1],
  ["memories-initial-generation-system", 2],
  ["memories-initial-generation-user", 2],
  ["memories-deduplication-system", 1],
  ["memories-deduplication-user", 1],
  ["memories-sensitivity-filter-system", 1],
  ["memories-sensitivity-filter-user", 1],
  ["memories-quality-filter-system", 1],
  ["memories-quality-filter-user", 1],
  ["memories-message-classification-system", 1],
  ["memories-message-classification-user", 1],
  ["memories-relevant-context", 2],
]
  .map(([feature, major]) => ({
    feature,
    // chat uses "generic" as the fallback model name (see selectMainConfig);
    // all other features use "test-model" with is_default: true.
    model: feature === "chat" ? "generic" : "test-model",
    service_type: "ai",
    parameters: {},
    prompts:
      feature === "conversation-suggestions-sidebar-starter"
        ? "Generate {n} prompts for {current_tab}.\nOpen tabs: {open_tabs}"
        : "Test system prompt.",
    version: `v${major}.0`,
    is_default: true,
  }))
  // Per-choice records for chat so resolveChatModelChoice returns correct model names.
  .concat([
    {
      feature: "chat",
      model: "gemini-3.1-flash-lite",
      model_choice_id: "1",
      service_type: "ai",
      purpose: "chat",
      parameters: {},
      prompts: "Test system prompt.",
      version: "v7.0",
    },
    {
      feature: "chat",
      model: "qwen3-235b-a22b-instruct-2507-maas",
      model_choice_id: "2",
      service_type: "ai",
      purpose: "chat",
      parameters: {},
      prompts: "Test system prompt.",
      version: "v7.0",
    },
    {
      feature: "chat",
      model: "gpt-oss-120b",
      model_choice_id: "3",
      service_type: "ai",
      purpose: "chat",
      parameters: {},
      prompts: "Test system prompt.",
      version: "v7.0",
    },
  ]);

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.smartwindow.endpoint", "http://localhost:0/v1"],
      ["browser.smartwindow.enabled", true],
      ["browser.smartwindow.firstrun.hasCompleted", true],
      ["browser.smartwindow.chat.interactionCount", 0],
    ],
  });

  // Stub intent engine so it doesn't attempt network requests
  const fakeIntentEngine = {
    run() {
      return [
        { label: "chat", score: 0.95 },
        { label: "search", score: 0.05 },
      ];
    },
  };
  gIntentEngineStub = sinon
    .stub(IntentClassifier, "_createEngine")
    .resolves(fakeIntentEngine);
  registerCleanupFunction(() => gIntentEngineStub.restore());

  // Stub getRemoteClient so loadCallContext never hits real Remote Settings.
  gRemoteClientStub = sinon
    .stub(openAIEngine, "getRemoteClient")
    .returns({ get: async () => MOCK_RS_RECORDS });
  registerCleanupFunction(() => gRemoteClientStub.restore());

  // Stub ChatConversation's loadPrompt so generatePrompt doesn't hit RS.
  _setLoadPromptForTesting(async () => "Test system prompt.");
  registerCleanupFunction(() => _setLoadPromptForTesting(null));
});

/**
 * Opens a new AI Window
 *
 * @param {object} options
 * @param {string} [options.waitForTabURL] - URL to wait for, or empty string to skip waiting
 * @returns {Promise<Window>}
 */

async function openAIWindow({ waitForTabURL = AIWINDOW_URL } = {}) {
  info("Opening new AI Window");
  const win = await BrowserTestUtils.openNewBrowserWindow({
    aiWindow: true,
    waitForTabURL,
  });
  info("Waiting for AI window attr");
  await BrowserTestUtils.waitForMutationCondition(
    win.document.documentElement,
    { attributes: true },
    () => win.document.documentElement.hasAttribute("ai-window")
  );
  info("Promising focus");
  await SimpleTest.promiseFocus(win);
  return win;
}

/**
 * Waits for the sidebar ai-window element to connect.
 *
 * @param {Window} win - Window reference
 * @returns {Promise<MozBrowser>} - The sidebar browser element
 */

async function waitForSidebarReady(win) {
  const sidebarBrowser = win.document.getElementById("ai-window-browser");
  await BrowserTestUtils.waitForCondition(
    () => sidebarBrowser.contentDocument?.querySelector("ai-window:defined"),
    "Sidebar ai-window should be loaded"
  );
  return sidebarBrowser;
}

/**
 * Opens a new AI Window with about:blank
 * and the chat assistant sidebar open
 *
 * @returns {Promise<{win: Window, sidebarBrowser: MozBrowser}>}
 */

async function openAIWindowWithSidebar() {
  const win = await openAIWindow();
  return openAIWindowSidebar(win);
}

/**
 * Navigates an AI Window to about:blank and opens the sidebar.
 *
 * @param {Window} win
 * @returns {Promise<{win: Window, sidebarBrowser: MozBrowser}>}
 */

async function openAIWindowSidebar(win) {
  BrowserTestUtils.startLoadingURIString(
    win.gBrowser.selectedBrowser,
    "about:blank"
  );
  await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser, {
    wantLoad: "about:blank",
  });
  if (!AIWindowUI.isSidebarOpen(win)) {
    info("Opening sidebar");
    AIWindowUI.toggleSidebar(win);
  }
  const sidebarBrowser = await waitForSidebarReady(win);
  return { win, sidebarBrowser };
}

/**
 * NOTE: If using to navigate to https://example.com/ make sure
 * to use a trailing slash in the URL or this function will hang.
 *
 * @param {MozBrowser} browser
 * @param {string} url
 */

function promiseNavigateAndLoad(browser, url) {
  let loaded = BrowserTestUtils.browserLoaded(browser, {
    wantLoad: url,
  });
  BrowserTestUtils.startLoadingURIString(browser, url);
  return loaded;
}

async function getPromptButtons(browser) {
  const aiWindow = await TestUtils.waitForCondition(
    () => browser.contentDocument?.querySelector("ai-window"),
    "Wait for ai-window element"
  );
  const promptsEl = await TestUtils.waitForCondition(
    () => aiWindow.shadowRoot.querySelector("smartwindow-prompts"),
    "Wait for smartwindow-prompts element"
  );
  return promptsEl.shadowRoot.querySelectorAll(".sw-prompt-button");
}

async function getConversationId(browser) {
  const aiWindow = await TestUtils.waitForCondition(
    () => browser.contentDocument?.querySelector("ai-window"),
    "Wait for ai-window element"
  );
  return aiWindow.conversationId.toString();
}

/**
 * @typedef {object} EngineRunResponse
 * @property {string} [finalOutput] - The text content returned by the mock
 *   engine's run() method. Used by non-chat features like title generation.
 */


/**
 * @typedef {object} StubEngineNetworkBoundariesConfig
 * @property {Set<string>} [passthroughFeatures] - Feature names that use the
 *   real openAIEngine.build (going through RemoteSettings, ML engine creation,
 *   and the mock HTTP server). All other features get a mock engine that
 *   resolves immediately. Defaults to new Set(["chat"]).
 * @property {string} [fxAccountToken] - Value that the stubbed
 *   getFxAccountToken resolves with. Defaults to "mock-fxa-token".
 * @property {EngineRunResponse} [engineRunResponse] - What the mock engine's
 *   run() resolves with for non-passthrough features. Defaults to
 *   { finalOutput: "Mock" }.
 * @property {MockOpenAIServerOptions|null} [serverOptions] - Options passed
 *   to startMockOpenAI. When non-null, a mock server is started and the
 *   endpoint pref is pushed so passthrough features route to it. Defaults to
 *   {} which starts a server with startMockOpenAI defaults (single-chunk
 *   "Hello from mock." stream). Pass null to skip starting a server.
 */


/**
 * @typedef {object} StubEngineNetworkBoundariesResult
 * @property {Function} restore - Async cleanup function that restores all
 *   stubs, pops the endpoint pref, and stops the mock server.
 * @property {number|null} port - The mock server's port number, or null when
 *   no server was started.
 * @property {Array<object>} capturedRequests - Array that collects every
 *   parsed request body sent to the mock server. Empty when no server was
 *   started. Mutate .length = 0 to clear between phases of a test.
 */


/**
 * Stubs openAIEngine.build and openAIEngine.getFxAccountToken so that
 * background async operations (title generation, conversation starters, etc.)
 * resolve immediately instead of going through RemoteSettings / ML engine
 * creation / FxA token fetching. Without this, fire-and-forget calls like
 * #addConversationTitle() can leave suspended async chains that prevent
 * window GC, causing leaked-window failures under --verify.
 *
 * Features listed in passthroughFeatures still use the real build path
 * (e.g. "chat" needs to hit the mock HTTP server).
 *
 * By default starts a mock OpenAI HTTP server and pushes the endpoint pref
 * so passthrough features route to it. Pass serverOptions: null to skip.
 *
 * Call the returned async restore function to clean up stubs, pop prefs, and
 * stop the server.
 *
 * @deprecated Please use MockEngineManager in AIWindowTestUtils.sys.mjs.
 *   TODO (Bug 2045844): Remove and replace existing usages across test files.
 * @param {StubEngineNetworkBoundariesConfig} [config]
 * @returns {Promise<StubEngineNetworkBoundariesResult>}
 */

async function stubEngineNetworkBoundaries({
  passthroughFeatures = new Set(["chat"]),
  fxAccountToken = "mock-fxa-token",
  engineRunResponse = { finalOutput: "Mock" },
  serverOptions = {},
} = {}) {
  // Save the original build before stubbing so passthrough features (e.g.
  // "chat") can still create a real engine that routes requests to the mock
  // HTTP server via RemoteSettings + EngineProcess.
  const originalBuild = openAIEngine.build.bind(openAIEngine);
  const buildStub = sinon
    .stub(openAIEngine, "build")
    .callsFake(async (featureOrConfig, ...rest) => {
      const feature =
        typeof featureOrConfig === "object" && featureOrConfig !== null
          ? featureOrConfig.feature
          : featureOrConfig;
      if (passthroughFeatures.has(feature)) {
        return originalBuild(featureOrConfig, ...rest);
      }
      // Non-passthrough features get a mock engine whose methods resolve
      // synchronously, preventing dangling async chains.
      return {
        feature,
        model:
          typeof featureOrConfig === "object"
            ? featureOrConfig.model
            : "test-model",
        async run() {
          return engineRunResponse;
        },
        runWithGenerator() {},
      };
    });

  const tokenStub = sinon
    .stub(openAIEngine, "getFxAccountToken")
    .resolves(fxAccountToken);

  const capturedRequests = [];
  let server = null;
  let port = null;
  if (serverOptions) {
    const callerOnRequest = serverOptions.onRequest;
    ({ server, port } = startMockOpenAI({
      ...serverOptions,
      onRequest(body) {
        capturedRequests.push(body);
        callerOnRequest?.(body);
      },
    }));
    await SpecialPowers.pushPrefEnv({
      set: [["browser.smartwindow.endpoint", `http://localhost:${port}/v1`]],
    });
  }

  async function restore() {
    buildStub.restore();
    tokenStub.restore();
    if (server) {
      await SpecialPowers.popPrefEnv();
      await stopMockOpenAI(server);
    }
  }

  return { restore, port, capturedRequests };
}

/**
 * Stubs AIWindowAccountAuth.ensureAIWindowAccess to skip sign-in flow
 * Call the returned restore function to clean up the stub
 *
 * @returns {Function} restore function to clean up the stub
 */

function skipSignIn() {
  const stub = sinon
    .stub(AIWindowAccountAuth, "ensureAIWindowAccess")
    .resolves(true);
  return () => stub.restore();
}

/**
 * Clicks the New Chat button in the sidebar
 *
 * @param {Window} win
 */

async function clickNewChatButton(win) {
  const sidebarBrowser = win.document.getElementById("ai-window-browser");
  await TestUtils.waitForCondition(
    () => sidebarBrowser.contentDocument?.querySelector("ai-window:defined"),
    "Wait for ai-window to be defined in sidebar"
  );
  const aiWindow = sidebarBrowser.contentDocument.querySelector("ai-window");
  const newChatBtn = aiWindow.shadowRoot.querySelector(".new-chat-icon-button");
  newChatBtn.click();
}

/**
 * Opens the Smartbar's context menu via the "+" button and clicks the
 * panel item whose visible text matches the given label. Waits for the
 * panel to populate before selecting.
 *
 * @param {MozBrowser} sidebarBrowser - The sidebar browser element
 *   (e.g. win.document.getElementById("ai-window-browser"))
 * @param {string} label - The visible text label of the tab to select
 *   (e.g. the tab's title)
 */

async function openTabContextMenuAndClickTabByLabel(sidebarBrowser, label) {
  await SpecialPowers.spawn(sidebarBrowser, [label], async tabLabel => {
    const aiWindowElement = await ContentTaskUtils.waitForCondition(
      () => content.document.querySelector("ai-window"),
      "Wait for ai-window"
    );
    const smartbar = await ContentTaskUtils.waitForCondition(
      () => aiWindowElement.shadowRoot?.querySelector("#ai-window-smartbar"),
      "Wait for Smartbar"
    );
    const contextButton = smartbar.querySelector("context-icon-button");
    const button = contextButton.shadowRoot.querySelector("moz-button");
    button.click();

    const panelList = smartbar.querySelector("smartwindow-panel-list");
    const panel = panelList.shadowRoot.querySelector("panel-list");
    await ContentTaskUtils.waitForMutationCondition(
      panel,
      { childList: true, subtree: true },
      () => panel.querySelector("panel-item:not(.panel-section-header)")
    );

    const items = panel.querySelectorAll(
      "panel-item:not(.panel-section-header)"
    );
    let targetItem;
    for (const item of items) {
      if (item.textContent.trim() === tabLabel) {
        targetItem = item;
        break;
      }
    }
    Assert.ok(
      targetItem,
      `Should find a tab labeled '${tabLabel}' in the context menu`
    );
    targetItem.click();
  });
}

async function getSmartbarContextChipLabels(browser, expectedUrl) {
  await BrowserTestUtils.waitForCondition(
    () => browser.contentDocument?.querySelector("ai-window:defined"),
    "Sidebar ai-window should be loaded"
  );

  return SpecialPowers.spawn(browser, [expectedUrl], async url => {
    const aiWindowElement = await ContentTaskUtils.waitForCondition(
      () => content.document.querySelector("ai-window"),
      "Wait for ai-window to be rendered"
    );
    const smartbar = await ContentTaskUtils.waitForCondition(
      () => aiWindowElement.shadowRoot?.querySelector("#ai-window-smartbar"),
      "Wait for Smartbar to be rendered"
    );
    const chipContainer = await ContentTaskUtils.waitForCondition(
      () => smartbar.querySelector(".smartbar-context-chips-header"),
      "Wait for chip container to be rendered"
    );

    if (url) {
      await ContentTaskUtils.waitForCondition(
        () =>
          Array.isArray(chipContainer.websites) &&
          chipContainer.websites.some(site => site.url.includes(url)),
        `Wait for chip with URL containing "${url}"`
      );
    } else {
      await ContentTaskUtils.waitForCondition(
        () =>
          Array.isArray(chipContainer.websites) &&
          chipContainer.websites.length,
        "Wait for at least one chip"
      );
    }

    const chips = chipContainer.shadowRoot.querySelectorAll("ai-website-chip");
    const chipLabels = Array.from(chips).map(
      chip => chip.shadowRoot?.querySelector(".chip-label")?.textContent ?? ""
    );

    return chipLabels;
  });
}

/**
 * Submits the current smartbar input.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {object} [options] - Options for submission
 * @param {boolean} [options.useButton=false] - If true, submit via CTA button
 */

async function submitSmartbar(browser, { useButton = false } = {}) {
  await SpecialPowers.spawn(browser, [useButton], async clickButton => {
    const aiWindow = content.document.querySelector("ai-window");
    await ContentTaskUtils.waitForMutationCondition(
      aiWindow.shadowRoot,
      { childList: true, subtree: true },
      () => aiWindow.shadowRoot.querySelector("#ai-window-smartbar")
    );
    const smartbar = aiWindow.shadowRoot.querySelector("#ai-window-smartbar");
    if (clickButton) {
      const inputCta = smartbar.querySelector("input-cta");
      const mozButton = inputCta.shadowRoot.querySelector("moz-button");
      const button = mozButton.shadowRoot.querySelector("button");
      button.click();
    } else {
      const inputCta = smartbar.querySelector("input-cta");
      await ContentTaskUtils.waitForCondition(
        () => inputCta.getAttribute("action") !== "stop",
        "Wait for generation to complete before submitting via Enter"
      );
      const inputField = smartbar.inputField;
      inputField.focus();
      EventUtils.synthesizeKey("KEY_Enter", {}, content);
    }
  });
}

/**
 * Select an explicit action from the smartbar CTA dropdown menu.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {SmartbarAction} action - The action to select
 */

async function selectExplicitSmartbarAction(browser, action) {
  await SpecialPowers.spawn(browser, [action], async actionType => {
    const aiWindow = content.document.querySelector("ai-window");
    await ContentTaskUtils.waitForMutationCondition(
      aiWindow.shadowRoot,
      { childList: true, subtree: true },
      () => aiWindow.shadowRoot.querySelector("#ai-window-smartbar")
    );
    const smartbar = aiWindow.shadowRoot.querySelector("#ai-window-smartbar");
    const inputCta = smartbar.querySelector("input-cta");
    const mozButton = inputCta.shadowRoot.querySelector("moz-button");

    await ContentTaskUtils.waitForMutationCondition(
      mozButton.shadowRoot,
      { childList: true, subtree: true },
      () => mozButton.shadowRoot.querySelector("#chevron-button")
    );
    const chevronButton = mozButton.shadowRoot.querySelector("#chevron-button");
    const panelList = inputCta.shadowRoot.querySelector("panel-list");
    const shownPromise = ContentTaskUtils.waitForEvent(panelList, "shown");
    chevronButton.click();
    await shownPromise;

    const actionItem = panelList.querySelector(
      `panel-item[icon="${actionType}"]`
    );
    actionItem.click();
  });
}

/**
 * Select the first search engine from the smartbar CTA "Search with…" submenu.
 *
 * @param {MozBrowser} browser - The browser element
 */

async function selectSmartbarSearchEngine(browser) {
  const inputCta = BrowserTestUtils.querySelectorDeep(
    browser.contentDocument,
    "input-cta"
  );
  const mozButton = BrowserTestUtils.querySelectorDeep(inputCta, "moz-button");

  const chevronButton = await BrowserTestUtils.waitForCondition(() =>
    mozButton.shadowRoot.querySelector("#chevron-button")
  );
  const [mainPanel, searchSubpanel] =
    inputCta.shadowRoot.querySelectorAll("panel-list");

  const mainShown = BrowserTestUtils.waitForEvent(mainPanel, "shown");
  chevronButton.click();
  await mainShown;

  const subpanelShown = BrowserTestUtils.waitForEvent(searchSubpanel, "shown");
  mainPanel.querySelector('panel-item[icon="search-with"]').click();
  await subpanelShown;

  const engineItem = await BrowserTestUtils.waitForCondition(() =>
    searchSubpanel.querySelector('panel-item[icon="engine"]')
  );
  engineItem.click();
}

/**
 * Wait for the smartbar action to be set.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {string} expectedAction - The expected action value
 */

async function waitForSmartbarAction(browser, expectedAction) {
  await SpecialPowers.spawn(browser, [expectedAction], async action => {
    const aiWindow = content.document.querySelector("ai-window");
    await ContentTaskUtils.waitForMutationCondition(
      aiWindow.shadowRoot,
      { childList: true, subtree: true },
      () => aiWindow.shadowRoot.querySelector("#ai-window-smartbar")
    );
    const smartbar = aiWindow.shadowRoot.querySelector("#ai-window-smartbar");
    await ContentTaskUtils.waitForCondition(
      () => smartbar.smartbarAction === action,
      `Wait for smartbar action to be "${action}"`
    );
  });
}

/**
 * Stub the smartbar method _loadURL to prevent navigation.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {object} [options] - Options for the stub
 * @param {boolean} [options.captureURL=false] - If true, capture the URL
 */

async function stubLoadURL(browser, { captureURL = false } = {}) {
  await SpecialPowers.spawn(browser, [captureURL], async capture => {
    const aiWindow = content.document.querySelector("ai-window");
    await ContentTaskUtils.waitForMutationCondition(
      aiWindow.shadowRoot,
      { childList: true, subtree: true },
      () => aiWindow.shadowRoot.querySelector("#ai-window-smartbar")
    );
    const smartbar = aiWindow.shadowRoot.querySelector("#ai-window-smartbar");
    if (capture) {
      content._stubLoadURLCalled = false;
      content._stubLoadedURL = null;
      smartbar._loadURL = url => {
        content._stubLoadURLCalled = true;
        content._stubLoadedURL = url;
      };
    } else {
      smartbar._loadURL = () => {};
    }
  });
}

/**
 * Get the result of a stubbed and captured stubLoadURL call.
 *
 * @param {MozBrowser} browser - The browser element
 * @returns {Promise<{called: boolean, url: string|null}>}
 */

async function getStubLoadURLResult(browser) {
  return SpecialPowers.spawn(browser, [], async () => {
    return {
      called: content._stubLoadURLCalled,
      url: content._stubLoadedURL,
    };
  });
}

/**
 * Assert the current smartbar value.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {string} expectedValue - The expected value
 * @param {string} message - The assertion message
 */

async function assertSmartbarValue(browser, expectedValue, message) {
  await SpecialPowers.spawn(
    browser,
    [expectedValue, message],
    async (val, msg) => {
      const aiWindow = content.document.querySelector("ai-window");
      await ContentTaskUtils.waitForMutationCondition(
        aiWindow.shadowRoot,
        { childList: true, subtree: true },
        () => aiWindow.shadowRoot.querySelector("#ai-window-smartbar")
      );
      const smartbar = aiWindow.shadowRoot.querySelector("#ai-window-smartbar");
      Assert.equal(smartbar.value, val, msg);
    }
  );
}

/**
 * Type text into the smartbar and wait for a pending query to complete.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {string} text - Text to type
 */

async function typeInSmartbar(browser, text) {
  await SpecialPowers.spawn(browser, [text], async searchText => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = await ContentTaskUtils.waitForCondition(
      () => aiWindowElement.shadowRoot?.querySelector("#ai-window-smartbar"),
      "Wait for Smartbar to be rendered"
    );
    info("typeInSmartbar: smartbar found, calling focus()");
    smartbar.inputField.focus();
    await ContentTaskUtils.waitForCondition(
      () => smartbar.matches(":focus-within"),
      "Wait for smartbar to receive focus"
    );
    info("typeInSmartbar: focus received, sending string");
    EventUtils.sendString(searchText, content);
    info("typeInSmartbar: string sent, awaiting lastQueryContextPromise");
    await smartbar.lastQueryContextPromise;
    info("typeInSmartbar: query complete");
  });
}

/**
 * Waits for the Smartbar suggestions view to open.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {Function} openFn - A function that should trigger the view opening
 */

async function promiseSmartbarSuggestionsOpen(browser, openFn) {
  if (!openFn) {
    throw new Error(
      "openFn should be supplied to promiseSmartbarSuggestionsOpen"
    );
  }

  const opened = SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = await ContentTaskUtils.waitForCondition(
      () => aiWindowElement.shadowRoot?.querySelector("#ai-window-smartbar"),
      "Wait for Smartbar to be rendered"
    );
    if (smartbar.view.isOpen) {
      return;
    }
    await ContentTaskUtils.waitForMutationCondition(
      smartbar,
      { attributes: true },
      () => smartbar.hasAttribute("open")
    );
  });
  await openFn();
  await opened;
}

/**
 * Waits for the Smartbar suggestions view to close.
 *
 * @param {MozBrowser} browser - The browser element
 */

async function promiseSmartbarSuggestionsClose(browser) {
  await SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = await ContentTaskUtils.waitForCondition(
      () => aiWindowElement.shadowRoot?.querySelector("#ai-window-smartbar"),
      "Wait for Smartbar to be rendered"
    );
    if (!smartbar.view.isOpen) {
      return;
    }
    await ContentTaskUtils.waitForMutationCondition(
      smartbar,
      { attributes: true },
      () => !smartbar.hasAttribute("open")
    );
  });
}

/**
 * Asserts the Smartbar suggestions view position and visibility.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {boolean} shouldBeVisible - Whether the suggestions view should be visible
 * @param {string} expectedPosition - The expected position
 */

async function assertSmartbarSuggestionsVisible(
  browser,
  shouldBeVisible,
  expectedPosition = "bottom"
) {
  const aiWindowElement =
    browser.contentWindow.document.querySelector("ai-window");
  const smartbarElement = aiWindowElement.shadowRoot.querySelector(
    "#ai-window-smartbar"
  );
  const urlbarView = smartbarElement.querySelector(".urlbarView");

  Assert.equal(
    BrowserTestUtils.isVisible(urlbarView),
    shouldBeVisible,
    `Suggestions view element should be visible: ${shouldBeVisible}`
  );
  Assert.equal(
    smartbarElement.getAttribute("suggestions-position"),
    expectedPosition,
    `Suggestions position should be: ${expectedPosition}`
  );
}

/**
 * Wait for panel list to be visible.
 *
 * @param {MozBrowser} browser - The browser element
 * @returns {Promise<boolean>} True if panel is visible
 */

async function waitForPanelOpen(browser) {
  return SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = await ContentTaskUtils.waitForCondition(
      () => aiWindowElement.shadowRoot?.querySelector("#ai-window-smartbar"),
      "Wait for Smartbar to be rendered"
    );
    const panelList = smartbar.querySelector("smartwindow-panel-list");
    const panel = panelList.shadowRoot.querySelector("panel-list");

    await ContentTaskUtils.waitForMutationCondition(
      panel,
      { attributes: true, attributeFilter: ["open"] },
      () => panel.hasAttribute("open")
    );

    return panel.hasAttribute("open");
  });
}

/**
 * Wait for the mentions panel list to be visible.
 *
 * @param {MozBrowser} browser - The browser element
 * @returns {Promise<boolean>} True if mentions are open
 */

async function waitForMentionsOpen(browser) {
  return SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = await ContentTaskUtils.waitForCondition(
      () => aiWindowElement.shadowRoot?.querySelector("#ai-window-smartbar"),
      "Wait for Smartbar to be rendered"
    );
    const panelList = smartbar.querySelector("smartwindow-panel-list");
    const panel = panelList.shadowRoot.querySelector("panel-list");

    await ContentTaskUtils.waitForMutationCondition(
      panel,
      { attributes: true, attributeFilter: ["open"] },
      () => panel.hasAttribute("open")
    );

    return panel.hasAttribute("open");
  });
}

/**
 * Wait for a mention to be inserted.
 *
 * @param {MozBrowser} browser - The browser element
 * @returns {Promise<boolean>} True if the mention exists
 */

async function waitForMentionInserted(browser) {
  return SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = aiWindowElement.shadowRoot.querySelector(
      "#ai-window-smartbar"
    );
    const editor = smartbar.querySelector("moz-multiline-editor");

    await ContentTaskUtils.waitForMutationCondition(
      editor.shadowRoot,
      { childList: true, subtree: true },
      () => editor.shadowRoot.querySelector("ai-website-chip") !== null
    );

    return !!editor.shadowRoot.querySelector("ai-website-chip");
  });
}

/**
 * Click the first non-header item in the smartbar mention suggestions panel.
 *
 * @param {MozBrowser} browser - The browser element
 */

async function selectFirstMentionPanelItem(browser) {
  await SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = aiWindowElement.shadowRoot.querySelector(
      "#ai-window-smartbar"
    );
    const panelList = smartbar.querySelector("smartwindow-panel-list");
    const panel = panelList.shadowRoot.querySelector("panel-list");
    const firstItem = panel.querySelector(
      "panel-item:not(.panel-section-header)"
    );
    firstItem.click();
  });
}

/**
 * Type "@", wait for the mention panel, click the first suggestion, and wait
 * for the mention chip to appear in the editor.
 *
 * @param {MozBrowser} browser - The browser element
 */

async function insertInlineMention(browser) {
  await typeInSmartbar(browser, "@");
  await waitForPanelOpen(browser);
  await selectFirstMentionPanelItem(browser);
  await waitForMentionInserted(browser);
}

/**
 * Return inline @mention data from the editor's mentions plugin.
 *
 * @param {MozBrowser} browser - The browser element
 * @returns {Promise<Array<{type: string, id: string, label: string}>>}
 */

async function getEditorInlineMentions(browser) {
  return SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = aiWindowElement.shadowRoot.querySelector(
      "#ai-window-smartbar"
    );
    const editor = smartbar.querySelector("moz-multiline-editor");
    return editor.getAllMentions();
  });
}

/**
 * Get the context chips from the smartbar header.
 *
 * @param {MozBrowser} browser - The browser element
 * @returns {Promise<Array<{url: string, label: string}>>} The context chip data
 */

async function getSmartbarContextChips(browser) {
  return SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = aiWindowElement.shadowRoot.querySelector(
      "#ai-window-smartbar"
    );
    const chipContainer = smartbar.querySelector(
      ".smartbar-context-chips-header"
    );
    return chipContainer.websites.map(w => ({ url: w.url, label: w.label }));
  });
}

/**
 * Gets the smartbar model select data.
 *
 * @param {MozBrowser} browser - The browser element
 * @returns {Promise<object>} Returns model select data
 */

async function getSmartbarModelSelectData(browser) {
  return SpecialPowers.spawn(browser, [], async () => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = aiWindowElement.shadowRoot.querySelector(
      "#ai-window-smartbar"
    );
    const modelSelect = smartbar.querySelector("input-model-select");

    return {
      availableModels: modelSelect.availableModels,
      selectedModelId: aiWindowElement.selectedModelId,
    };
  });
}

/**
 * Switches to a model in the smartbar by choice ID.
 *
 * @param {MozBrowser} browser - The browser element
 * @param {string} modelChoiceId - Model choice id to switch to
 */

async function switchSmartbarModel(browser, modelChoiceId) {
  return SpecialPowers.spawn(browser, [modelChoiceId], async choiceId => {
    const aiWindowElement = content.document.querySelector("ai-window");
    const smartbar = aiWindowElement.shadowRoot.querySelector(
      "#ai-window-smartbar"
    );
    const modelSelect = smartbar.querySelector("input-model-select");
    const triggerButton = modelSelect.shadowRoot.querySelector("moz-button");
    triggerButton.click();

    const panelList = modelSelect.shadowRoot.querySelector("panel-list");
    await ContentTaskUtils.waitForMutationCondition(
      panelList,
      { attributes: true },
      () => panelList.hasAttribute("open")
    );

    const modelKeys = Object.keys(modelSelect.availableModels);
    const index = modelKeys.indexOf(choiceId);
    if (index === -1) {
      throw new Error(`Model choice "${choiceId}" not available`);
    }
    const targetModelId = modelSelect.availableModels[choiceId].model;
    panelList.querySelectorAll("button.model-item")[index].click();

    await ContentTaskUtils.waitForMutationCondition(
      aiWindowElement,
      { attributes: true },
      () => aiWindowElement.selectedModelId === targetModelId
    );
  });
}

/**
 * Returns the chat messages currently displayed in the sidebar.
 *
 * @param {MozBrowser} sidebarBrowser - The sidebar browser element
 * @returns {Promise<Array<{role: string, message: string}>>}
 */

async function getSidebarChatMessages(sidebarBrowser) {
  const aiWindow = await TestUtils.waitForCondition(
    () => sidebarBrowser.contentDocument?.querySelector("ai-window"),
    "Wait for ai-window element"
  );
  const aichatBrowser = await TestUtils.waitForCondition(
    () => aiWindow.shadowRoot?.querySelector("#aichat-browser"),
    "Wait for #aichat-browser element"
  );
  return SpecialPowers.spawn(aichatBrowser, [], async () => {
    const contentEl = await ContentTaskUtils.waitForCondition(
      () => content.document.querySelector("ai-chat-content"),
      "Wait for ai-chat-content element"
    );
    await contentEl.updateComplete;
    const messageEls = contentEl.shadowRoot.querySelectorAll("ai-chat-message");
    return Array.from(messageEls, el => {
      const elJS = el.wrappedJSObject || el;
      return {
        role: elJS.role,
        message: elJS.message,
        hasRendered: !!el.shadowRoot?.querySelector(
          ".message-assistant, .message-user"
        ),
      };
    });
  });
}

/**
 * Default bound for the render waits below. Generous enough to never false-fail
 * behind the mock LLM round-trip, but a fraction of the harness timeout so a
 * genuine stall fails fast with a clear message rather than hanging.
 */

const RENDER_TIMEOUT_MS = 15000;

/**
 * Bounded wrapper around BrowserTestUtils.waitForMutationCondition, which on its
 * own never rejects. Races the (event-driven) mutation wait against a timeout so
 * a missing element fails fast with a clear message instead of hanging until the
 * harness aborts the task.
 *
 * @param {Node} target - The node on which to observe mutations
 * @param {MutationObserverInit} options - Options for MutationObserver.observe()
 * @param {Function} checkFn - Returns the awaited value once it is truthy
 * @param {string} label - Description used in the timeout error message
 * @param {number} [timeoutMs=RENDER_TIMEOUT_MS]
 *
 * @returns {Promise<any>} The value returned by checkFn
 */

function waitForMutationBounded(
  target,
  options,
  checkFn,
  label,
  timeoutMs = RENDER_TIMEOUT_MS
) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`Timed out waiting for: ${label}`)),
      timeoutMs
    );
  });
  return Promise.race([
    BrowserTestUtils.waitForMutationCondition(target, options, checkFn),
    timeout,
  ]).finally(() => clearTimeout(timer));
}

/**
 * Resolves the #aichat-browser frame for the AI Window hosted in the given
 * browser. By the time the post-response helpers below run, both ai-window and
 * #aichat-browser already exist, so the check resolves immediately; the bound
 * just guarantees a fast, clear failure if they ever don't.
 *
 * @param {MozBrowser} browser - The browser hosting the AI Window
 *
 * @returns {Promise<MozBrowser>} The #aichat-browser frame
 */

function getAIChatBrowser(browser) {
  return waitForMutationBounded(
    browser.contentDocument.documentElement,
    { childList: true, subtree: true },
    () =>
      browser.contentDocument
        ?.querySelector("ai-window")
        ?.shadowRoot?.querySelector("#aichat-browser"),
    "ai-window #aichat-browser"
  );
}

/**
 * Runs a SpecialPowers.spawn task bounded by a timeout. The content task can use
 * plain ContentTaskUtils.waitForMutationCondition (event-driven, no polling),
 * which never rejects on its own; this wrapper races the spawn against a timer so
 * a task that never settles fails fast with a clear message instead of hanging
 * until the harness aborts the SpecialPowers actor.
 *
 * @param {MozBrowser} browser - The frame to run the task in
 * @param {Array} args - Arguments forwarded to the task
 * @param {Function} task - The content task
 * @param {string} label - Description used in the timeout error message
 * @param {number} [timeoutMs=RENDER_TIMEOUT_MS]
 *
 * @returns {Promise<any>} The task's return value
 */

function spawnBounded(
  browser,
  args,
  task,
  label,
  timeoutMs = RENDER_TIMEOUT_MS
) {
  const spawned = SpecialPowers.spawn(browser, args, task);
  // If the timeout wins, the spawn promise is abandoned; handle its eventual
  // rejection (the actor is torn down at cleanup) so it isn't surfaced as an
  // unhandled rejection. A real task error still propagates through the race.
  spawned.catch(() => {});

  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`Timed out waiting for: ${label}`)),
      timeoutMs
    );
  });
  return Promise.race([spawned, timeout]).finally(() => clearTimeout(timer));
}

/**
 * Checks for presence of a selector within the chat messages, either at a
 * specific message element or in any message element
 *
 * @param {MozBrowser} browser - browser that the messages are in
 * @param {string} selector - The selector to look for
 * @param {number} [nthElement] - Which message index to check for the selector,
 *                                defaults to last item
 *
 * @returns {boolean} Whether the selector was found
 */

async function checkForElementInChatMessage(
  browser,
  selector,
  nthElement = -1
) {
  const aiChatBrowser = await getAIChatBrowser(browser);

  return spawnBounded(
    aiChatBrowser,
    [selector, nthElement],
    async (sel, nthEl) => {
      await ContentTaskUtils.waitForMutationCondition(
        content.document.documentElement,
        { childList: true, subtree: true },
        () => content.document.querySelector("ai-chat-content")
      );
      const contentEl = content.document.querySelector("ai-chat-content");

      await ContentTaskUtils.waitForMutationCondition(
        contentEl.shadowRoot,
        { childList: true, subtree: true },
        () => {
          const messages = Array.from(
            contentEl.shadowRoot.querySelectorAll("ai-chat-message")
          );
          const message = messages.at(nthEl);
          return message && ContentTaskUtils.querySelectorDeep(message, sel);
        }
      );

      const messages = Array.from(
        contentEl.shadowRoot.querySelectorAll("ai-chat-message")
      );
      return !!ContentTaskUtils.querySelectorDeep(messages.at(nthEl), sel);
    },
    `${selector} in chat message ${nthElement}`
  );
}

async function checkForNumberOfElementsInChatMessage(
  browser,
  selector,
  amount,
  nthElement = -1
) {
  const aiChatBrowser = await getAIChatBrowser(browser);

  return spawnBounded(
    aiChatBrowser,
    [selector, nthElement, amount],
    async (sel, nthEl, amt) => {
      await ContentTaskUtils.waitForMutationCondition(
        content.document.documentElement,
        { childList: true, subtree: true },
        () => content.document.querySelector("ai-chat-content")
      );
      const contentEl = content.document.querySelector("ai-chat-content");

      await ContentTaskUtils.waitForMutationCondition(
        contentEl.shadowRoot,
        { childList: true, subtree: true },
        () => {
          const messages = Array.from(
            contentEl.shadowRoot.querySelectorAll("ai-chat-message")
          );
          const message = messages.at(nthEl);
          return (
            message &&
            ContentTaskUtils.querySelectorDeep(message, "ai-chat-grid")
          );
        }
      );

      const messages = Array.from(
        contentEl.shadowRoot.querySelectorAll("ai-chat-message")
      );
      const aiChatGrid = ContentTaskUtils.querySelectorDeep(
        messages.at(nthEl),
        "ai-chat-grid"
      );

      // The counted children render asynchronously inside the grid's own shadow
      // root, so observe there.
      await ContentTaskUtils.waitForMutationCondition(
        aiChatGrid.shadowRoot,
        { childList: true, subtree: true },
        () =>
          ContentTaskUtils.querySelectorDeep(aiChatGrid, sel)?.children
            .length === amt
      );

      return true;
    },
    `${amount} ${selector} in chat message ${nthElement}`
  );
}

/**
 * Mock OpenAI server helpers
 */


const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);

function readRequestBody(request) {
  const stream = request.bodyInputStream;
  const available = stream.available();
  return NetUtil.readInputStreamToString(stream, available, {
    charset: "UTF-8",
  });
}

/**
 * @typedef {object} MockToolCall
 * @property {string} name - The tool function name (e.g. "run_search",
 *   "get_page_content").
 * @property {string} [args] - JSON-encoded arguments for the tool call.
 *   Defaults to "{}".
 */


/**
 * @typedef {object} MockOpenAIServerOptions
 * @property {string[]} [streamChunks] - Array of content strings sent as
 *   individual SSE chunks in the streaming response. Defaults to
 *   ["Hello from mock."].
 * @property {MockToolCall|null} [toolCall] - When non-null, the first
 *   streaming request that includes tools will respond with this tool call
 *   instead of text content. A subsequent request containing the tool result
 *   will receive followupChunks as the response. Defaults to null.
 * @property {string[]} [followupChunks] - Content chunks sent in the
 *   streaming response after a tool result is received. Only used when
 *   toolCall is set. Defaults to ["Tool complete."].
 * @property {Function} [onRequest] - Callback invoked with the parsed
 *   request body for every request to /v1/chat/completions.
 */


/**
 * Starts a local HTTP server that mimics the OpenAI chat completions API.
 *
 * Handles both streaming (SSE) and non-streaming (JSON) requests to
 * /v1/chat/completions. When toolCall is configured, the server simulates
 * a tool-use round-trip: the first request returns the tool call, and the
 * follow-up request (containing the tool result) returns followupChunks.
 *
 * @deprecated - Please use MockEngineManager in AIWindowTestUtils.sys.mjs unless
 * a test is explicitly needing to test the network layer of the OpenAI chat protocol.
 *   TODO (Bug 2045844): Remove and replace existing usages across test files.
 * @param {MockOpenAIServerOptions} [options]
 * @returns {{ server: HttpServer, port: number }} The running server and
 *   its port number.
 */

function startMockOpenAI({
  streamChunks = ["Hello from mock."],
  streamChunkDelayMs = 0,
  toolCall = null,
  followupChunks = ["Tool complete."],
  onRequest,
} = {}) {
  const server = new HttpServer();

  server.registerPathHandler("/v1/chat/completions", (request, response) => {
    let bodyText = "";
    if (request.method === "POST") {
      try {
        bodyText = readRequestBody(request);
      } catch (_) {}
    }

    let body;
    try {
      body = JSON.parse(bodyText || "{}");
    } catch (_) {
      body = {};
    }

    onRequest?.(body);

    const wantsStream = !!body.stream;
    const tools = Array.isArray(body.tools) ? body.tools : [];
    const askedForTools = tools.length;
    const messages = Array.isArray(body.messages) ? body.messages : [];
    const hasToolResult = messages.some(m => m && m.role === "tool");
    const timestamp = Math.floor(Date.now() / 1000);

    const startSSE = () => {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader(
        "Content-Type",
        "text/event-stream; charset=utf-8",
        false
      );
      response.setHeader("Cache-Control", "no-cache", false);
      response.setHeader("Access-Control-Allow-Origin", "*", false);
      response.processAsync();
    };

    const sendSSE = obj => {
      // Encode data so special §followup:§-type tokens preserves utf-8
      response.write(
        Array.from(
          new TextEncoder().encode(`data: ${JSON.stringify(obj)}\n\n`),
          b => String.fromCharCode(b)
        ).join("")
      );
    };

    if (wantsStream && toolCall && askedForTools && !hasToolResult) {
      startSSE();

      sendSSE({
        id: "chatcmpl-aiwindow-stream-tool-1",
        object: "chat.completion.chunk",
        created: timestamp,
        model: "aiwindow-mock",
        choices: [
          {
            index: 0,
            delta: {
              content: "",
              tool_calls: [
                {
                  index: 0,
                  id: "call_1",
                  type: "function",
                  function: {
                    name: toolCall.name,
                    arguments: toolCall.args ?? "{}",
                  },
                },
              ],
            },
            finish_reason: null,
          },
        ],
      });

      sendSSE({
        id: "chatcmpl-aiwindow-stream-tool-2",
        object: "chat.completion.chunk",
        created: timestamp,
        model: "aiwindow-mock",
        choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }],
      });

      response.write("data: [DONE]\n\n");
      response.finish();
      return;
    }

    if (wantsStream && toolCall && askedForTools && hasToolResult) {
      startSSE();

      followupChunks.forEach((chunk, index) => {
        sendSSE({
          id: `chatcmpl-aiwindow-stream-tool-followup-${index}`,
          object: "chat.completion.chunk",
          created: timestamp,
          model: "aiwindow-mock",
          choices: [
            {
              index: 0,
              delta: { content: chunk },
              finish_reason:
                index === followupChunks.length - 1 ? "stop" : null,
            },
          ],
        });
      });

      response.write("data: [DONE]\n\n");
      response.finish();
      return;
    }

    if (wantsStream) {
      startSSE();

      (async () => {
        for (const [index, chunk] of streamChunks.entries()) {
          if (streamChunkDelayMs) {
            await new Promise(resolve =>
              setTimeout(resolve, streamChunkDelayMs)
            );
          }
          sendSSE({
            id: `chatcmpl-aiwindow-stream-${index}`,
            object: "chat.completion.chunk",
            created: timestamp,
            model: "aiwindow-mock",
            choices: [
              {
                index: 0,
                delta: { content: chunk },
                finish_reason:
                  index === streamChunks.length - 1 ? "stop" : null,
              },
            ],
          });
        }

        response.write("data: [DONE]\n\n");
        response.finish();
      })();
      return;
    }

    // Non-streaming fallback for conversation starters, title generation, etc.
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.setHeader("Content-Type", "application/json", false);
    response.write(
      JSON.stringify({
        id: "chatcmpl-aiwindow-non-stream",
        object: "chat.completion",
        created: timestamp,
        model: "aiwindow-mock",
        choices: [
          {
            index: 0,
            message: { role: "assistant", content: "Mock response" },
            finish_reason: "stop",
          },
        ],
      })
    );
  });

  server.start(-1);
  return { server, port: server.identity.primaryPort };
}

/**
 * Stops a running mock OpenAI server.
 *
 * @param {HttpServer} server - The server instance returned by startMockOpenAI.
 * @returns {Promise<void>} Resolves when the server has fully stopped.
 */

function stopMockOpenAI(server) {
  return new Promise(resolve => server.stop(resolve));
}

/**
 * Retrieves the context chip labels from a user message rendered in the
 * ai-chat-content area. Waits for the aichat-browser, chat content, and
 * chips to be available before reading labels.
 *
 * @param {MozBrowser} sidebarBrowser - The sidebar browser element
 *   (e.g. win.document.getElementById("ai-window-browser"))
 * @param {number} [messageIndex=0] - Zero-based index selecting which user
 *   message to read chips from (0 = first user message, 1 = second, etc.)
 * @returns {Promise<string[]>} Array of chip label strings from the
 *   website-chip-container in the selected user message
 */

async function getUserMessageChipLabels(sidebarBrowser, messageIndex = 0) {
  await BrowserTestUtils.waitForCondition(
    () => sidebarBrowser.contentDocument?.querySelector("ai-window:defined"),
    "Sidebar ai-window should be loaded"
  );

  const aiWindowEl = sidebarBrowser.contentDocument.querySelector("ai-window");
  const aichatBrowser = await BrowserTestUtils.waitForCondition(
    () => aiWindowEl.shadowRoot?.querySelector("#aichat-browser"),
    "Wait for aichat-browser"
  );

  return SpecialPowers.spawn(aichatBrowser, [messageIndex], async msgIndex => {
    const chatContent = await ContentTaskUtils.waitForCondition(
      () => content.document.querySelector("ai-chat-content"),
      "Wait for ai-chat-content"
    );

    const containers = await ContentTaskUtils.waitForCondition(() => {
      const found = chatContent.shadowRoot.querySelectorAll(
        ".chat-bubble-user website-chip-container"
      );
      return found.length > msgIndex ? found : null;
    }, `Wait for user message at index ${msgIndex}`);

    const chipContainer = containers[msgIndex];

    const chips = await ContentTaskUtils.waitForCondition(() => {
      const found =
        chipContainer.shadowRoot.querySelectorAll("ai-website-chip");
      return found.length ? found : null;
    }, "Wait for context chips to render");

    return Array.from(chips).map(
      chip => chip.shadowRoot?.querySelector(".chip-label")?.textContent ?? ""
    );
  });
}

/**
 * Convenience wrapper that starts a mock OpenAI server, pushes the endpoint
 * pref, stubs getFxAccountToken, runs a task, then tears everything down.
 *
 * Consider using stubEngineNetworkBoundaries instead for new tests — it
 * additionally stubs openAIEngine.build to prevent leaked-window issues from
 * background async operations, and its setup/restore pattern fits
 * beforeEach/afterEach without requiring a callback wrapper.
 *
 * @param {MockOpenAIServerOptions} serverOptions - Options for the mock server.
 * @param {Function} task - Async callback receiving { port }.
 */

async function withServer(serverOptions, task) {
  const { server, port } = startMockOpenAI(serverOptions);
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.smartwindow.endpoint", `http://localhost:${port}/v1`],
      ["browser.smartwindow.customEndpoint", `http://localhost:${port}/v1`],
    ],
  });

  const getFxAccountTokenStub = sinon
    .stub(openAIEngine, "getFxAccountToken")
    .resolves("mock-fxa-token");

  try {
    await task({ port });
  } finally {
    getFxAccountTokenStub.restore();
    await SpecialPowers.popPrefEnv();
    await stopMockOpenAI(server);
  }
}

Messung V0.5 in Prozent
C=95 H=96 G=95

¤ Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.0.99Bemerkung:  (vorverarbeitet am  2026-09-30) ¤

*Bot Zugriff






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1126438
#Domains=1897691