Compare commits

..
Author SHA1 Message Date
paseo-bot[bot] aa7ade90fd fix(server): retain feature definitions in stored agent snapshots
Stored fetches dropped saved features that unread and archive updates
already published. Project them through the existing normalizer so both
interfaces expose the same definitions without reopening the runtime.
2026-11-07 21:31:12 +00:00
6 changed files with 42 additions and 72 deletions
@@ -485,6 +485,43 @@ describe("toAgentPayload", () => {
});
});
describe("buildStoredAgentPayload features", () => {
it.each([null, "2026-11-01T00:00:00.000Z"])(
"preserves saved feature definitions like live snapshots (archivedAt=%s)",
(archivedAt) => {
const features: AgentFeature[] = [
createFeature({ value: false }),
{
type: "select",
id: "service_tier",
label: "Service tier",
value: "default",
options: [
{ id: "default", label: "Normal" },
{ id: "fast", label: "Fast" },
],
},
];
const agent = createManagedAgent({ features });
const record = { ...toStoredAgentRecord(agent), archivedAt };
const payload = buildStoredAgentPayload(record, [agent.provider]);
expect(payload.features).toEqual(features);
expect(payload.features).toEqual(toAgentPayload(agent).features);
expect(payload.features).not.toBe(record.features);
expect(payload.features?.[0]).not.toBe(record.features?.[0]);
},
);
it("uses an empty feature list for older records without saved definitions", () => {
const record = toStoredAgentRecord(createManagedAgent());
delete record.features;
expect(buildStoredAgentPayload(record, [record.provider]).features).toEqual([]);
});
});
describe("toRecentProviderSessionDescriptorPayload", () => {
it("projects provider import rows to provider-opaque public recent sessions", () => {
const session: ImportableProviderSession & { provider: string } = {
@@ -263,6 +263,7 @@ export function buildStoredAgentPayload(
capabilities: defaultCapabilities,
currentModeId: record.lastModeId ?? null,
availableModes: [],
features: normalizeFeatures(record.features),
pendingPermissions: [],
persistence,
title: record.title ?? null,
-30
View File
@@ -9,23 +9,6 @@ async function chooseLanguage(page: Page, language: string) {
await page.getByRole("menuitemradio", { name: language }).click();
}
async function withPageScriptsHeld(page: Page, run: (release: () => void) => Promise<void>) {
let releaseScripts!: () => void;
const scriptsReleased = new Promise<void>((resolve) => {
releaseScripts = resolve;
});
await page.route("**/*.js", async (route) => {
await scriptsReleased;
await route.continue();
});
try {
await run(releaseScripts);
} finally {
releaseScripts();
await page.unrouteAll({ behavior: "wait" });
}
}
function describeAlternates(links: Element[]): string[] {
return links.map((link) => `${link.getAttribute("hreflang")} ${link.getAttribute("href")}`);
}
@@ -131,7 +114,6 @@ test("shows the language as an abbreviation and lists every language by name", a
test("switches language from the keyboard", async ({ page }) => {
await page.goto("/");
await expect(languageButton(page)).toBeEnabled();
await languageButton(page).focus();
await page.keyboard.press("Enter");
await expect(page.getByRole("menuitemradio", { name: "English" })).toBeFocused();
@@ -201,15 +183,3 @@ test("shows the changelog in Chinese", async ({ page }) => {
await expect(page.getByRole("heading", { name: "新增" }).first()).toBeVisible();
await expect(page.getByRole("note")).toHaveCount(0);
});
test("waits for scripts before enabling the language menu", async ({ page }) => {
await withPageScriptsHeld(page, async (releaseScripts) => {
await page.goto("/docs/cli#running-agents", { waitUntil: "commit" });
await expect(languageButton(page)).toBeVisible();
await expect(languageButton(page)).toBeDisabled();
releaseScripts();
await languageButton(page).click();
await expect(page.getByRole("menuitemradio", { name: "English" })).toBeVisible();
});
});
-32
View File
@@ -181,23 +181,6 @@ test("keeps the directory's ranking window when searching", async ({ page }) =>
);
});
test("submits the visible search term when its input event was missed", async ({ page }) => {
await page.goto("/plugins?window=month");
await expect(page.getByRole("button", { name: "Language: English" }).first()).toBeEnabled();
await fillSearchWithMissedInputEvent(page, "graphite");
await page.keyboard.press("Enter");
await expect(page).toHaveURL(/\/plugins\/all\?q=graphite&window=month$/);
await expect(
page.getByRole("heading", { level: 1, name: /^Results for “graphite”/ }),
).toBeVisible();
await expect(page.getByRole("link", { name: "This month" })).toHaveAttribute(
"aria-current",
"true",
);
});
test("searches for a term typed before the page finished loading", async ({ page }) => {
const loadScripts = await holdScripts(page);
await page.goto("/plugins?window=month", { waitUntil: "domcontentloaded" });
@@ -436,21 +419,6 @@ async function searchPlugins(page: Page, term: string) {
await page.getByRole("searchbox", { name: "Search plugins" }).fill(term);
}
/** Reproduce text entered before hydration's input listener can observe it, without a clock race. */
async function fillSearchWithMissedInputEvent(page: Page, term: string) {
const search = page.getByRole("searchbox", { name: "Search plugins" });
await search.evaluate(() => {
for (const eventType of ["input", "change"]) {
document.addEventListener(eventType, (event) => event.stopImmediatePropagation(), {
capture: true,
once: true,
});
}
});
await search.fill(term);
await expect(search).toHaveValue(term);
}
async function submitSearch(page: Page, term: string) {
await searchPlugins(page, term);
await page.keyboard.press("Enter");
@@ -1,4 +1,4 @@
import { useHydrated, useLocation, useNavigate } from "@tanstack/react-router";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { AnimatePresence, motion, type Transition } from "framer-motion";
import { Check, ChevronDown, Languages } from "lucide-react";
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
@@ -33,7 +33,6 @@ type OpenFocus = "current" | "first" | "last";
* the menu button pattern: arrows move between languages, Escape and outside presses close it.
*/
export function LanguageSelector() {
const hydrated = useHydrated();
const { t } = useTranslation();
const locale = useLocale();
const location = useLocation();
@@ -126,7 +125,6 @@ export function LanguageSelector() {
<button
ref={triggerRef}
type="button"
disabled={!hydrated}
aria-label={t("languageSelector.current", { language: LANGUAGE_NAMES[locale] })}
aria-haspopup="menu"
aria-expanded={open}
@@ -68,16 +68,12 @@ export function PluginSearch({
input.current?.focus();
}, [edit]);
const handleSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
(event: FormEvent) => {
event.preventDefault();
// Live results already show the term.
if (!live) {
// Hydration can attach this handler before state adopts text already in the input.
const submittedTerm = String(new FormData(event.currentTarget).get("q") ?? "");
show(submittedTerm, { replace: false });
}
if (!live) show(term, { replace: false });
},
[live, show],
[live, show, term],
);
return (
<form