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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test:webui": "node scripts/run-vitest-suite.mjs webui",
"test:webui-browser": "playwright install chromium && playwright test",
"typecheck:webui": "pnpm --filter @mavis/webui typecheck:server && pnpm --filter @mavis/webui typecheck:client && pnpm --filter @mavis/webui typecheck:test",
"typecheck:webui-full": "pnpm --filter @mavis/webui typecheck:test-full",
"check:source": "node scripts/source-inventory.mjs",
"test:capabilities": "node scripts/run-vitest-suite.mjs capability",
"test:windows": "node scripts/run-vitest-suite.mjs windows",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ export class FeishuSender {
if (spec.endpoint === 'files') form.set('file_name', uploadFileName);
form.set(
spec.endpoint === 'images' ? 'image' : 'file',
new Blob([buffer], { type: uploadMimeType }),
new Blob([new Uint8Array(buffer)], { type: uploadMimeType }),
uploadFileName,
);
const response = await this.fetcher(`${FEISHU_API_BASE}/im/v1/${spec.endpoint}`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class TelegramSender {
form.set('chat_id', chatId);
const threadId = toMessageThreadId(messageThreadId);
if (threadId !== undefined) form.set('message_thread_id', String(threadId));
form.set(field, new Blob([buffer]), fileName);
form.set(field, new Blob([new Uint8Array(buffer)]), fileName);
if (ref.caption) form.set('caption', ref.caption);
const response = await this.fetcher(this.endpoint(method), {
method: 'POST',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export function createRuntimeTransportHost(
signal: entry.controller.signal,
};
if (body !== undefined) {
init.body = body;
init.body = typeof body === 'string' ? body : new Uint8Array(body);
init.duplex = 'half';
}
const response = await handleRequest(new Request(url, init));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export async function uploadArchive(input: UploadArchiveInput): Promise<Uploaded
await runDeployStage(uploadStage, input.signal, () =>
input.gateway.putBytes(
putUrl,
archive,
new Uint8Array(archive),
{ 'Content-Type': ARCHIVE_MIME },
input.signal,
input.timeoutMs,
Expand Down
1 change: 1 addition & 0 deletions packages/webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"typecheck:server": "tsc -p tsconfig.server.json --noEmit",
"typecheck:client": "tsc -p tsconfig.client.json --noEmit",
"typecheck:test": "tsc -p tsconfig.test.json --noEmit",
"typecheck:test-full": "tsc -p tsconfig.test-full.json --noEmit",
"build:styles": "node ../../scripts/build-webui-styles.mjs",
"test": "node ../../scripts/run-vitest-suite.mjs webui"
},
Expand Down
8 changes: 7 additions & 1 deletion packages/webui/src/client/components/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,8 +417,14 @@ export interface MessagePassiveLoadingPlaceholderProps {
label?: string;
}

// No default for `props`: React's `createElement` always materialises a props
// object (it substitutes `{}` for a missing config), so the `= {}` default was
// unreachable from either call site — `SessionTranscript.tsx` passes `label`,
// and the tests call through `createElement`. Keeping it made the parameter
// type `Props | undefined`, which defeats `createElement`'s `P extends {}`
// inference and pushed `label` onto `Attributes` as an excess property.
export function MessagePassiveLoadingPlaceholder(
props: MessagePassiveLoadingPlaceholderProps = {},
props: MessagePassiveLoadingPlaceholderProps,
): React.JSX.Element {
const { label } = props;
return (
Expand Down
59 changes: 35 additions & 24 deletions packages/webui/test/unit/composer-intent.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { createElement } from "react";
import {
deriveRecentWorkspaceDirs,
isTurnLive,
Expand Down Expand Up @@ -41,37 +42,47 @@ import { setPermissionModeOperation } from "../../src/server/operation/permissio
* job is to emit the right kind).
*/

const goalCommand: SlashCommandEntry = {
name: "goal",
description: "open the goal workflow",
section: "session",
supported: true,
detail: undefined,
};
const ICON: SlashCommandEntry["icon"] = () => createElement("span");

/**
* The resolver reads only `name` and `supported`, but `SlashCommandEntry`
* also declares the display fields the palette renders, so the fixtures
* carry the full shape instead of a partial one.
*/
const commandEntry = (
name: string,
description: string,
supported: boolean,
): SlashCommandEntry => ({
name,
displayName: name,
label: name,
description,
source_type: -1,
icon: ICON,
supported,
});

const helpCommand: SlashCommandEntry = {
/** The narrower type the `run-command` intent carries for its command. */
type WebuiRunCommandEntry = Extract<WebuiSubmissionIntent, { readonly kind: "run-command" }>["command"];

const goalCommand = commandEntry("goal", "open the goal workflow", true);

// Typed as the runnable narrowing of `SlashCommandEntry` so the fixtures below
// can stand in for the `command` the `run-command` intent carries.
const helpCommand: WebuiRunCommandEntry = {
name: "help",
displayName: "help",
label: "help",
description: "show help",
section: "session",
source_type: -1,
icon: ICON,
supported: true,
detail: undefined,
};

const disabledCommand: SlashCommandEntry = {
name: "compact",
description: "compact the session",
section: "session",
supported: false,
detail: undefined,
};
const disabledCommand = commandEntry("compact", "compact the session", false);

const unknownCommand: SlashCommandEntry = {
name: "totally-unknown",
description: "outline only",
section: "session",
supported: true,
detail: undefined,
};
const unknownCommand = commandEntry("totally-unknown", "outline only", true);

const runCommandArgs = (overrides: {
draft?: string;
Expand Down
27 changes: 27 additions & 0 deletions packages/webui/test/unit/outside-close.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ describe("evaluateComposerDismiss — the popover's container is its wrap, not t
evaluateComposerDismiss({
permissionMenuOpen: true,
insidePermissionWrap: false,
// Unread while the permission popover is open — the anchored branch
// resolves on `permissionMenuOpen` alone. Passed explicitly so the
// fixture matches the scenario (one anchored dropdown, not two).
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: true,
}),
).toEqual({
Expand All @@ -238,6 +243,8 @@ describe("evaluateComposerDismiss — the popover's container is its wrap, not t
evaluateComposerDismiss({
permissionMenuOpen: true,
insidePermissionWrap: true,
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: true,
}),
).toEqual({
Expand All @@ -252,6 +259,8 @@ describe("evaluateComposerDismiss — the popover's container is its wrap, not t
evaluateComposerDismiss({
permissionMenuOpen: true,
insidePermissionWrap: false,
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: false,
}),
).toEqual({
Expand All @@ -268,6 +277,12 @@ describe("evaluateComposerDismiss — the popover's container is its wrap, not t
evaluateComposerDismiss({
permissionMenuOpen: false,
insidePermissionWrap: false,
// Load-bearing here: with the permission popover shut, `addMenuOpen` is
// the flag that decides whether an anchored dropdown exists at all. The
// scenario is "no anchored dropdown, caret inside the region", so it
// must be false or the anchored branch would answer instead.
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: true,
}),
).toEqual({
Expand All @@ -284,6 +299,10 @@ describe("evaluateComposerDismiss — the popover's container is its wrap, not t
evaluateComposerDismiss({
permissionMenuOpen: false,
insidePermissionWrap: false,
// "no surface open yet" is the scenario this test names, so the second
// anchored dropdown has to be shut for the region rule to be reached.
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: false,
}),
).toEqual({
Expand All @@ -299,11 +318,15 @@ describe("evaluateComposerDismiss — the popover's container is its wrap, not t
const insideRegion = evaluateComposerDismiss({
permissionMenuOpen: true,
insidePermissionWrap: false,
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: true,
});
const outsideRegion = evaluateComposerDismiss({
permissionMenuOpen: true,
insidePermissionWrap: false,
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: false,
});
expect(insideRegion).toEqual(outsideRegion);
Expand All @@ -317,6 +340,10 @@ describe("evaluateComposerDismiss — the popover's container is its wrap, not t
evaluateComposerDismiss({
permissionMenuOpen: true,
insidePermissionWrap: undefined as unknown as boolean,
// Unreached while the popover is open; false keeps the fixture honest
// about the unmounted-ref scenario the casts above are simulating.
addMenuOpen: false,
insideAddWrap: false,
insideComposerRegion: undefined as unknown as boolean,
}).closePermissionMenu,
).toBe(true);
Expand Down
1 change: 0 additions & 1 deletion packages/webui/test/unit/plugin-market-catalogue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const marketplace = (initialArea: "plugins" | "skills"): string =>
renderToStaticMarkup(
createElement(PluginManagement, {
transport,
onClose: () => undefined,
initialArea,
}),
);
Expand Down
32 changes: 31 additions & 1 deletion packages/webui/test/unit/session-activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,31 @@ describe("rail rendering", () => {
const html = renderToStaticMarkup(
createElement(WebuiProjectList, {
page: { sessions: [session({ sessionId: "mvs_a" })] , hasMore: false },
projectRecords: [{ workspaceDir: "/tmp/project", name: "project" }],
// `loading` only reaches the "Load more" button, which needs
// `hasMore && onLoadMore`; this page is neither. Passed explicitly so
// the fixture is complete rather than accidentally short.
loading: false,
// A complete record, not a loose bag. The rail filters on `hidden`,
// sorts on `pinned`, and derives `updatedAt` from
// `recentAtMs ?? latestActivityAtMs` — so a partial fixture would let
// the row keep passing if any of those three were read wrongly. The
// record's own activity is deliberately 5h old, the same stale figure
// the session's `updatedAt` carries, which is what makes the `>2h` /
// not-`>5h` pair below discriminating: the age has to come from the
// activity map because the project record offers a competing 5h.
projectRecords: [
{
projectId: 1,
projectKind: "workspace",
workspaceDir: "/tmp/project",
pinned: false,
hidden: false,
orderIndex: 0,
recentAtMs: null,
latestActivityAtMs: NOW - 5 * HOUR,
sessionCount: 1,
},
],
// Deliberately NOT the session's `updatedAt`: the row must read the
// activity map, or a session that ran since the list was fetched would
// still show the stale time -- and this assertion would not notice.
Expand All @@ -302,6 +326,7 @@ describe("rail rendering", () => {
renderToStaticMarkup(
createElement(WebuiSessionList, {
page: { sessions: [session({ sessionId: "mvs_a" })], hasMore: false },
loading: false,
activity: {
mvs_a: { lastActivityAt: NOW - 5 * HOUR, ...(busy ? { busy: { turnId: "t1", busyReason: "turn" as const } } : {}) },
},
Expand All @@ -320,6 +345,7 @@ describe("rail rendering", () => {
const html = renderToStaticMarkup(
createElement(WebuiSessionList, {
page: { sessions: [session({ sessionId: "mvs_a" })], hasMore: false },
loading: false,
}),
);
expect(html).not.toMatch(/webui-rail-session-meta/u);
Expand Down Expand Up @@ -658,6 +684,7 @@ describe("unread badge rendering", () => {
const html = renderToStaticMarkup(
createElement(WebuiSessionList, {
page: { sessions: [session({ sessionId: "mvs_a" })], hasMore: false },
loading: false,
activity: { mvs_a: { lastActivityAt: NOW - 2 * HOUR, unread: 1 } },
now: NOW,
}),
Expand All @@ -675,6 +702,7 @@ describe("unread badge rendering", () => {
const html = renderToStaticMarkup(
createElement(WebuiSessionList, {
page: { sessions: [session({ sessionId: "mvs_a" })], hasMore: false },
loading: false,
activity: {
mvs_a: { lastActivityAt: NOW, unread: 3, busy: { turnId: "t2", busyReason: "turn" } },
},
Expand All @@ -689,6 +717,7 @@ describe("unread badge rendering", () => {
const html = renderToStaticMarkup(
createElement(WebuiSessionList, {
page: { sessions: [session({ sessionId: "mvs_a" })], hasMore: false },
loading: false,
activity: { mvs_a: { lastActivityAt: NOW - 2 * HOUR, unread: 0 } },
now: NOW,
}),
Expand All @@ -704,6 +733,7 @@ describe("unread badge rendering", () => {
const html = renderToStaticMarkup(
createElement(WebuiSessionList, {
page: { sessions: [session({ sessionId: "mvs_a" })], hasMore: false },
loading: false,
activity: { mvs_a: { lastActivityAt: NOW - 2 * HOUR, unread: 150 } },
now: NOW,
}),
Expand Down
Loading
Loading