Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions ts/examples/workflow/cli/test/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,37 @@ function workflowPath(name: string): string {
/** Run the CLI and return { stdout, stderr, code }. */
function runCli(
args: string[],
timeout = 30_000,
): Promise<{ stdout: string; stderr: string; code: number }> {
return new Promise((resolve) => {
execFile(
"node",
return new Promise((resolve, reject) => {
const child = execFile(
process.execPath,
[CLI, ...args],
{ timeout: 30_000, maxBuffer: 1024 * 1024 },
{ timeout, maxBuffer: 1024 * 1024 },
(err, stdout, stderr) => {
const code = err && "code" in err ? (err.code as number) : 0;
resolve({ stdout, stderr, code: code ?? 0 });
const code = err?.code;
if (
err &&
(err.killed || err.signal || typeof code !== "number")
) {
reject(
Object.assign(
new Error(
`CLI process did not exit normally: ${err.message}\nstdout: ${stdout}\nstderr: ${stderr}`,
),
{ cause: err },
),
);
return;
}
resolve({
stdout,
stderr,
code: typeof code === "number" ? code : 0,
});
},
);
child.stdin?.end();
});
}

Expand All @@ -94,6 +114,12 @@ function cliTest(name: string, fn: () => Promise<void>): void {
}

describe("CLI integration", () => {
cliTest("reports child timeouts instead of a successful exit", async () => {
await expect(runCli(["list-tasks"], 1)).rejects.toThrow(
"CLI process did not exit normally",
);
});

describe("list-tasks", () => {
cliTest("lists all registered tasks", async () => {
const { stdout, code } = await runCli(["list-tasks"]);
Expand Down
34 changes: 34 additions & 0 deletions ts/packages/agents/browser/src/views/client/library/memoryHub.css
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,40 @@ body[data-memory-hub] {
.hub-tabs > :focus-visible {
outline-offset: -3px;
}
.hub-view-type-tabs {
overflow: visible;
align-items: center;
}
.hub-view-type-tabs > .hub-menu-host {
flex: 0 0 auto;
margin-left: auto;
}
.hub-view-type-tabs > [hidden] {
display: none;
}
.hub-view-type-tabs > .hub-menu-host > button {
border: 0;
border-bottom: 2px solid transparent;
border-radius: 0;
}
.hub-view-type-tabs > .hub-menu-host > button[aria-pressed="true"] {
color: var(--primary-dark);
border-bottom-color: var(--primary);
}
.hub-view-type-tabs .hub-menu {
top: 100%;
max-width: calc(100vw - 32px);
max-height: 60vh;
overflow-y: auto;
}
.hub-view-type-tabs .hub-menu [aria-checked="true"] {
color: var(--primary-dark);
background: var(--accent-soft);
font-weight: 650;
}
.hub-view-type-tabs .hub-menu button {
margin: 0;
}
.hub-explore-nav {
margin-top: 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ test("bounded per-collection pages traverse exhausted collections and restore pr
{ corpusId: "b", total: 3 },
]);
expect(pager.total).toBe(63);
expect(pager.canNavigate).toBe(true);
const controls = pager.controls("views", reload);
expect(controls.hidden).toBe(false);
const next = controls.querySelector<HTMLButtonElement>(
'[aria-label="Next views page"]',
)!;
Expand All @@ -33,6 +35,8 @@ test("bounded per-collection pages traverse exhausted collections and restore pr
.click();
pager.update([{ corpusId: "a", total: 60 }]);
expect(pager.hasNext).toBe(false);
expect(pager.canNavigate).toBe(true);
expect(pager.controls("views", reload).hidden).toBe(false);
expect(pager.total).toBe(63);
pager
.controls("views", reload)
Expand All @@ -49,6 +53,8 @@ test("bounded per-collection pages traverse exhausted collections and restore pr
pager.reset();
expect(pager.total).toBe(0);
expect(pager.hasNext).toBe(false);
expect(pager.canNavigate).toBe(false);
expect(pager.controls("views", reload).hidden).toBe(true);
expect(
pager
.controls("views", reload)
Expand All @@ -57,3 +63,17 @@ test("bounded per-collection pages traverse exhausted collections and restore pr
)!.disabled,
).toBe(true);
});

test.each([0, 1, 25, 50])(
"hides static controls when %i total items fit on one page per collection",
(total) => {
const pager = new MemoryHubInventoryPager();
pager.update([
{ corpusId: "a", total: Math.min(total, 25) },
{ corpusId: "b", total: Math.max(total - 25, 0) },
]);
expect(pager.total).toBe(total);
expect(pager.canNavigate).toBe(false);
expect(pager.controls("views", jest.fn()).hidden).toBe(true);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ export class MemoryHubInventoryPager {
public get hasNext() {
return [...this.next.values()].some((token) => token !== null);
}
public get canNavigate() {
return this.previous.length > 0 || this.hasNext;
}
public controls(label: string, reload: () => void): HTMLElement {
const nav = rbNode("nav", undefined, "hub-pager");
nav.setAttribute("aria-label", `${label} pages`);
nav.hidden = !this.canNavigate;
const back = rbIconButton(
"fa-chevron-left",
`Previous ${label} page`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export function inventoryFixture(legacy: Fixture): Fixture {
(item) =>
(!filters.viewId ||
item.viewId === filters.viewId) &&
(!filters.kind ||
item.content.kind === filters.kind) &&
!filters.excludeKinds?.includes(item.content.kind),
),
request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const view = {
};
const head = "a".repeat(40);
async function settle() {
for (let index = 0; index < 20; index++) await Promise.resolve();
for (let index = 0; index < 60; index++) await Promise.resolve();
}
function click(host: HTMLElement, label: string) {
const button = [...host.querySelectorAll("button")].find(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type MenuItem = {
action: () => void;
danger?: boolean;
disabled?: boolean | (() => boolean);
checked?: boolean;
};

export function icon(name: string): HTMLElement {
Expand Down Expand Up @@ -120,7 +121,12 @@ export function menuButton(
for (const item of items) {
const entry = document.createElement("button");
entry.type = "button";
entry.setAttribute("role", "menuitem");
entry.setAttribute(
"role",
item.checked === undefined ? "menuitem" : "menuitemradio",
);
if (item.checked !== undefined)
entry.setAttribute("aria-checked", String(item.checked));
entry.disabled =
typeof item.disabled === "function"
? item.disabled()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ test("topic generations are not presented as openable Wiki article revisions", a
}
});

test("current indexing attention is counted once and maintenance plans respect the 32-view bound", async () => {
const views: ViewVersion[] = Array.from({ length: 33 }, (_, index) => ({
function guide(index: number): ViewVersion {
return {
corpusId: "alpha",
viewId: `guide-${index}`,
revisionId: `r-${index}`,
Expand All @@ -290,7 +290,148 @@ test("current indexing attention is counted once and maintenance plans respect t
},
relationships: [],
provenance: "generated",
}));
};
}

test.each([
[false, false],
[true, false],
[false, true],
[true, true],
])(
"Inbox pagination labels independent view (%s) and build (%s) feeds only when navigable",
async (moreViews, moreBuilds) => {
const previous = invoke.getMockImplementation()!;
invoke.mockImplementation(
inventoryFixture(async (method, params) => {
if (method === "memoryListViews")
return {
head: null,
views: Array.from(
{ length: moreViews ? 26 : 1 },
(_, index) => guide(index),
),
};
if (method === "memoryListViewBuilds")
return Array.from(
{ length: moreBuilds ? 26 : 1 },
(_, index) => ({ ...job(), jobId: `job-${index}` }),
);
if (method === "memoryGetViewPublication")
return { indexState: "absent" };
return previous(method, params);
}),
);
const attention = document.createElement("div");
const onAttentionCount = jest.fn();
const onError = jest.fn();
const mounted = mountMemoryHubViewActivity(
document.createElement("div"),
attention,
{
scope: () => "alpha",
onError,
onOpenView: jest.fn(),
onOpenBuild: jest.fn(),
onAttentionCount,
},
);
try {
await mounted.refresh();
const viewPages = () =>
attention.querySelector<HTMLElement>(
'[aria-label="view attention pages"]',
)!;
const buildPages = () =>
attention.querySelector<HTMLElement>(
'[aria-label="operation attention pages"]',
)!;
expect(viewPages().hidden).toBe(!moreViews);
expect(buildPages().hidden).toBe(!moreBuilds);
expect(viewPages().textContent).toContain("View issues");
expect(buildPages().textContent).toContain("Build issues");
expect(onAttentionCount).toHaveBeenLastCalledWith(
1,
moreViews || moreBuilds,
);
if (moreViews && moreBuilds) {
const nextViews = new Promise<void>((resolve) =>
onAttentionCount.mockImplementationOnce(() => resolve()),
);
textButton(attention, "Next view attention page").click();
await nextViews;
expect(invoke).toHaveBeenCalledWith("memoryListViewSummaries", {
corpusId: "alpha",
pageSize: 25,
continuationToken: "25",
});
expect(
invoke.mock.calls
.filter(
([method]) =>
method === "memoryListViewBuildSummaries",
)
.at(-1),
).toEqual([
"memoryListViewBuildSummaries",
{ corpusId: "alpha", pageSize: 25 },
]);
const nextBuilds = new Promise<void>((resolve) =>
onAttentionCount.mockImplementationOnce(() => resolve()),
);
textButton(attention, "Next operation attention page").click();
await nextBuilds;
expect(invoke).toHaveBeenCalledWith(
"memoryListViewBuildSummaries",
{
corpusId: "alpha",
pageSize: 25,
continuationToken: "25",
},
);
expect(viewPages().hidden).toBe(false);
expect(buildPages().hidden).toBe(false);
expect(onAttentionCount).toHaveBeenLastCalledWith(1, false);
}
expect(onError).not.toHaveBeenCalled();
} finally {
mounted.dispose();
}
},
);

test("empty Inbox attention has neither visible pagers nor a phantom heading", async () => {
const previous = invoke.getMockImplementation()!;
invoke.mockImplementation((method, params) =>
method === "memoryListViewBuildSummaries"
? { items: [], total: 0 }
: previous(method, params),
);
const attention = document.createElement("div");
const onAttentionCount = jest.fn();
const mounted = mountMemoryHubViewActivity(
document.createElement("div"),
attention,
{
scope: () => "alpha",
onError: jest.fn(),
onOpenView: jest.fn(),
onOpenBuild: jest.fn(),
onAttentionCount,
},
);
try {
await mounted.refresh();
expect(attention.querySelector("h3")).toBeNull();
expect(attention.querySelectorAll("nav:not([hidden])")).toHaveLength(0);
expect(onAttentionCount).toHaveBeenLastCalledWith(0, false);
} finally {
mounted.dispose();
}
});

test("current indexing attention is counted once and maintenance plans respect the 32-view bound", async () => {
const views = Array.from({ length: 33 }, (_, index) => guide(index));
invoke.mockImplementation(
inventoryFixture(
async (
Expand Down
Loading
Loading