Merge remote-tracking branch 'origin/main' into kennylopez-agent-catalog-sharing

This commit is contained in:
kenny lopez
2026-07-23 10:49:11 -07:00
44 changed files with 3282 additions and 399 deletions
+144
View File
@@ -216,4 +216,148 @@ test.describe("community rail", () => {
expect(toggleBox).not.toBeNull();
expect(toggleBox?.x ?? 0).toBeLessThan(120);
});
test("drag-to-reorder updates the stored community order and survives reload", async ({
page,
}) => {
await installMockBridge(page, undefined, { skipCommunitySeed: true });
// Seed only if not already set so the persisted order survives page.reload().
await page.addInitScript(
({ list, active }) => {
if (!window.localStorage.getItem("buzz-communities")) {
window.localStorage.setItem("buzz-communities", JSON.stringify(list));
}
if (!window.localStorage.getItem("buzz-active-community-id")) {
window.localStorage.setItem("buzz-active-community-id", active);
}
},
{ list: [COMMUNITY_A, COMMUNITY_B], active: COMMUNITY_A.id },
);
await page.goto("/");
const buttonA = page.getByTestId(`community-rail-button-${COMMUNITY_A.id}`);
const buttonB = page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`);
await expect(buttonA).toBeVisible();
await expect(buttonB).toBeVisible();
// Drag B (lower) up over A (higher) so the order becomes [B, A].
const boxA = await buttonA.boundingBox();
const boxB = await buttonB.boundingBox();
if (!boxA || !boxB) throw new Error("community buttons not laid out");
const startX = boxB.x + boxB.width / 2;
const startY = boxB.y + boxB.height / 2;
const targetY = boxA.y + boxA.height / 2;
// dnd-kit PointerSensor requires a 6px activation distance before it picks
// up the drag. Move in small steps so pointermove events fire on every pixel.
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(startX, startY - 3, { steps: 3 });
await page.mouse.move(startX, targetY, { steps: 20 });
await page.mouse.up();
// The community list in localStorage must now be [B, A].
await expect
.poll(() =>
page.evaluate(() => {
const raw = window.localStorage.getItem("buzz-communities");
if (!raw) return null;
const list = JSON.parse(raw) as Array<{ id: string }>;
return list.map((c) => c.id);
}),
)
.toEqual([COMMUNITY_B.id, COMMUNITY_A.id]);
// Verify the new order is also reflected in the rendered DOM — B button
// must appear above A button.
const newBoxA = await buttonA.boundingBox();
const newBoxB = await buttonB.boundingBox();
if (!newBoxA || !newBoxB)
throw new Error("community buttons not laid out after drag");
expect(newBoxB.y).toBeLessThan(newBoxA.y);
// Reload and confirm the order survives restart: addInitScript is
// conditional (no-op when data already exists), so the dragged [B, A]
// order is what React reads on boot.
await page.reload();
await expect(page.getByTestId("community-rail")).toBeVisible();
// Storage must still be [B, A] after reload.
const storedOrder = await page.evaluate(() => {
const raw = window.localStorage.getItem("buzz-communities");
if (!raw) return null;
const list = JSON.parse(raw) as Array<{ id: string }>;
return list.map((c) => c.id);
});
expect(storedOrder).toEqual([COMMUNITY_B.id, COMMUNITY_A.id]);
// DOM order must also be [B, A] after reload.
const reloadBoxA = await buttonA.boundingBox();
const reloadBoxB = await buttonB.boundingBox();
if (!reloadBoxA || !reloadBoxB)
throw new Error("community buttons not laid out after reload");
expect(reloadBoxB.y).toBeLessThan(reloadBoxA.y);
});
test("keyboard reorder: Space to pick up, ArrowUp to move, Space to drop updates stored order", async ({
page,
}) => {
await installMockBridge(page, undefined, { skipCommunitySeed: true });
await seedCommunities(page, [COMMUNITY_A, COMMUNITY_B], COMMUNITY_A.id);
await page.goto("/");
const buttonA = page.getByTestId(`community-rail-button-${COMMUNITY_A.id}`);
const buttonB = page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`);
await expect(buttonA).toBeVisible();
await expect(buttonB).toBeVisible();
// Focus B (the second/lower item) and use keyboard to move it above A.
// Note: page.keyboard.press("Space") fires the button's native click on this
// Chromium build even when React's onKeyDown calls preventDefault — a CDP
// input-injection quirk. The synthetic dispatch below goes directly through
// React's event system where preventDefault correctly suppresses the click,
// while still exercising the real KeyboardSensor path (Thufir verified the
// test fails when KeyboardSensor is removed).
await buttonB.focus();
await page.evaluate((testId) => {
const el = document.querySelector(`[data-testid="${testId}"]`);
if (!el) throw new Error(`button not found: ${testId}`);
el.dispatchEvent(
new KeyboardEvent("keydown", {
key: " ",
code: "Space",
bubbles: true,
cancelable: true,
}),
);
}, `community-rail-button-${COMMUNITY_B.id}`);
// ArrowUp moves the active item one slot up.
await page.keyboard.press("ArrowUp");
// Space drops the item — same synthetic dispatch for consistency.
await page.evaluate((testId) => {
const el = document.querySelector(`[data-testid="${testId}"]`);
if (!el) throw new Error(`button not found: ${testId}`);
el.dispatchEvent(
new KeyboardEvent("keydown", {
key: " ",
code: "Space",
bubbles: true,
cancelable: true,
}),
);
}, `community-rail-button-${COMMUNITY_B.id}`);
// The community list in localStorage must now be [B, A].
await expect
.poll(() =>
page.evaluate(() => {
const raw = window.localStorage.getItem("buzz-communities");
if (!raw) return null;
const list = JSON.parse(raw) as Array<{ id: string }>;
return list.map((c) => c.id);
}),
)
.toEqual([COMMUNITY_B.id, COMMUNITY_A.id]);
});
});
@@ -431,6 +431,76 @@ test("defaults renders only fields supported by the selected harness", async ({
).toHaveCount(0);
});
test("defaults hides model when optional harness has empty discovery", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
discoverAgentModels: {
models: [],
supportsSwitching: false,
},
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
// Confirmed successful empty catalog — omit the Model control; harness
// default applies and Finish stays available.
await expect(page.getByTestId("global-agent-model")).toHaveCount(0);
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
});
test("defaults keeps model control when optional harness discovery fails", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
discoverAgentModelsError: "CLI discovery timed out",
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
// Failed discovery must not look like successful empty: keep the control
// and surface #2246 failure UI (status line bypasses onboarding-essential).
await expect(page.getByTestId("global-agent-model")).toBeVisible();
await expect(page.getByText(/Could not load live models/i)).toBeVisible();
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
});
test("defaults Back returns to harness setup", async ({ page }) => {
await installMockBridge(
page,
+21
View File
@@ -431,6 +431,27 @@ type MockBridgeOptions = {
* test can interleave edits and exercise the mid-save race handling.
*/
globalConfigSaveDelayMs?: number;
/**
* Override the `discover_agent_models` mock response. When set, the bridge
* returns this catalog instead of the default per-harness model list.
* Use `{ models: [], supportsSwitching: false }` to exercise empty discovery
* (e.g. optional harnesses that omit the Model control).
*/
discoverAgentModels?: {
models: Array<{
id: string;
name: string | null;
description?: string | null;
}>;
supportsSwitching: boolean;
agentDefaultModel?: string | null;
selectedModel?: string | null;
};
/**
* When set, `discover_agent_models` throws with this message instead of
* returning a catalog. Exercises the discovery-failure UI path.
*/
discoverAgentModelsError?: string;
};
type BridgeOptions = {