fix: event-loop freezes, plugin-install boot crash, tmux attach/scrollbar, Codex transcripts
- GET /api/workflows: one LEAD() window pass in a read worker (read-pool.js, workflow-stats.js); idx_events_session restored, idx_events_tool added - compat-sqlite gains function/pluck/readonly so a plugin install without a better-sqlite3 build boots again; antigravity-usage uses db.js's Database - install-hooks adds no checkout hooks while the agenthub plugin is enabled (events were doubled) - terminal attach runs the live tmux server's own binary; the pane scrollbar gets a visible 2-column style, and claude panes start with CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 so tmux keeps history to scroll - Codex sessions' conversation in GET /api/sessions/:id/transcript - Codex/Antigravity sessions claimed by their workspace without hooks - UI-audit follow-ups, docs and OpenAPI
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @file ActivityFeed.agents.test.ts
|
||||
* @description The Activity feed asks for agent labels by agent, not by
|
||||
* session: a subagent spawned later in a session already on screen is fetched
|
||||
* too, and an agent already asked about is not asked for again.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { agentsToFetch } from "../ActivityFeed";
|
||||
|
||||
describe("agentsToFetch", () => {
|
||||
it("asks again for a session once a new agent of it appears", () => {
|
||||
const asked = new Set(["s1-main"]);
|
||||
const events = [
|
||||
{ session_id: "s1", agent_id: "s1-main" },
|
||||
{ session_id: "s1", agent_id: "s1-sub-late" },
|
||||
{ session_id: "s2", agent_id: null },
|
||||
];
|
||||
expect(agentsToFetch(events, asked)).toEqual({
|
||||
agentIds: ["s1-sub-late"],
|
||||
sessionIds: ["s1"],
|
||||
});
|
||||
});
|
||||
|
||||
it("asks nothing when every agent on screen is known", () => {
|
||||
expect(agentsToFetch([{ session_id: "s1", agent_id: "a" }], new Set(["a"]))).toEqual({
|
||||
agentIds: [],
|
||||
sessionIds: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render, screen, act, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { MemoryRouter, useLocation, useNavigate } from "react-router-dom";
|
||||
import { settleConfirm, subscribeConfirm, type PendingConfirm } from "../../lib/confirm";
|
||||
import { eventBus } from "../../lib/eventBus";
|
||||
|
||||
@@ -119,6 +119,40 @@ describe("CcConfig — settings editor", () => {
|
||||
expect(ccConfigMock.patchSettings).toHaveBeenCalledWith({ scope: "user", unset: ["a"] });
|
||||
});
|
||||
|
||||
it("a tab change from the URL (a search result) asks before dropping an unsaved edit", async () => {
|
||||
const user = userEvent.setup();
|
||||
function Jump() {
|
||||
const navigate = useNavigate();
|
||||
const loc = useLocation();
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => navigate("/settings/claude?tab=skills")}>jump</button>
|
||||
<span data-testid="url">{loc.search}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/settings/claude?tab=settings"]}>
|
||||
<CcConfig />
|
||||
<Jump />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
await user.click(screen.getByRole("button", { name: /^edit$/i }));
|
||||
await user.click(screen.getAllByRole("button", { name: /remove key/i })[0]!);
|
||||
let pending: PendingConfirm | null = null;
|
||||
const off = subscribeConfirm((p) => (pending = p));
|
||||
await user.click(screen.getByRole("button", { name: "jump" }));
|
||||
await settle();
|
||||
expect(pending).not.toBeNull();
|
||||
// Keep editing: the draft stays and the URL goes back to its tab.
|
||||
await act(async () => settleConfirm(false));
|
||||
await settle();
|
||||
off();
|
||||
expect(screen.getByRole("button", { name: /^save$/i })).toBeInTheDocument();
|
||||
expect(screen.getByTestId("url")).toHaveTextContent("?tab=settings");
|
||||
});
|
||||
|
||||
it("a renamed key is sent under its new name, not dropped", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
|
||||
@@ -58,7 +58,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
import { SettingsHub } from "../SettingsHub";
|
||||
import { SettingsHub, backDelta } from "../SettingsHub";
|
||||
import { __resetAiTools } from "../../hooks/useAiTools";
|
||||
import { subscribeToasts, type ToastItem } from "../../lib/toast";
|
||||
|
||||
@@ -243,6 +243,21 @@ describe("SettingsHub", () => {
|
||||
await vi.waitFor(() => expect(row.className).toContain("ring-2"));
|
||||
});
|
||||
|
||||
it("a malformed #anchor is ignored instead of crashing the page", async () => {
|
||||
renderAt("/settings/data#%E0%A4");
|
||||
expect(await screen.findByTestId("settings-data")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Back always goes back: one step past the entry, at least one step", () => {
|
||||
// Entered at index 2, now two pages further on: back to index 1.
|
||||
expect(backDelta(4, 2)).toBe(-3);
|
||||
expect(backDelta(2, 2)).toBe(-1);
|
||||
// Browser Back moved below the entry with the hub still mounted: one step,
|
||||
// never history.go(0) (a reload) or a step forward.
|
||||
expect(backDelta(1, 2)).toBe(-1);
|
||||
expect(backDelta(0, 2)).toBe(-1);
|
||||
});
|
||||
|
||||
it("toasts when an unknown section falls back to General", async () => {
|
||||
let seen: ToastItem[] = [];
|
||||
const off = subscribeToasts((i) => (seen = i));
|
||||
|
||||
Reference in New Issue
Block a user