Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
4722 lines
137 KiB
TypeScript
4722 lines
137 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { setAgentGuiI18nTestLocale } from "../../i18n/testUtils";
|
|
import {
|
|
AgentMentionSearchController as BaseAgentMentionSearchController,
|
|
MAX_BROWSE_CACHE_ENTRIES,
|
|
preloadAgentMentionBrowse,
|
|
resetAgentMentionSearchBrowseCacheForTests,
|
|
type AgentMentionSearchState
|
|
} from "./AgentMentionSearchController";
|
|
import { issuePreviewText } from "./agentMentionSearchHelpers";
|
|
import type { AgentContextMentionProvider } from "./agentContextMentionProvider";
|
|
import { AGENT_CONTEXT_MENTION_PROVIDER_IDS } from "./agentContextMentionProvider";
|
|
|
|
// Controller work settles through timers or microtasks. Vitest's 50 ms polling
|
|
// default adds seconds of idle time across this state-heavy suite.
|
|
function waitForFast<T>(assertion: () => T | Promise<T>): Promise<T> {
|
|
return vi.waitFor(assertion, { interval: 1 });
|
|
}
|
|
|
|
interface TestFileMentionItem {
|
|
childCount?: number;
|
|
label: string;
|
|
href: string;
|
|
kind?: "directory" | "file";
|
|
}
|
|
|
|
interface TestIssueMentionItem {
|
|
issueId: string;
|
|
title: string;
|
|
status: string;
|
|
}
|
|
|
|
interface TestWorkspaceAppMentionItem {
|
|
appId: string;
|
|
description?: string;
|
|
name: string;
|
|
workspaceId: string;
|
|
}
|
|
|
|
interface TestAgentTargetMentionItem {
|
|
description?: string;
|
|
iconUrl?: string;
|
|
name: string;
|
|
provider: string;
|
|
status?: string;
|
|
targetId: string;
|
|
workspaceId: string;
|
|
}
|
|
|
|
interface TestSessionMentionItem {
|
|
agentTargetId?: string;
|
|
agentName: string;
|
|
id: string;
|
|
initiatorName: string;
|
|
provider: string;
|
|
scope?: "my_sessions" | "collab_sessions";
|
|
status: string;
|
|
title: string;
|
|
updatedAtUnixMs: number;
|
|
userId: string;
|
|
workspaceId: string;
|
|
}
|
|
|
|
const {
|
|
agentGeneratedFile: AGENT_GENERATED_FILE_PROVIDER_ID,
|
|
agentSession: AGENT_SESSION_PROVIDER_ID,
|
|
agentTarget: AGENT_TARGET_PROVIDER_ID,
|
|
file: FILE_PROVIDER_ID,
|
|
workspaceApp: WORKSPACE_APP_PROVIDER_ID,
|
|
workspaceIssue: WORKSPACE_ISSUE_PROVIDER_ID
|
|
} = AGENT_CONTEXT_MENTION_PROVIDER_IDS;
|
|
|
|
interface TestContextMentionProviderOptions {
|
|
queryAgentGeneratedFiles?: (input: any) => Promise<any>;
|
|
queryFiles?: (input: any) => Promise<any>;
|
|
queryIssues?: (input: any) => Promise<any>;
|
|
queryAgentTargets?: (input: any) => Promise<any>;
|
|
queryWorkspaceApps?: (input: any) => Promise<any>;
|
|
querySessions?: (input: any) => Promise<any>;
|
|
loadSessionSummary?: (input: any) => Promise<any>;
|
|
loadUserProfiles?: (input: any) => Promise<any>;
|
|
loadSessionMessages?: (input: any) => Promise<any>;
|
|
contextMentionProviders?: readonly AgentContextMentionProvider[];
|
|
debounceMs?: number;
|
|
diagnosticInfoLogger?: (payload: any) => void;
|
|
diagnosticNow?: () => number;
|
|
diagnosticSlowThresholdMs?: number;
|
|
fileLimit?: number;
|
|
browseCacheTtlMs?: number;
|
|
issueLimit?: number;
|
|
providerTimeoutMs?: number;
|
|
hiddenFilterIds?: readonly string[];
|
|
}
|
|
|
|
class AgentMentionSearchController extends BaseAgentMentionSearchController {
|
|
constructor(options: TestContextMentionProviderOptions) {
|
|
super({
|
|
debounceMs: options.debounceMs,
|
|
diagnosticInfoLogger: options.diagnosticInfoLogger,
|
|
diagnosticNow: options.diagnosticNow,
|
|
diagnosticSlowThresholdMs: options.diagnosticSlowThresholdMs,
|
|
fileLimit: options.fileLimit,
|
|
browseCacheTtlMs: options.browseCacheTtlMs,
|
|
issueLimit: options.issueLimit,
|
|
providerTimeoutMs: options.providerTimeoutMs,
|
|
hiddenFilterIds: options.hiddenFilterIds,
|
|
contextMentionProviders:
|
|
options.contextMentionProviders ??
|
|
createTestContextMentionProviders(options)
|
|
});
|
|
}
|
|
}
|
|
|
|
function createTestContextMentionProviders(
|
|
options: TestContextMentionProviderOptions
|
|
): readonly AgentContextMentionProvider[] {
|
|
return [
|
|
createTestFileProvider(options),
|
|
createTestAgentGeneratedFileProvider(options),
|
|
createTestAgentTargetProvider(options),
|
|
createTestWorkspaceAppProvider(options),
|
|
createTestIssueProvider(options),
|
|
createTestSessionProvider(options)
|
|
];
|
|
}
|
|
|
|
function createTestAgentGeneratedFileProvider(
|
|
options: TestContextMentionProviderOptions
|
|
): AgentContextMentionProvider<{ label: string; href: string }> {
|
|
return {
|
|
id: AGENT_GENERATED_FILE_PROVIDER_ID,
|
|
trigger: "@",
|
|
async query({ context, keyword, maxResults }) {
|
|
if (!options.queryAgentGeneratedFiles) {
|
|
return [];
|
|
}
|
|
const result = await options.queryAgentGeneratedFiles({
|
|
workspaceId: context?.metadata?.workspaceId,
|
|
query: keyword,
|
|
limit: maxResults,
|
|
sectionKey: context?.metadata?.sectionKey,
|
|
provenanceFilter: context?.metadata?.referenceProvenanceFilter
|
|
});
|
|
return (result.entries ?? []).map((entry: any) => ({
|
|
label: entry.name,
|
|
href: entry.path
|
|
}));
|
|
},
|
|
getItemKey: (item) => item.href,
|
|
getItemLabel: (item) => item.label,
|
|
toInsertResult: (item) => ({
|
|
kind: "markdown-link",
|
|
label: item.label,
|
|
href: item.href
|
|
})
|
|
};
|
|
}
|
|
|
|
function createTestFileProvider(
|
|
options: TestContextMentionProviderOptions
|
|
): AgentContextMentionProvider<TestFileMentionItem> {
|
|
const queryFiles = async (input: {
|
|
context: { metadata?: Readonly<Record<string, unknown>> };
|
|
directoryPath?: string;
|
|
keyword: string;
|
|
maxResults?: number;
|
|
}) => {
|
|
if (!options.queryFiles) {
|
|
return [];
|
|
}
|
|
const result = await options.queryFiles({
|
|
workspaceId: input.context?.metadata?.workspaceId,
|
|
query: input.keyword,
|
|
limit: input.maxResults,
|
|
includeKinds: ["file", "directory"],
|
|
directoryPath: input.directoryPath
|
|
});
|
|
return (result.entries ?? []).map((entry: any) => ({
|
|
childCount: entry.childCount,
|
|
label: entry.name,
|
|
href: entry.path,
|
|
kind: entry.kind
|
|
}));
|
|
};
|
|
return {
|
|
id: FILE_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: queryFiles,
|
|
queryDirectory: queryFiles,
|
|
getItemDirectory: (item) =>
|
|
item.kind === "directory"
|
|
? { childCount: item.childCount, path: item.href }
|
|
: null,
|
|
getItemKey: (item) => item.href,
|
|
getItemLabel: (item) => item.label,
|
|
toInsertResult: (item) => ({
|
|
kind: "markdown-link",
|
|
label: item.label,
|
|
href: item.href
|
|
})
|
|
};
|
|
}
|
|
|
|
function createTestIssueProvider(
|
|
options: TestContextMentionProviderOptions
|
|
): AgentContextMentionProvider<any> {
|
|
return {
|
|
id: WORKSPACE_ISSUE_PROVIDER_ID,
|
|
trigger: "@",
|
|
async query({ context, keyword, maxResults }) {
|
|
if (!options.queryIssues) {
|
|
return [];
|
|
}
|
|
const result = await options.queryIssues({
|
|
workspaceId: context?.metadata?.workspaceId,
|
|
pageSize: maxResults,
|
|
searchQuery: keyword
|
|
});
|
|
return result.issues ?? [];
|
|
},
|
|
getItemKey: (item) => item.issueId,
|
|
getItemLabel: (item) => item.title,
|
|
getItemSubtitle: (item) => issuePreviewText(item.content),
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.issueId,
|
|
label: item.title,
|
|
scope: { workspaceId: item.workspaceId },
|
|
presentation: {
|
|
description: issuePreviewText(item.content),
|
|
status: item.status
|
|
}
|
|
}
|
|
})
|
|
};
|
|
}
|
|
|
|
function createTestAgentTargetProvider(
|
|
options: TestContextMentionProviderOptions
|
|
): AgentContextMentionProvider<TestAgentTargetMentionItem> {
|
|
return {
|
|
id: AGENT_TARGET_PROVIDER_ID,
|
|
trigger: "@",
|
|
async query({ context, keyword, maxResults }) {
|
|
if (!options.queryAgentTargets) {
|
|
return [];
|
|
}
|
|
const result = await options.queryAgentTargets({
|
|
workspaceId: context?.metadata?.workspaceId,
|
|
query: keyword,
|
|
limit: maxResults
|
|
});
|
|
return result.targets ?? [];
|
|
},
|
|
getItemKey: (item) => item.targetId,
|
|
getItemLabel: (item) => item.name,
|
|
getItemSubtitle: (item) => item.description,
|
|
getItemIconUrl: (item) => item.iconUrl,
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.targetId,
|
|
label: item.name,
|
|
scope: { workspaceId: item.workspaceId },
|
|
presentation: {
|
|
agentProviderId: item.provider,
|
|
description: item.description,
|
|
iconUrl: item.iconUrl,
|
|
status: item.status,
|
|
subtitle: item.description
|
|
}
|
|
}
|
|
})
|
|
};
|
|
}
|
|
|
|
function createTestWorkspaceAppProvider(
|
|
options: TestContextMentionProviderOptions
|
|
): AgentContextMentionProvider<TestWorkspaceAppMentionItem> {
|
|
return {
|
|
id: WORKSPACE_APP_PROVIDER_ID,
|
|
trigger: "@",
|
|
async query({ context, keyword, maxResults }) {
|
|
if (!options.queryWorkspaceApps) {
|
|
return [];
|
|
}
|
|
const result = await options.queryWorkspaceApps({
|
|
workspaceId: context?.metadata?.workspaceId,
|
|
query: keyword,
|
|
limit: maxResults
|
|
});
|
|
return result.apps ?? [];
|
|
},
|
|
getItemKey: (item) => item.appId,
|
|
getItemLabel: (item) => item.name,
|
|
getItemSubtitle: (item) => item.description,
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.appId,
|
|
label: item.name,
|
|
scope: { workspaceId: item.workspaceId },
|
|
presentation: {
|
|
description: item.description
|
|
}
|
|
}
|
|
})
|
|
};
|
|
}
|
|
|
|
function createTestSessionProvider(
|
|
options: TestContextMentionProviderOptions
|
|
): AgentContextMentionProvider<any> {
|
|
return {
|
|
id: AGENT_SESSION_PROVIDER_ID,
|
|
trigger: "@",
|
|
async query({ context, keyword, maxResults }) {
|
|
if (!options.querySessions) {
|
|
return [];
|
|
}
|
|
const workspaceId = String(context?.metadata?.workspaceId ?? "");
|
|
const currentUserId = String(context?.metadata?.currentUserId ?? "");
|
|
const snapshot = await options.querySessions({
|
|
workspaceId,
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME"
|
|
});
|
|
const allSessions = snapshot.sessions ?? [];
|
|
const sessions =
|
|
maxResults === undefined
|
|
? allSessions
|
|
: allSessions.slice(0, maxResults);
|
|
const userIds = [
|
|
...new Set(
|
|
sessions
|
|
.map((session: any) => String(session.userId ?? "").trim())
|
|
.filter(Boolean)
|
|
)
|
|
];
|
|
const profilesResult =
|
|
userIds.length > 0 && options.loadUserProfiles
|
|
? await options.loadUserProfiles({ userIds })
|
|
: { users: [] };
|
|
const profiles = new Map(
|
|
(profilesResult.users ?? []).map((user: any) => [user.userId, user])
|
|
);
|
|
const items = await Promise.all(
|
|
sessions.map(async (session: any) => {
|
|
const summary = options.loadSessionSummary
|
|
? await Promise.resolve(
|
|
options.loadSessionSummary({
|
|
workspaceId,
|
|
agentSessionId: session.agentSessionId,
|
|
agentReplyLimit: 1,
|
|
recentTurnLimit: 1
|
|
})
|
|
).catch(() => null)
|
|
: null;
|
|
const userId = String(session.userId ?? "");
|
|
const profile = profiles.get(userId) as any;
|
|
const title =
|
|
compactSessionText(session.title) ||
|
|
compactSessionText(summary?.latestUserRequirement) ||
|
|
compactSessionText(summary?.initialUserRequirement) ||
|
|
(await testSessionFallbackTitle({
|
|
loadSessionMessages: options.loadSessionMessages,
|
|
session,
|
|
workspaceId
|
|
})) ||
|
|
session.agentSessionId;
|
|
return {
|
|
agentTargetId: session.agentTargetId,
|
|
agentName: testProviderLabel(session.provider),
|
|
id: session.agentSessionId,
|
|
initiatorAvatarUrl: profile?.avatar ?? "",
|
|
initiatorName: profile?.name || userId,
|
|
provider: session.provider,
|
|
scope:
|
|
userId && userId === currentUserId
|
|
? "my_sessions"
|
|
: "collab_sessions",
|
|
status: testSessionStatus(summary, session),
|
|
title,
|
|
inputPreview:
|
|
compactSessionText(summary?.latestUserRequirement) ||
|
|
compactSessionText(summary?.initialUserRequirement),
|
|
summaryPreview: compactSessionText(
|
|
summary?.recentAgentReplies?.[0]
|
|
),
|
|
updatedAtUnixMs:
|
|
session.updatedAtUnixMs ?? session.createdAtUnixMs ?? 0,
|
|
userId,
|
|
workspaceId
|
|
};
|
|
})
|
|
);
|
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
|
return normalizedKeyword
|
|
? items.filter((item) =>
|
|
[
|
|
item.agentName,
|
|
item.initiatorName,
|
|
item.provider,
|
|
item.title,
|
|
item.inputPreview,
|
|
item.summaryPreview
|
|
]
|
|
.join("\n")
|
|
.toLowerCase()
|
|
.includes(normalizedKeyword)
|
|
)
|
|
: items;
|
|
},
|
|
getItemKey: (item) => item.id,
|
|
getItemLabel: (item) => item.title,
|
|
getItemSubtitle: (item) => item.inputPreview || item.status,
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.id,
|
|
label: item.title,
|
|
scope: {
|
|
...(item.agentTargetId ? { agentTargetId: item.agentTargetId } : {}),
|
|
scope: item.scope ?? "",
|
|
userId: item.userId,
|
|
workspaceId: item.workspaceId
|
|
},
|
|
presentation: {
|
|
description: item.inputPreview || item.summaryPreview,
|
|
status: item.status,
|
|
subtitle: item.agentName
|
|
}
|
|
}
|
|
})
|
|
};
|
|
}
|
|
|
|
function compactSessionText(value: unknown): string {
|
|
return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
}
|
|
|
|
function testProviderLabel(provider: string): string {
|
|
if (provider === "codex") {
|
|
return "Codex";
|
|
}
|
|
if (provider === "nexight") {
|
|
return "Nexight";
|
|
}
|
|
if (provider === "claude-code") {
|
|
return "Claude Code";
|
|
}
|
|
return provider;
|
|
}
|
|
|
|
function testSessionStatus(summary: any, session: any): string {
|
|
const raw = String(
|
|
(typeof summary?.executionStatus === "string"
|
|
? summary.executionStatus
|
|
: summary?.executionStatus?.currentOrFinalStatus) ??
|
|
summary?.currentOrFinalStatus ??
|
|
session.lifecycleStatus ??
|
|
session.status ??
|
|
"idle"
|
|
).toLowerCase();
|
|
return raw === "running" ? "working" : raw;
|
|
}
|
|
|
|
async function testSessionFallbackTitle({
|
|
loadSessionMessages,
|
|
session,
|
|
workspaceId
|
|
}: {
|
|
loadSessionMessages?: (input: any) => Promise<any>;
|
|
session: any;
|
|
workspaceId: string;
|
|
}): Promise<string> {
|
|
if (!loadSessionMessages) {
|
|
return "";
|
|
}
|
|
const result = await loadSessionMessages({
|
|
workspaceId,
|
|
agentSessionId: session.agentSessionId,
|
|
...(session.sessionOrigin ? { sessionOrigin: session.sessionOrigin } : {}),
|
|
afterVersion: 0,
|
|
limit: 20
|
|
}).catch(() => null);
|
|
const messages = result?.messages ?? [];
|
|
const firstUserMessage = messages.find((message: any) => {
|
|
return message.role === "user" || message.kind === "user";
|
|
});
|
|
return compactSessionText(
|
|
firstUserMessage?.payload?.text ?? firstUserMessage?.body ?? ""
|
|
);
|
|
}
|
|
|
|
describe("AgentMentionSearchController", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
void setAgentGuiI18nTestLocale("en");
|
|
resetAgentMentionSearchBrowseCacheForTests();
|
|
});
|
|
|
|
it("localizes browse filter categories using the active locale at emit time", () => {
|
|
// Regression: browse category labels used to be frozen to the default ("en")
|
|
// runtime because they were computed once at module load. They must reflect
|
|
// the active agent GUI locale when a state is emitted.
|
|
setAgentGuiI18nTestLocale("zh-CN");
|
|
const controller = new AgentMentionSearchController({});
|
|
const states: { categories: readonly { id: string; label: string }[] }[] =
|
|
[];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
const categories = states.at(-1)?.categories ?? [];
|
|
const labelById = new Map(categories.map((c) => [c.id, c.label]));
|
|
|
|
expect(categories.map((category) => category.id)).toEqual([
|
|
"session",
|
|
"file",
|
|
"issue",
|
|
"agent",
|
|
"app"
|
|
]);
|
|
expect(labelById.get("app")).toBe("应用");
|
|
expect(labelById.get("session")).toBe("会话");
|
|
expect(labelById.get("issue")).toBe("任务");
|
|
expect(labelById.get("agent")).toBe("智能体");
|
|
});
|
|
|
|
it("omits host-disabled mention filters from browse categories", () => {
|
|
const controller = new AgentMentionSearchController({
|
|
hiddenFilterIds: ["issue", " file "]
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
expect(states.at(-1)?.categories.map((category) => category.id)).toEqual([
|
|
"session",
|
|
"agent",
|
|
"app"
|
|
]);
|
|
});
|
|
|
|
it("uses Tasks for the English issue browse category label", () => {
|
|
setAgentGuiI18nTestLocale("en");
|
|
const controller = new AgentMentionSearchController({});
|
|
const states: { categories: readonly { id: string; label: string }[] }[] =
|
|
[];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
const categories = states.at(-1)?.categories ?? [];
|
|
const labelById = new Map(categories.map((c) => [c.id, c.label]));
|
|
|
|
expect(labelById.get("issue")).toBe("Tasks");
|
|
});
|
|
|
|
it("prefetches the default session tab for blank queries", async () => {
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
});
|
|
const queryIssues = vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
});
|
|
const querySessions = vi
|
|
.fn()
|
|
.mockResolvedValue({ presences: [], sessions: [] });
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues,
|
|
querySessions,
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: " " });
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
mode: "browse",
|
|
query: ""
|
|
});
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "session"
|
|
})
|
|
);
|
|
expect(querySessions).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME"
|
|
});
|
|
expect(queryFiles).not.toHaveBeenCalled();
|
|
expect(queryIssues).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("groups session mentions by initiator instead of Agent owner", async () => {
|
|
const controller = new AgentMentionSearchController({
|
|
querySessions: vi.fn().mockResolvedValue({
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-local-claude",
|
|
agentTargetId: "claude-main",
|
|
provider: "claude-code",
|
|
title: "Local Claude build",
|
|
userId: "user-1"
|
|
},
|
|
...Array.from({ length: 31 }, (_, index) => ({
|
|
agentSessionId: `session-local-${index}`,
|
|
agentTargetId: "codex-main",
|
|
provider: "codex",
|
|
title: `Local build ${index}`,
|
|
userId: "user-1"
|
|
})),
|
|
{
|
|
agentSessionId: "session-shared",
|
|
agentTargetId: "shared-agent:shared-codex",
|
|
provider: "codex",
|
|
title: "Shared build initiated by me",
|
|
userId: "user-1"
|
|
},
|
|
{
|
|
agentSessionId: "session-shared-by-owner",
|
|
agentTargetId: "shared-agent:shared-codex",
|
|
provider: "codex",
|
|
title: "Shared build initiated by owner",
|
|
userId: "user-2"
|
|
},
|
|
{
|
|
agentSessionId: "session-uncatalogued",
|
|
agentTargetId: "legacy-gemini",
|
|
provider: "gemini",
|
|
title: "Legacy build",
|
|
userId: "user-3"
|
|
}
|
|
]
|
|
})
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
controller.setProvenanceCatalog({
|
|
enabledDimensions: ["agent"],
|
|
agentOptions: [
|
|
{
|
|
id: "codex-main",
|
|
label: "Me · Codex",
|
|
parentMemberId: "user-1"
|
|
},
|
|
{
|
|
id: "claude-main",
|
|
label: "Me · Claude Code",
|
|
parentMemberId: "user-1"
|
|
},
|
|
{
|
|
id: "shared-agent:shared-codex",
|
|
label: "Lin · Codex",
|
|
parentMemberId: "user-2"
|
|
}
|
|
],
|
|
memberOptions: [
|
|
{ id: "user-1", label: "Me" },
|
|
{
|
|
id: "user-2",
|
|
label: "Lin",
|
|
iconUrl: "https://cdn.example.com/lin.png"
|
|
}
|
|
]
|
|
});
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
filter: "session",
|
|
groups: [
|
|
{
|
|
id: "member:user-1",
|
|
label: "Me",
|
|
items: expect.arrayContaining([
|
|
expect.objectContaining({ targetId: "session-local-0" }),
|
|
expect.objectContaining({ targetId: "session-local-claude" })
|
|
]),
|
|
totalCount: 33,
|
|
hasMore: true
|
|
},
|
|
{
|
|
id: "member:user-2",
|
|
label: "Lin",
|
|
items: [
|
|
expect.objectContaining({
|
|
initiatorAvatarUrl: "https://cdn.example.com/lin.png",
|
|
initiatorName: "Lin",
|
|
initiatorUserId: "user-2",
|
|
targetId: "session-shared-by-owner"
|
|
})
|
|
]
|
|
},
|
|
{
|
|
id: "agent:uncatalogued%3Auser-3",
|
|
label: "user-3",
|
|
items: [
|
|
expect.objectContaining({ targetId: "session-uncatalogued" })
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
controller.expandGroup("member:user-1");
|
|
expect(
|
|
(
|
|
states.at(-1) as { groups: Array<{ id: string; items: unknown[] }> }
|
|
).groups.find((group) => group.id === "member:user-1")?.items
|
|
).toHaveLength(20);
|
|
controller.setProvenanceFilters({
|
|
session: {
|
|
agentTargetIds: ["shared-agent:shared-codex"],
|
|
memberIds: null
|
|
},
|
|
file: null,
|
|
issue: null,
|
|
agent: null,
|
|
app: null
|
|
});
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
groups: [
|
|
{
|
|
id: "member:user-1",
|
|
items: [expect.objectContaining({ targetId: "session-shared" })]
|
|
},
|
|
{
|
|
id: "member:user-2",
|
|
items: [
|
|
expect.objectContaining({ targetId: "session-shared-by-owner" })
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
controller.dispose();
|
|
});
|
|
|
|
it("loads the app provider when switching to the app tab for blank queries", async () => {
|
|
const queryWorkspaceApps = vi.fn().mockResolvedValue({
|
|
apps: [
|
|
{
|
|
appId: "vibe-design",
|
|
description: "Design prototypes in Tutti.",
|
|
name: "Vibe Design",
|
|
workspaceId: "room-1"
|
|
}
|
|
]
|
|
});
|
|
const queryAgentTargets = vi.fn().mockResolvedValue({
|
|
targets: [
|
|
{
|
|
name: "Codex",
|
|
provider: "codex",
|
|
targetId: "local:codex",
|
|
workspaceId: "room-1"
|
|
}
|
|
]
|
|
});
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
});
|
|
const queryIssues = vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
});
|
|
const querySessions = vi
|
|
.fn()
|
|
.mockResolvedValue({ presences: [], sessions: [] });
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues,
|
|
querySessions,
|
|
queryAgentTargets,
|
|
queryWorkspaceApps,
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "session"
|
|
})
|
|
);
|
|
controller.setFilter("app");
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
mode: "browse",
|
|
filter: "app"
|
|
});
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "app",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "apps",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "workspace-app",
|
|
appId: "vibe-design"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
})
|
|
);
|
|
expect(queryWorkspaceApps).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
query: "",
|
|
limit: undefined
|
|
});
|
|
expect(queryFiles).not.toHaveBeenCalled();
|
|
expect(queryIssues).not.toHaveBeenCalled();
|
|
expect(queryAgentTargets).not.toHaveBeenCalled();
|
|
expect(querySessions).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("keeps provenance filters independent across mention tabs", async () => {
|
|
const querySessions = vi.fn().mockResolvedValue([]);
|
|
const queryFiles = vi.fn().mockResolvedValue([]);
|
|
const sessionProvider: AgentContextMentionProvider<{ id: string }> = {
|
|
id: AGENT_SESSION_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: querySessions,
|
|
getItemKey: (item) => item.id,
|
|
getItemLabel: (item) => item.id,
|
|
toInsertResult: (item) => ({
|
|
kind: "markdown-link",
|
|
href: `mention://agent-session/${item.id}`,
|
|
label: item.id
|
|
})
|
|
};
|
|
const fileProvider: AgentContextMentionProvider<{
|
|
href: string;
|
|
label: string;
|
|
}> = {
|
|
id: FILE_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: queryFiles,
|
|
getItemKey: (item) => item.href,
|
|
getItemLabel: (item) => item.label,
|
|
toInsertResult: (item) => ({
|
|
kind: "markdown-link",
|
|
href: item.href,
|
|
label: item.label
|
|
})
|
|
};
|
|
const controller = new AgentMentionSearchController({
|
|
contextMentionProviders: [sessionProvider, fileProvider],
|
|
debounceMs: 0
|
|
});
|
|
controller.setProvenanceFilters({
|
|
session: {
|
|
agentTargetIds: null,
|
|
memberIds: ["zhou-man"]
|
|
},
|
|
file: {
|
|
agentTargetIds: null,
|
|
memberIds: null
|
|
},
|
|
issue: null,
|
|
agent: null,
|
|
app: null
|
|
});
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: "report"
|
|
});
|
|
|
|
await waitForFast(() => expect(querySessions).toHaveBeenCalledTimes(1));
|
|
expect(querySessions).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
context: {
|
|
metadata: expect.objectContaining({
|
|
referenceProvenanceFilter: {
|
|
agentTargetIds: null,
|
|
memberIds: ["zhou-man"]
|
|
}
|
|
})
|
|
}
|
|
})
|
|
);
|
|
|
|
controller.setFilter("file");
|
|
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(1));
|
|
expect(queryFiles).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
context: {
|
|
metadata: expect.objectContaining({
|
|
referenceProvenanceFilter: {
|
|
agentTargetIds: null,
|
|
memberIds: null
|
|
}
|
|
})
|
|
}
|
|
})
|
|
);
|
|
|
|
controller.setFilter("session");
|
|
|
|
await waitForFast(() => expect(querySessions).toHaveBeenCalledTimes(2));
|
|
expect(querySessions).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
context: {
|
|
metadata: expect.objectContaining({
|
|
referenceProvenanceFilter: {
|
|
agentTargetIds: null,
|
|
memberIds: ["zhou-man"]
|
|
}
|
|
})
|
|
}
|
|
})
|
|
);
|
|
controller.dispose();
|
|
});
|
|
|
|
it("loads agent targets from the agent tab and inserts agent-target mentions", async () => {
|
|
const queryAgentTargets = vi.fn().mockResolvedValue({
|
|
targets: [
|
|
{
|
|
description: "Run Codex locally",
|
|
iconUrl: "tutti://agent/codex.svg",
|
|
name: "Codex",
|
|
provider: "codex",
|
|
targetId: "local:codex",
|
|
workspaceId: "room-1"
|
|
},
|
|
{
|
|
description: "Run Claude Code locally",
|
|
iconUrl: "tutti://agent/claude-code.svg",
|
|
name: "Claude Code",
|
|
provider: "claude-code",
|
|
targetId: "local:claude-code",
|
|
workspaceId: "room-1"
|
|
},
|
|
{
|
|
description: "Run a shared Codex agent",
|
|
iconUrl: "tutti://agent/codex.svg",
|
|
name: "Lin's Codex",
|
|
provider: "codex",
|
|
status: "unavailable",
|
|
targetId: "shared-agent:shared-codex",
|
|
workspaceId: "room-1"
|
|
},
|
|
{
|
|
description: "Run an uncatalogued agent",
|
|
name: "Legacy Gemini",
|
|
provider: "gemini",
|
|
targetId: "legacy:gemini",
|
|
workspaceId: "room-1"
|
|
}
|
|
]
|
|
});
|
|
const queryWorkspaceApps = vi.fn().mockResolvedValue({
|
|
apps: [
|
|
{
|
|
appId: "vibe-design",
|
|
name: "Vibe Design",
|
|
workspaceId: "room-1"
|
|
}
|
|
]
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryAgentTargets,
|
|
queryWorkspaceApps,
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
controller.setProvenanceCatalog({
|
|
enabledDimensions: ["agent"],
|
|
agentOptions: [
|
|
{
|
|
id: "shared-agent:shared-codex",
|
|
label: "Lin · Codex",
|
|
parentMemberId: "user-lin"
|
|
},
|
|
{
|
|
id: "local:codex",
|
|
label: "Me · Codex",
|
|
parentMemberId: "user-current"
|
|
},
|
|
{
|
|
id: "local:claude-code",
|
|
label: "Me · Claude Code",
|
|
parentMemberId: "user-current"
|
|
}
|
|
],
|
|
memberOptions: [
|
|
{ id: "user-current", label: "Me" },
|
|
{ id: "user-lin", label: "Lin" }
|
|
]
|
|
});
|
|
|
|
controller.setFilter("agent");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "agent",
|
|
groups: [
|
|
{
|
|
id: "member:user-current",
|
|
label: "Me",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "agent-target",
|
|
targetId: "local:codex",
|
|
agentProviderId: "codex"
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "agent-target",
|
|
targetId: "local:claude-code"
|
|
})
|
|
]
|
|
},
|
|
{
|
|
id: "member:user-lin",
|
|
label: "Lin",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "agent-target",
|
|
targetId: "shared-agent:shared-codex",
|
|
href: "mention://agent-target/shared-agent:shared-codex?workspaceId=room-1",
|
|
agentProviderId: "codex",
|
|
availabilityStatus: "unavailable"
|
|
})
|
|
]
|
|
},
|
|
{
|
|
id: "agent:uncatalogued%3Alegacy%3Agemini",
|
|
label: "Legacy Gemini",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "agent-target",
|
|
targetId: "legacy:gemini"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(queryAgentTargets).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
query: "",
|
|
limit: undefined
|
|
});
|
|
expect(queryWorkspaceApps).not.toHaveBeenCalled();
|
|
controller.setProvenanceFilters({
|
|
session: null,
|
|
file: null,
|
|
issue: null,
|
|
agent: {
|
|
agentTargetIds: ["shared-agent:shared-codex"],
|
|
memberIds: null
|
|
},
|
|
app: null
|
|
});
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
groups: [
|
|
{
|
|
id: "member:user-lin",
|
|
items: [
|
|
expect.objectContaining({
|
|
targetId: "shared-agent:shared-codex"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
});
|
|
|
|
it("renders all workspace apps without mention pagination", async () => {
|
|
const apps = Array.from({ length: 12 }, (_, index) => ({
|
|
appId: `app-${index + 1}`,
|
|
name: `App ${index + 1}`,
|
|
description: `Workspace app ${index + 1}`,
|
|
workspaceId: "room-1"
|
|
}));
|
|
const queryWorkspaceApps = vi.fn().mockResolvedValue({ apps });
|
|
const controller = new AgentMentionSearchController({
|
|
queryWorkspaceApps,
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("app");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "app",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "apps",
|
|
totalCount: 12,
|
|
visibleCount: 12,
|
|
hasMore: false
|
|
})
|
|
]
|
|
})
|
|
);
|
|
expect(states.at(-1).groups[0].items).toHaveLength(12);
|
|
expect(queryWorkspaceApps).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
query: "",
|
|
limit: undefined
|
|
});
|
|
});
|
|
|
|
it("shows fresh cached browse results immediately and revalidates when the mention palette reopens", async () => {
|
|
let now = 1_000;
|
|
let entries = [
|
|
{
|
|
path: "/workspace/README.md",
|
|
name: "README.md",
|
|
kind: "file"
|
|
}
|
|
];
|
|
const queryFiles = vi.fn(async () => ({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries
|
|
}));
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
diagnosticNow: () => now
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("file");
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/README.md"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
})
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledTimes(1);
|
|
|
|
controller.close();
|
|
now += 1_000;
|
|
entries = [
|
|
{
|
|
path: "/workspace/package.json",
|
|
name: "package.json",
|
|
kind: "file"
|
|
}
|
|
];
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("file");
|
|
|
|
// Cache is presentation-only: it paints synchronously on reopen.
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/README.md"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
});
|
|
await waitForFast(() => {
|
|
expect(queryFiles).toHaveBeenCalledTimes(2);
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/package.json"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
});
|
|
});
|
|
});
|
|
|
|
it("revalidates cached Agent availability when the Agent tab reopens", async () => {
|
|
let status = "available";
|
|
const queryAgentTargets = vi.fn(async () => ({
|
|
targets: [
|
|
{
|
|
name: "Claude Code",
|
|
provider: "claude-code",
|
|
status,
|
|
targetId: "local:claude-code",
|
|
workspaceId: "room-1"
|
|
}
|
|
]
|
|
}));
|
|
const controller = new AgentMentionSearchController({ queryAgentTargets });
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
const latestClaudeAvailability = () =>
|
|
states
|
|
.at(-1)
|
|
?.groups.flatMap((group: { items: any[] }) => group.items)
|
|
.find(
|
|
(item: { targetId?: string }) => item.targetId === "local:claude-code"
|
|
)?.availabilityStatus;
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("agent");
|
|
await waitForFast(() => {
|
|
expect(queryAgentTargets).toHaveBeenCalledTimes(1);
|
|
expect(latestClaudeAvailability()).toBe("available");
|
|
});
|
|
|
|
controller.close();
|
|
status = "unavailable";
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("agent");
|
|
|
|
expect(latestClaudeAvailability()).toBe("available");
|
|
await waitForFast(() => {
|
|
expect(queryAgentTargets).toHaveBeenCalledTimes(2);
|
|
expect(latestClaudeAvailability()).toBe("unavailable");
|
|
});
|
|
controller.dispose();
|
|
});
|
|
|
|
it("shows fresh cached browse results and revalidates after the controller is recreated", async () => {
|
|
let now = 2_000;
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/package.json",
|
|
name: "package.json",
|
|
kind: "file"
|
|
}
|
|
]
|
|
});
|
|
const providerOptions: TestContextMentionProviderOptions = {
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
diagnosticNow: () => now
|
|
};
|
|
const options: TestContextMentionProviderOptions = {
|
|
...providerOptions,
|
|
contextMentionProviders:
|
|
createTestContextMentionProviders(providerOptions)
|
|
};
|
|
const firstController = new AgentMentionSearchController(options);
|
|
const firstStates: any[] = [];
|
|
firstController.subscribe((state) => firstStates.push(state));
|
|
|
|
firstController.setFilter("file");
|
|
firstController.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() =>
|
|
expect(firstStates.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/package.json"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
})
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledTimes(1);
|
|
|
|
firstController.dispose();
|
|
now += 1_000;
|
|
|
|
const secondController = new AgentMentionSearchController(options);
|
|
const secondStates: any[] = [];
|
|
secondController.subscribe((state) => secondStates.push(state));
|
|
secondController.setFilter("file");
|
|
secondController.updateQuery({ workspaceId: "room-1", query: "" });
|
|
|
|
expect(secondStates.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/package.json"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
});
|
|
expect(queryFiles).toHaveBeenCalledTimes(1);
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(2));
|
|
});
|
|
|
|
it("evicts the oldest browse cache entry once the shared cap is exceeded", async () => {
|
|
const now = 5_000;
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room",
|
|
root: "/workspace",
|
|
entries: [{ path: "/workspace/a.md", name: "a.md", kind: "file" }]
|
|
});
|
|
const providerOptions: TestContextMentionProviderOptions = {
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
diagnosticNow: () => now
|
|
};
|
|
const controller = new AgentMentionSearchController({
|
|
...providerOptions,
|
|
contextMentionProviders:
|
|
createTestContextMentionProviders(providerOptions)
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
controller.setFilter("file");
|
|
|
|
// Warm one more distinct workspace than the cache can hold. Each distinct
|
|
// workspace is a cache miss, so this drives MAX + 1 fetches and evicts the
|
|
// oldest (room-0) on the final insert.
|
|
const warmCount = MAX_BROWSE_CACHE_ENTRIES + 1;
|
|
for (let index = 0; index < warmCount; index += 1) {
|
|
controller.updateQuery({ workspaceId: `room-${index}`, query: "" });
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await waitForFast(() =>
|
|
expect(queryFiles).toHaveBeenCalledTimes(index + 1)
|
|
);
|
|
}
|
|
|
|
// The most recently warmed workspace paints from cache synchronously, then
|
|
// still revalidates because this is a user-opened browse.
|
|
controller.updateQuery({
|
|
workspaceId: `room-${warmCount - 1}`,
|
|
query: ""
|
|
});
|
|
expect(states.at(-1)).toMatchObject({ status: "ready", mode: "browse" });
|
|
await waitForFast(() =>
|
|
expect(queryFiles).toHaveBeenCalledTimes(warmCount + 1)
|
|
);
|
|
|
|
// The oldest workspace was evicted by the cap, so it has no synchronous
|
|
// cached presentation before its required realtime fetch.
|
|
controller.updateQuery({ workspaceId: "room-0", query: "" });
|
|
expect(states.at(-1)).toMatchObject({ status: "loading", mode: "browse" });
|
|
await waitForFast(() =>
|
|
expect(queryFiles).toHaveBeenCalledTimes(warmCount + 2)
|
|
);
|
|
|
|
controller.dispose();
|
|
});
|
|
|
|
it("preloadAgentMentionBrowse warms the shared cache for a later controller", async () => {
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/preloaded.md",
|
|
name: "preloaded.md",
|
|
kind: "file"
|
|
}
|
|
]
|
|
});
|
|
const providerOptions: TestContextMentionProviderOptions = {
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
};
|
|
|
|
// Warm the shared cache without a mounted controller (startup-style).
|
|
preloadAgentMentionBrowse({
|
|
workspaceId: "room-1",
|
|
filter: "file",
|
|
contextMentionProviders:
|
|
createTestContextMentionProviders(providerOptions)
|
|
});
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(1));
|
|
|
|
// A later controller built with the same providers paints the warmed cache.
|
|
const controller = new AgentMentionSearchController({
|
|
contextMentionProviders:
|
|
createTestContextMentionProviders(providerOptions)
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/preloaded.md"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
});
|
|
expect(queryFiles).toHaveBeenCalledTimes(1);
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(2));
|
|
controller.dispose();
|
|
});
|
|
|
|
it("isolates generated-file browse cache entries by persisted section key", async () => {
|
|
const queryAgentGeneratedFiles = vi.fn().mockResolvedValue({ entries: [] });
|
|
const controller = new AgentMentionSearchController({
|
|
queryAgentGeneratedFiles,
|
|
queryFiles: vi.fn().mockResolvedValue({ entries: [] })
|
|
});
|
|
controller.setFilter("file");
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
query: "",
|
|
sectionKey: "project:/workspace/one"
|
|
});
|
|
await waitForFast(() =>
|
|
expect(queryAgentGeneratedFiles).toHaveBeenCalledTimes(1)
|
|
);
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
query: "",
|
|
sectionKey: "project:/workspace/two"
|
|
});
|
|
await waitForFast(() =>
|
|
expect(queryAgentGeneratedFiles).toHaveBeenCalledTimes(2)
|
|
);
|
|
expect(queryAgentGeneratedFiles).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({ sectionKey: "project:/workspace/one" })
|
|
);
|
|
expect(queryAgentGeneratedFiles).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({ sectionKey: "project:/workspace/two" })
|
|
);
|
|
controller.dispose();
|
|
});
|
|
|
|
it("uses preloaded browse results when the mention palette first opens", async () => {
|
|
let now = 3_000;
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/preloaded.md",
|
|
name: "preloaded.md",
|
|
kind: "file"
|
|
}
|
|
]
|
|
});
|
|
const providerOptions: TestContextMentionProviderOptions = {
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
diagnosticNow: () => now
|
|
};
|
|
const controller = new AgentMentionSearchController({
|
|
...providerOptions,
|
|
contextMentionProviders:
|
|
createTestContextMentionProviders(providerOptions)
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.preloadBrowse({ workspaceId: "room-1", filter: "file" });
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(1));
|
|
|
|
now += 1_000;
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/preloaded.md"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
});
|
|
expect(queryFiles).toHaveBeenCalledTimes(1);
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(2));
|
|
});
|
|
|
|
it("reuses preloaded app results when the app category first opens", async () => {
|
|
let now = 4_000;
|
|
const queryWorkspaceApps = vi.fn().mockResolvedValue({
|
|
apps: [
|
|
{
|
|
appId: "app-1",
|
|
name: "Task Manager",
|
|
description: "Manage workspace issues",
|
|
workspaceId: "room-1"
|
|
}
|
|
]
|
|
});
|
|
const providerOptions: TestContextMentionProviderOptions = {
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
queryWorkspaceApps,
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
diagnosticNow: () => now
|
|
};
|
|
const controller = new AgentMentionSearchController({
|
|
...providerOptions,
|
|
contextMentionProviders:
|
|
createTestContextMentionProviders(providerOptions)
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.preloadBrowse({ workspaceId: "room-1", filter: "app" });
|
|
await waitForFast(() =>
|
|
expect(queryWorkspaceApps).toHaveBeenCalledTimes(1)
|
|
);
|
|
|
|
now += 1_000;
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.enterCategory("app");
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "app",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "apps",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "workspace-app",
|
|
appId: "app-1",
|
|
name: "Task Manager"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
});
|
|
expect(queryWorkspaceApps).toHaveBeenCalledTimes(1);
|
|
await waitForFast(() =>
|
|
expect(queryWorkspaceApps).toHaveBeenCalledTimes(2)
|
|
);
|
|
});
|
|
|
|
it("cancels an orphaned browse load and refetches after reopen", async () => {
|
|
const resolveFiles: Array<
|
|
(value: {
|
|
workspaceId: string;
|
|
root: string;
|
|
entries: { path: string; name: string; kind: string }[];
|
|
}) => void
|
|
> = [];
|
|
const queryFiles = vi.fn(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveFiles.push(resolve);
|
|
})
|
|
);
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: any[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(1));
|
|
controller.close();
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("file");
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(2));
|
|
|
|
resolveFiles[1]?.({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/App.tsx",
|
|
name: "App.tsx",
|
|
kind: "file"
|
|
}
|
|
]
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/src/App.tsx"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
})
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("cancels an orphaned browse load and refetches after controller recreation", async () => {
|
|
const resolveFiles: Array<
|
|
(value: {
|
|
workspaceId: string;
|
|
root: string;
|
|
entries: { path: string; name: string; kind: string }[];
|
|
}) => void
|
|
> = [];
|
|
const queryFiles = vi.fn(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveFiles.push(resolve);
|
|
})
|
|
);
|
|
const providerOptions: TestContextMentionProviderOptions = {
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
};
|
|
const options: TestContextMentionProviderOptions = {
|
|
...providerOptions,
|
|
contextMentionProviders:
|
|
createTestContextMentionProviders(providerOptions)
|
|
};
|
|
const firstController = new AgentMentionSearchController(options);
|
|
firstController.setFilter("file");
|
|
firstController.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(1));
|
|
firstController.dispose();
|
|
|
|
const secondController = new AgentMentionSearchController(options);
|
|
const secondStates: any[] = [];
|
|
secondController.subscribe((state) => secondStates.push(state));
|
|
secondController.setFilter("file");
|
|
secondController.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(2));
|
|
|
|
resolveFiles[1]?.({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/pnpm-lock.yaml",
|
|
name: "pnpm-lock.yaml",
|
|
kind: "file"
|
|
}
|
|
]
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(secondStates.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/pnpm-lock.yaml"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
})
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("debounces grouped searches and returns file results", async () => {
|
|
vi.useFakeTimers();
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/App.tsx",
|
|
name: "App.tsx",
|
|
kind: "file",
|
|
directoryPath: "/workspace/src",
|
|
score: 10
|
|
}
|
|
]
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
debounceMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({
|
|
workspaceId: " room-1 ",
|
|
currentUserId: "user-1",
|
|
query: " app "
|
|
});
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
query: "app",
|
|
mode: "results"
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
await waitForFast(() => expect(queryFiles).toHaveBeenCalledTimes(1));
|
|
|
|
expect(queryFiles).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
query: "app",
|
|
limit: 30,
|
|
includeKinds: ["file", "directory"]
|
|
});
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
query: "app",
|
|
mode: "results",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/src/App.tsx",
|
|
name: "App.tsx"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
});
|
|
});
|
|
|
|
it("logs completed mention search diagnostics without the raw query", async () => {
|
|
vi.useFakeTimers();
|
|
const diagnosticLogs: any[] = [];
|
|
const rawQuery = "secret-file";
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/App.tsx",
|
|
name: "App.tsx",
|
|
kind: "file",
|
|
directoryPath: "/workspace/src",
|
|
score: 10
|
|
}
|
|
]
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
debounceMs: 20,
|
|
diagnosticInfoLogger: (payload) => diagnosticLogs.push(payload),
|
|
diagnosticSlowThresholdMs: 0
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: rawQuery
|
|
});
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
query: rawQuery,
|
|
mode: "results"
|
|
})
|
|
);
|
|
expect(diagnosticLogs).toEqual([
|
|
expect.objectContaining({
|
|
debounceMs: 20,
|
|
event: "agent_gui.mention_search",
|
|
mode: "results",
|
|
queryLength: rawQuery.length,
|
|
status: "ready",
|
|
workspaceId: "room-1",
|
|
providerResults: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
providerId: FILE_PROVIDER_ID,
|
|
resultCount: 1,
|
|
status: "success"
|
|
})
|
|
])
|
|
})
|
|
]);
|
|
expect(JSON.stringify(diagnosticLogs)).not.toContain(rawQuery);
|
|
});
|
|
|
|
it("keeps mention search ready when diagnostic logging fails", async () => {
|
|
vi.useFakeTimers();
|
|
const diagnosticInfoLogger = vi.fn(() => {
|
|
throw new Error("diagnostic sink failed");
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/App.tsx",
|
|
name: "App.tsx",
|
|
kind: "file",
|
|
directoryPath: "/workspace/src",
|
|
score: 10
|
|
}
|
|
]
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
debounceMs: 20,
|
|
diagnosticInfoLogger,
|
|
diagnosticSlowThresholdMs: 0
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: "app"
|
|
});
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
query: "app",
|
|
mode: "results"
|
|
})
|
|
);
|
|
expect(diagnosticInfoLogger).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("times out the selected stalled result provider", async () => {
|
|
vi.useFakeTimers();
|
|
const diagnosticLogs: any[] = [];
|
|
const rawQuery = "secret-token";
|
|
const queryIssues = vi.fn(
|
|
() =>
|
|
new Promise(() => {
|
|
// Simulates a host provider that neither resolves nor rejects.
|
|
})
|
|
);
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/App.tsx",
|
|
name: "App.tsx",
|
|
kind: "file",
|
|
directoryPath: "/workspace/src",
|
|
score: 10
|
|
}
|
|
]
|
|
}),
|
|
queryIssues,
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
debounceMs: 20,
|
|
diagnosticInfoLogger: (payload) => diagnosticLogs.push(payload),
|
|
providerTimeoutMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("issue");
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: rawQuery
|
|
});
|
|
await vi.advanceTimersByTimeAsync(40);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
query: rawQuery,
|
|
mode: "results",
|
|
filter: "issue",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "issues",
|
|
items: []
|
|
})
|
|
]
|
|
})
|
|
);
|
|
expect(queryIssues).toHaveBeenCalledTimes(1);
|
|
expect(diagnosticLogs).toEqual([
|
|
expect.objectContaining({
|
|
event: "agent_gui.mention_search",
|
|
mode: "results",
|
|
providerTimeoutMs: 20,
|
|
queryLength: rawQuery.length,
|
|
status: "ready",
|
|
providerResults: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
providerId: WORKSPACE_ISSUE_PROVIDER_ID,
|
|
resultCount: 0,
|
|
status: "timeout"
|
|
})
|
|
])
|
|
})
|
|
]);
|
|
expect(JSON.stringify(diagnosticLogs)).not.toContain(rawQuery);
|
|
});
|
|
|
|
it("times out stalled browse providers and keeps partial results", async () => {
|
|
vi.useFakeTimers();
|
|
const queryIssues = vi.fn(
|
|
() =>
|
|
new Promise(() => {
|
|
// Simulates a host provider that neither resolves nor rejects.
|
|
})
|
|
);
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/App.tsx",
|
|
name: "App.tsx",
|
|
kind: "file",
|
|
directoryPath: "/workspace/src",
|
|
score: 10
|
|
}
|
|
]
|
|
}),
|
|
queryAgentGeneratedFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues,
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
providerTimeoutMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
mode: "browse",
|
|
query: ""
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
query: "",
|
|
mode: "browse",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/src/App.tsx"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
})
|
|
);
|
|
expect(queryIssues).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses rich text @ providers for workspace files, issues, and sessions when available", async () => {
|
|
vi.useFakeTimers();
|
|
const queryFiles = vi.fn();
|
|
const queryIssues = vi.fn();
|
|
const querySessions = vi.fn();
|
|
const fileProviderQuery = vi.fn().mockResolvedValue([
|
|
{
|
|
label: "App.tsx",
|
|
href: "/Users/test/project/tutti/src/App.tsx"
|
|
}
|
|
]);
|
|
const issueProviderQuery = vi.fn().mockResolvedValue([
|
|
{
|
|
issueId: "issue-1",
|
|
title: "Fix mention search",
|
|
status: "running"
|
|
}
|
|
]);
|
|
const sessionProviderQuery = vi.fn().mockResolvedValue([
|
|
{
|
|
agentName: "Codex",
|
|
id: "session-1",
|
|
initiatorName: "local",
|
|
provider: "codex",
|
|
status: "working",
|
|
title: "Fix mention session provider",
|
|
updatedAtUnixMs: 30,
|
|
userId: "local",
|
|
workspaceId: "room-1"
|
|
}
|
|
]);
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues,
|
|
querySessions,
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
contextMentionProviders: [
|
|
{
|
|
id: FILE_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: fileProviderQuery,
|
|
getItemKey: (item) => item.href,
|
|
getItemLabel: (item) => item.label,
|
|
toInsertResult: (item) => ({
|
|
kind: "markdown-link",
|
|
label: item.label,
|
|
href: item.href
|
|
})
|
|
},
|
|
{
|
|
id: WORKSPACE_ISSUE_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: issueProviderQuery,
|
|
getItemKey: (item) => item.issueId,
|
|
getItemLabel: (item) => item.title,
|
|
getItemSubtitle: (item) => item.status,
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.issueId,
|
|
label: item.title,
|
|
scope: { workspaceId: "room-1" },
|
|
presentation: { status: item.status }
|
|
}
|
|
})
|
|
},
|
|
{
|
|
id: AGENT_SESSION_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: sessionProviderQuery,
|
|
getItemKey: (item) => item.id,
|
|
getItemLabel: (item) => item.title,
|
|
getItemSubtitle: (item) => `${item.provider} · ${item.status}`,
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.id,
|
|
label: item.title,
|
|
scope: {
|
|
userId: item.userId,
|
|
workspaceId: item.workspaceId
|
|
},
|
|
presentation: {
|
|
agentIconUrl: "data:image/svg+xml;base64,gemini",
|
|
status: item.status,
|
|
subtitle: item.agentName
|
|
}
|
|
}
|
|
})
|
|
}
|
|
] satisfies [
|
|
AgentContextMentionProvider<TestFileMentionItem>,
|
|
AgentContextMentionProvider<TestIssueMentionItem>,
|
|
AgentContextMentionProvider<TestSessionMentionItem>
|
|
],
|
|
debounceMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "local",
|
|
query: "mention"
|
|
});
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "session",
|
|
agentName: "Codex",
|
|
agentIconUrl: "data:image/svg+xml;base64,gemini",
|
|
initiatorName: "",
|
|
name: "Fix mention session provider",
|
|
scope: "my_sessions",
|
|
targetId: "session-1"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
})
|
|
);
|
|
expect(queryFiles).not.toHaveBeenCalled();
|
|
expect(queryIssues).not.toHaveBeenCalled();
|
|
expect(querySessions).not.toHaveBeenCalled();
|
|
expect(fileProviderQuery).not.toHaveBeenCalled();
|
|
expect(issueProviderQuery).not.toHaveBeenCalled();
|
|
expect(sessionProviderQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
keyword: "mention",
|
|
maxResults: 30
|
|
})
|
|
);
|
|
});
|
|
|
|
it("builds grouped issue results with parsed previews", async () => {
|
|
vi.useFakeTimers();
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [
|
|
{
|
|
issueId: "issue-1",
|
|
workspaceId: "room-1",
|
|
title: "修复 room status",
|
|
content: JSON.stringify({
|
|
type: "doc",
|
|
content: [
|
|
{
|
|
type: "paragraph",
|
|
content: [
|
|
{ type: "text", text: "补齐 statusBatch 的错误处理" }
|
|
]
|
|
}
|
|
]
|
|
}),
|
|
status: "running",
|
|
creatorUserId: "user-2",
|
|
creatorDisplayName: "Alice",
|
|
issueCount: 0,
|
|
notStartedCount: 0,
|
|
runningCount: 0,
|
|
pendingAcceptanceCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
canceledCount: 0,
|
|
updatedAtUnix: 20
|
|
}
|
|
],
|
|
totalCount: 1,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
...{
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: []
|
|
},
|
|
agentSessionId: "session-1",
|
|
workspaceId: "room-1",
|
|
userId: "user-1",
|
|
provider: "codex",
|
|
title: "看看项目有什么文件",
|
|
effectiveStatus: "working",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 10
|
|
},
|
|
{
|
|
agentSessionId: "session-2",
|
|
workspaceId: "room-1",
|
|
userId: "user-2",
|
|
provider: "nexight",
|
|
title: "room status 接口整理",
|
|
lifecycleStatus: "completed",
|
|
createdAtUnixMs: 2,
|
|
updatedAtUnixMs: 9
|
|
}
|
|
]
|
|
}),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn().mockImplementation(({ agentSessionId }) =>
|
|
Promise.resolve({
|
|
workspaceId: "room-1",
|
|
agentSessionId,
|
|
executionStatus:
|
|
agentSessionId === "session-1" ? "RUNNING" : "COMPLETED",
|
|
latestUserRequirement:
|
|
agentSessionId === "session-1"
|
|
? "看看项目有什么文件"
|
|
: "整理 room status",
|
|
recentAgentReplies:
|
|
agentSessionId === "session-1"
|
|
? ["已读取 workspace 结构"]
|
|
: ["输出了 statusBatch 调用链"],
|
|
initialUserRequirement: "",
|
|
initialTurn: null,
|
|
latestTurn: null,
|
|
recentTurns: []
|
|
})
|
|
),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({
|
|
users: [
|
|
{
|
|
userId: "user-1",
|
|
name: "Wang",
|
|
avatar: "https://cdn.example.com/wang.png"
|
|
},
|
|
{
|
|
userId: "user-2",
|
|
name: "Alice",
|
|
avatar: "https://cdn.example.com/alice.png"
|
|
}
|
|
]
|
|
}),
|
|
debounceMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("issue");
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: "status"
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
filter: "issue",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "issues",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "workspace-issue",
|
|
targetId: "issue-1",
|
|
contentPreview: "补齐 statusBatch 的错误处理"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
})
|
|
);
|
|
});
|
|
|
|
it("resets the active filter when the picker closes", () => {
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn(),
|
|
querySessions: vi.fn(),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn()
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("issue");
|
|
controller.close();
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "idle",
|
|
mode: "browse",
|
|
filter: "session"
|
|
});
|
|
});
|
|
|
|
it("loads dock-backed file items when switching to the file tab without a keyword", async () => {
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/README.md",
|
|
name: "README.md",
|
|
kind: "file"
|
|
}
|
|
]
|
|
});
|
|
const queryAgentGeneratedFiles = vi.fn().mockResolvedValue({
|
|
entries: [
|
|
{
|
|
path: "/workspace/output/report.md",
|
|
name: "report.md"
|
|
}
|
|
]
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryAgentGeneratedFiles,
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("file");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
query: "",
|
|
filter: "file",
|
|
groups: [
|
|
{
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "README.md",
|
|
path: "/workspace/README.md"
|
|
})
|
|
]
|
|
},
|
|
{
|
|
id: "agent_generated_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "report.md",
|
|
path: "/workspace/output/report.md"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
workspaceId: "room-1",
|
|
query: "",
|
|
limit: 30
|
|
})
|
|
);
|
|
expect(queryAgentGeneratedFiles).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
workspaceId: "room-1",
|
|
query: "",
|
|
limit: 30
|
|
})
|
|
);
|
|
});
|
|
|
|
it("searches agent-generated files for typed file queries under an active provenance filter", async () => {
|
|
const queryFiles = vi.fn().mockResolvedValue({ entries: [] });
|
|
const queryAgentGeneratedFiles = vi.fn().mockResolvedValue({
|
|
entries: [
|
|
{
|
|
path: "/workspace/output/report.md",
|
|
name: "report.md"
|
|
}
|
|
]
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
debounceMs: 0,
|
|
queryAgentGeneratedFiles,
|
|
queryFiles
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
controller.setFilter("file");
|
|
controller.setProvenanceFilters({
|
|
session: null,
|
|
file: {
|
|
agentTargetIds: ["agent-codex"],
|
|
memberIds: null
|
|
},
|
|
issue: null,
|
|
agent: null,
|
|
app: null
|
|
});
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: "report"
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "results",
|
|
filter: "file",
|
|
groups: [
|
|
{
|
|
id: "agent_generated_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "/workspace/output/report.md"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(queryFiles).not.toHaveBeenCalled();
|
|
expect(queryAgentGeneratedFiles).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
query: "report",
|
|
provenanceFilter: {
|
|
agentTargetIds: ["agent-codex"],
|
|
memberIds: null
|
|
}
|
|
})
|
|
);
|
|
controller.dispose();
|
|
});
|
|
|
|
it("groups agent-generated files by folder and supports folder drill-down", async () => {
|
|
setAgentGuiI18nTestLocale("zh-CN");
|
|
const queryAgentGeneratedFiles = vi.fn().mockResolvedValue({
|
|
entries: [
|
|
{
|
|
path: "/workspace/demo/apps/11.md",
|
|
name: "11.md"
|
|
},
|
|
{
|
|
path: "/workspace/demo/static/app.js",
|
|
name: "app.js"
|
|
},
|
|
{
|
|
path: "/workspace/demo/static/index.html",
|
|
name: "index.html"
|
|
},
|
|
{
|
|
path: "/workspace/demo/static/styles.css",
|
|
name: "styles.css"
|
|
}
|
|
]
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryAgentGeneratedFiles,
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("file");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
groups: [
|
|
expect.objectContaining({ id: "opened_files" }),
|
|
{
|
|
id: "agent_generated_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "11.md",
|
|
path: "/workspace/demo/apps/11.md"
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "static",
|
|
path: "/workspace/demo/static",
|
|
entryKind: "directory",
|
|
mentionNavigation: "agent-generated-folder",
|
|
childCount: 3
|
|
})
|
|
],
|
|
totalCount: 2,
|
|
visibleCount: 2,
|
|
hasMore: false
|
|
}
|
|
]
|
|
})
|
|
);
|
|
|
|
const latestState = states.at(-1) as {
|
|
groups: Array<{
|
|
id: string;
|
|
items: Array<{ mentionNavigation?: string; path: string }>;
|
|
}>;
|
|
};
|
|
const folderItem = latestState.groups
|
|
.find((group) => group.id === "agent_generated_files")
|
|
?.items.find(
|
|
(item) => item.mentionNavigation === "agent-generated-folder"
|
|
);
|
|
expect(folderItem).toBeDefined();
|
|
controller.selectFileMentionNavigationItem(
|
|
folderItem as Parameters<
|
|
BaseAgentMentionSearchController["selectFileMentionNavigationItem"]
|
|
>[0]
|
|
);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
groups: [
|
|
expect.objectContaining({ id: "opened_files" }),
|
|
{
|
|
id: "agent_generated_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "agent-generated-folder-back",
|
|
name: "返回"
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "app.js",
|
|
path: "/workspace/demo/static/app.js"
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "index.html",
|
|
path: "/workspace/demo/static/index.html"
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "styles.css",
|
|
path: "/workspace/demo/static/styles.css"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
setAgentGuiI18nTestLocale("en");
|
|
});
|
|
|
|
it("loads nested workspace folders with child counts and returns one level at a time", async () => {
|
|
setAgentGuiI18nTestLocale("zh-CN");
|
|
const queryFiles = vi.fn(
|
|
async ({ directoryPath }: { directoryPath?: string }) => {
|
|
if (directoryPath === "/workspace/src/components") {
|
|
return {
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/components/Button.tsx",
|
|
name: "Button.tsx",
|
|
kind: "file"
|
|
}
|
|
]
|
|
};
|
|
}
|
|
if (directoryPath === "/workspace/src") {
|
|
return {
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/components",
|
|
name: "components",
|
|
kind: "directory",
|
|
childCount: 1
|
|
},
|
|
{
|
|
path: "/workspace/src/index.ts",
|
|
name: "index.ts",
|
|
kind: "file"
|
|
}
|
|
]
|
|
};
|
|
}
|
|
return {
|
|
entries: [
|
|
{
|
|
path: "/workspace/src",
|
|
name: "src",
|
|
kind: "directory",
|
|
childCount: 2
|
|
}
|
|
]
|
|
};
|
|
}
|
|
);
|
|
const controller = new AgentMentionSearchController({ queryFiles });
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("file");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups).toEqual([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
childCount: 2,
|
|
entryKind: "directory",
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
]
|
|
}),
|
|
expect.objectContaining({ id: "agent_generated_files" })
|
|
])
|
|
);
|
|
|
|
const rootFolder = states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "opened_files")
|
|
?.items.at(0);
|
|
expect(rootFolder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(rootFolder!)).toBe(true);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups[0]).toMatchObject({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder-back",
|
|
name: "返回"
|
|
}),
|
|
expect.objectContaining({
|
|
childCount: 1,
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src/components"
|
|
}),
|
|
expect.objectContaining({ path: "/workspace/src/index.ts" })
|
|
]
|
|
})
|
|
);
|
|
|
|
const nestedFolder = states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "opened_files")
|
|
?.items.find(
|
|
(item) =>
|
|
item.kind === "file" && item.path === "/workspace/src/components"
|
|
);
|
|
expect(nestedFolder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(nestedFolder!)).toBe(
|
|
true
|
|
);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups[0]?.items).toEqual([
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder-back",
|
|
path: "/workspace/src/components"
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/workspace/src/components/Button.tsx"
|
|
})
|
|
])
|
|
);
|
|
|
|
expect(controller.exitFileMentionBrowse()).toBe(true);
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups[0]?.items).toEqual([
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder-back",
|
|
path: "/workspace/src"
|
|
}),
|
|
expect.objectContaining({ path: "/workspace/src/components" }),
|
|
expect.objectContaining({ path: "/workspace/src/index.ts" })
|
|
])
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledWith(
|
|
expect.objectContaining({ directoryPath: "/workspace/src/components" })
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledWith(
|
|
expect.objectContaining({ directoryPath: "/workspace/src" })
|
|
);
|
|
|
|
controller.dispose();
|
|
setAgentGuiI18nTestLocale("en");
|
|
});
|
|
|
|
it("keeps an entered workspace folder when the cleared editor query repeats", async () => {
|
|
const queryFiles = vi.fn(
|
|
async ({ directoryPath }: { directoryPath?: string }) => ({
|
|
entries: directoryPath
|
|
? [
|
|
{
|
|
path: `${directoryPath}/Button.tsx`,
|
|
name: "Button.tsx",
|
|
kind: "file"
|
|
}
|
|
]
|
|
: [
|
|
{
|
|
path: "/workspace/src",
|
|
name: "src",
|
|
kind: "directory",
|
|
childCount: 1
|
|
}
|
|
]
|
|
})
|
|
);
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
debounceMs: 0
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "src" });
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
mode: "results",
|
|
groups: [
|
|
expect.objectContaining({
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
})
|
|
);
|
|
const folder = states.at(-1)?.groups[0]?.items[0];
|
|
expect(folder).toBeDefined();
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
expect(controller.selectFileMentionNavigationItem(folder!)).toBe(true);
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
|
|
await waitForFast(() => {
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "file"
|
|
});
|
|
expect(
|
|
states.at(-1)?.groups.find((group) => group.id === "opened_files")
|
|
?.items
|
|
).toEqual([
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder-back"
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/workspace/src/Button.tsx"
|
|
})
|
|
]);
|
|
});
|
|
expect(queryFiles).toHaveBeenCalledWith(
|
|
expect.objectContaining({ directoryPath: "/workspace/src" })
|
|
);
|
|
|
|
controller.setProvenanceFilters({
|
|
session: null,
|
|
file: { agentTargetIds: ["agent-1"], memberIds: null },
|
|
issue: null,
|
|
agent: null,
|
|
app: null
|
|
});
|
|
expect(controller.exitFileMentionBrowse()).toBe(false);
|
|
|
|
controller.dispose();
|
|
});
|
|
|
|
it("keeps an entered agent-generated folder when the cleared editor query repeats", async () => {
|
|
const queryAgentGeneratedFiles = vi.fn().mockResolvedValue({
|
|
entries: [
|
|
{
|
|
path: "/workspace/demo/static/app.js",
|
|
name: "app.js"
|
|
},
|
|
{
|
|
path: "/workspace/demo/static/index.html",
|
|
name: "index.html"
|
|
},
|
|
{
|
|
path: "/workspace/demo/static/styles.css",
|
|
name: "styles.css"
|
|
}
|
|
]
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryAgentGeneratedFiles,
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("file");
|
|
|
|
await waitForFast(() => {
|
|
const folder = states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "agent_generated_files")
|
|
?.items.find(
|
|
(item) =>
|
|
item.kind === "file" &&
|
|
item.mentionNavigation === "agent-generated-folder"
|
|
);
|
|
expect(folder).toEqual(
|
|
expect.objectContaining({
|
|
path: "/workspace/demo/static",
|
|
childCount: 3
|
|
})
|
|
);
|
|
});
|
|
|
|
const folder = states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "agent_generated_files")
|
|
?.items.find(
|
|
(item) =>
|
|
item.kind === "file" &&
|
|
item.mentionNavigation === "agent-generated-folder"
|
|
);
|
|
expect(folder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(folder!)).toBe(true);
|
|
|
|
// The editor emits this duplicate empty query after the click. It must
|
|
// not reset the agent-generated browse path back to the root folder list.
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
|
|
await waitForFast(() => {
|
|
expect(states.at(-1)?.groups).toEqual([
|
|
expect.objectContaining({
|
|
id: "opened_files"
|
|
}),
|
|
expect.objectContaining({
|
|
id: "agent_generated_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "agent-generated-folder-back"
|
|
}),
|
|
expect.objectContaining({ path: "/workspace/demo/static/app.js" }),
|
|
expect.objectContaining({
|
|
path: "/workspace/demo/static/index.html"
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/workspace/demo/static/styles.css"
|
|
})
|
|
]
|
|
})
|
|
]);
|
|
});
|
|
|
|
controller.dispose();
|
|
});
|
|
|
|
it("keeps a directory request alive beyond the provider search timeout", async () => {
|
|
let resolveDirectory:
|
|
| ((value: {
|
|
entries: Array<{ path: string; name: string; kind: string }>;
|
|
}) => void)
|
|
| undefined;
|
|
const queryFiles = vi.fn(
|
|
({ directoryPath }: { directoryPath?: string }) => {
|
|
if (directoryPath === "/workspace/src") {
|
|
return new Promise<{
|
|
entries: Array<{ path: string; name: string; kind: string }>;
|
|
}>((resolve) => {
|
|
resolveDirectory = resolve;
|
|
});
|
|
}
|
|
return Promise.resolve({
|
|
entries: [
|
|
{
|
|
path: "/workspace/src",
|
|
name: "src",
|
|
kind: "directory",
|
|
childCount: 1
|
|
}
|
|
]
|
|
});
|
|
}
|
|
);
|
|
const controller = new AgentMentionSearchController({
|
|
providerTimeoutMs: 20,
|
|
queryFiles
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups[0]?.items[0]).toMatchObject({
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
);
|
|
|
|
const folder = states.at(-1)?.groups[0]?.items[0];
|
|
expect(folder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(folder!)).toBe(true);
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
error: null
|
|
});
|
|
|
|
resolveDirectory?.({
|
|
entries: [
|
|
{
|
|
path: "/workspace/src/index.ts",
|
|
name: "index.ts",
|
|
kind: "file"
|
|
}
|
|
]
|
|
});
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder-back"
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/workspace/src/index.ts"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
})
|
|
);
|
|
});
|
|
|
|
it("cancels an in-flight directory request when the search scope changes", async () => {
|
|
let resolveDirectory:
|
|
| ((value: readonly TestFileMentionItem[]) => void)
|
|
| undefined;
|
|
let directoryAbortSignal: AbortSignal | undefined;
|
|
const fileProvider = createTestFileProvider({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
entries: [
|
|
{
|
|
path: "/workspace/src",
|
|
name: "src",
|
|
kind: "directory",
|
|
childCount: 1
|
|
}
|
|
]
|
|
})
|
|
});
|
|
fileProvider.queryDirectory = (input) => {
|
|
directoryAbortSignal = input.abortSignal;
|
|
return new Promise<readonly TestFileMentionItem[]>((resolve) => {
|
|
resolveDirectory = resolve;
|
|
});
|
|
};
|
|
const controller = new AgentMentionSearchController({
|
|
contextMentionProviders: [fileProvider]
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups[0]?.items[0]).toMatchObject({
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
);
|
|
|
|
const folder = states.at(-1)?.groups[0]?.items[0];
|
|
expect(folder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(folder!)).toBe(true);
|
|
expect(directoryAbortSignal?.aborted).toBe(false);
|
|
|
|
controller.updateQuery({ workspaceId: "room-2", query: "" });
|
|
expect(directoryAbortSignal?.aborted).toBe(true);
|
|
resolveDirectory?.([
|
|
{
|
|
href: "/workspace/src/stale.ts",
|
|
label: "stale.ts",
|
|
kind: "file"
|
|
}
|
|
]);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
})
|
|
);
|
|
expect(JSON.stringify(states.at(-1))).not.toContain("stale.ts");
|
|
});
|
|
|
|
it("surfaces directory provider failures instead of presenting an empty directory", async () => {
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn(({ directoryPath }: { directoryPath?: string }) => {
|
|
if (directoryPath === "/workspace/src") {
|
|
return Promise.reject(new Error("directory read failed"));
|
|
}
|
|
return Promise.resolve({
|
|
entries: [
|
|
{
|
|
path: "/workspace/src",
|
|
name: "src",
|
|
kind: "directory",
|
|
childCount: 1
|
|
}
|
|
]
|
|
});
|
|
})
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups[0]?.items[0]).toMatchObject({
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
);
|
|
|
|
const folder = states.at(-1)?.groups[0]?.items[0];
|
|
expect(folder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(folder!)).toBe(true);
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "error",
|
|
error: "directory read failed"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("fails closed when a file provider does not implement directory queries", async () => {
|
|
const query = vi.fn().mockResolvedValue([
|
|
{
|
|
childCount: 1,
|
|
href: "/workspace/src",
|
|
kind: "directory",
|
|
label: "src"
|
|
}
|
|
]);
|
|
const fileProvider: AgentContextMentionProvider<TestFileMentionItem> = {
|
|
id: FILE_PROVIDER_ID,
|
|
trigger: "@",
|
|
query,
|
|
getItemDirectory: (item) => ({
|
|
childCount: item.childCount,
|
|
path: item.href
|
|
}),
|
|
getItemKey: (item) => item.href,
|
|
getItemLabel: (item) => item.label,
|
|
toInsertResult: (item) => ({
|
|
kind: "markdown-link",
|
|
label: item.label,
|
|
href: item.href
|
|
})
|
|
};
|
|
const controller = new AgentMentionSearchController({
|
|
contextMentionProviders: [fileProvider]
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups[0]?.items[0]).toMatchObject({
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
);
|
|
|
|
const folder = states.at(-1)?.groups[0]?.items[0];
|
|
expect(folder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(folder!)).toBe(true);
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "error",
|
|
error: "Mention provider does not support directory browsing."
|
|
})
|
|
);
|
|
expect(query).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("keeps workspace and agent-generated folder navigation independent", async () => {
|
|
setAgentGuiI18nTestLocale("zh-CN");
|
|
const queryFiles = vi.fn(
|
|
async ({ directoryPath }: { directoryPath?: string }) => ({
|
|
entries:
|
|
directoryPath === "/workspace/src"
|
|
? [
|
|
{
|
|
path: "/workspace/src/index.ts",
|
|
name: "index.ts",
|
|
kind: "file"
|
|
}
|
|
]
|
|
: [
|
|
{
|
|
path: "/workspace/src",
|
|
name: "src",
|
|
kind: "directory",
|
|
childCount: 1
|
|
}
|
|
]
|
|
})
|
|
);
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryAgentGeneratedFiles: vi.fn().mockResolvedValue({
|
|
entries: [
|
|
{
|
|
path: "/workspace/generated/app.js",
|
|
name: "app.js"
|
|
},
|
|
{
|
|
path: "/workspace/generated/index.html",
|
|
name: "index.html"
|
|
}
|
|
]
|
|
})
|
|
});
|
|
const states: AgentMentionSearchState[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({ workspaceId: "room-1", query: "" });
|
|
controller.setFilter("file");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)?.groups).toEqual([
|
|
expect.objectContaining({
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "workspace-folder",
|
|
path: "/workspace/src"
|
|
})
|
|
]
|
|
}),
|
|
expect.objectContaining({
|
|
id: "agent_generated_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
mentionNavigation: "agent-generated-folder",
|
|
path: "/workspace/generated"
|
|
})
|
|
]
|
|
})
|
|
])
|
|
);
|
|
|
|
const workspaceFolder = states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "opened_files")
|
|
?.items.find(
|
|
(item) =>
|
|
item.kind === "file" && item.mentionNavigation === "workspace-folder"
|
|
);
|
|
expect(workspaceFolder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(workspaceFolder!)).toBe(
|
|
true
|
|
);
|
|
|
|
await waitForFast(() =>
|
|
expect(
|
|
states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "opened_files")
|
|
?.items.at(0)
|
|
).toMatchObject({ mentionNavigation: "workspace-folder-back" })
|
|
);
|
|
|
|
const generatedFolder = states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "agent_generated_files")
|
|
?.items.find(
|
|
(item) =>
|
|
item.kind === "file" &&
|
|
item.mentionNavigation === "agent-generated-folder"
|
|
);
|
|
expect(generatedFolder).toBeDefined();
|
|
expect(controller.selectFileMentionNavigationItem(generatedFolder!)).toBe(
|
|
true
|
|
);
|
|
|
|
const bothNestedState = states.at(-1);
|
|
const workspaceBack = bothNestedState?.groups
|
|
.find((group) => group.id === "opened_files")
|
|
?.items.find(
|
|
(item) =>
|
|
item.kind === "file" &&
|
|
item.mentionNavigation === "workspace-folder-back"
|
|
);
|
|
const generatedBack = bothNestedState?.groups
|
|
.find((group) => group.id === "agent_generated_files")
|
|
?.items.find(
|
|
(item) =>
|
|
item.kind === "file" &&
|
|
item.mentionNavigation === "agent-generated-folder-back"
|
|
);
|
|
expect(workspaceBack).toBeDefined();
|
|
expect(generatedBack).toBeDefined();
|
|
|
|
expect(controller.selectFileMentionNavigationItem(workspaceBack!)).toBe(
|
|
true
|
|
);
|
|
await waitForFast(() =>
|
|
expect(
|
|
states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "opened_files")
|
|
?.items.some(
|
|
(item) =>
|
|
item.kind === "file" &&
|
|
item.mentionNavigation === "workspace-folder-back"
|
|
)
|
|
).toBe(false)
|
|
);
|
|
expect(
|
|
states
|
|
.at(-1)
|
|
?.groups.find((group) => group.id === "agent_generated_files")
|
|
?.items.at(0)
|
|
).toMatchObject({ mentionNavigation: "agent-generated-folder-back" });
|
|
|
|
expect(controller.selectFileMentionNavigationItem(generatedBack!)).toBe(
|
|
true
|
|
);
|
|
controller.dispose();
|
|
setAgentGuiI18nTestLocale("en");
|
|
});
|
|
|
|
it("loads default session items in browse mode when switching to the session tab", async () => {
|
|
const querySessions = vi.fn().mockResolvedValue({
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
...{
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: []
|
|
},
|
|
agentSessionId: "session-1",
|
|
workspaceId: "room-1",
|
|
userId: "user-1",
|
|
provider: "codex",
|
|
title: "@README.md 这是什么内容",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 10
|
|
}
|
|
]
|
|
});
|
|
const loadSessionSummary = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
agentSessionId: "session-1",
|
|
executionStatus: "RUNNING",
|
|
latestUserRequirement: "看看项目有什么文件",
|
|
recentAgentReplies: ["已读取 workspace 结构"],
|
|
initialUserRequirement: "",
|
|
initialTurn: null,
|
|
latestTurn: null,
|
|
recentTurns: []
|
|
});
|
|
const loadSessionMessages = vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false });
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions,
|
|
loadSessionMessages,
|
|
loadSessionSummary,
|
|
loadUserProfiles: vi.fn().mockResolvedValue({
|
|
users: [{ userId: "user-1", name: "Wang" }]
|
|
})
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("session");
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
mode: "browse",
|
|
filter: "session",
|
|
query: ""
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "session",
|
|
groups: [
|
|
{
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "session",
|
|
targetId: "session-1",
|
|
agentName: "Codex",
|
|
name: "@README.md 这是什么内容",
|
|
title: "README.md 这是什么内容"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(querySessions).toHaveBeenLastCalledWith({
|
|
workspaceId: "room-1",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME"
|
|
});
|
|
expect(loadSessionSummary).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
agentSessionId: "session-1",
|
|
agentReplyLimit: 1,
|
|
recentTurnLimit: 1
|
|
});
|
|
expect(loadSessionMessages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("queries runtime-origin sessions in browse mode when switching to the session tab", async () => {
|
|
const querySessions = vi.fn().mockImplementation(async (input) => {
|
|
if (
|
|
typeof input !== "string" &&
|
|
input.sessionOrigin === "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME"
|
|
) {
|
|
return {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
...{
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: []
|
|
},
|
|
agentSessionId: "runtime-session-1",
|
|
workspaceId: "room-1",
|
|
userId: "user-1",
|
|
provider: "codex",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
|
|
title: "分析open code 架构设计",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 10
|
|
}
|
|
]
|
|
};
|
|
}
|
|
return { presences: [], sessions: [] };
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions,
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
agentSessionId: "runtime-session-1",
|
|
executionStatus: "RUNNING",
|
|
latestUserRequirement: "分析open code 架构设计",
|
|
recentAgentReplies: [],
|
|
initialUserRequirement: "",
|
|
initialTurn: null,
|
|
latestTurn: null,
|
|
recentTurns: []
|
|
}),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({
|
|
users: [{ userId: "user-1", name: "Wang" }]
|
|
})
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("session");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "session",
|
|
groups: [
|
|
{
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "session",
|
|
targetId: "runtime-session-1",
|
|
title: "分析open code 架构设计"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(querySessions).toHaveBeenLastCalledWith({
|
|
workspaceId: "room-1",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME"
|
|
});
|
|
});
|
|
|
|
it("uses the canonical session title without loading message fallback", async () => {
|
|
const loadSessionMessages = vi.fn().mockResolvedValue({
|
|
messages: [
|
|
{
|
|
id: 1,
|
|
messageId: "message-user-1",
|
|
agentSessionId: "runtime-session-1",
|
|
role: "user",
|
|
kind: "text",
|
|
payload: { text: "这个会话里面做了什么事情" },
|
|
version: 1,
|
|
occurredAtUnixMs: 10
|
|
}
|
|
],
|
|
latestVersion: 1,
|
|
hasMore: false
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
...{
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: []
|
|
},
|
|
agentSessionId: "runtime-session-1",
|
|
workspaceId: "room-1",
|
|
userId: "user-1",
|
|
provider: "codex",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
|
|
title: "Canonical title",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 10
|
|
}
|
|
]
|
|
}),
|
|
loadSessionMessages,
|
|
loadSessionSummary: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
agentSessionId: "runtime-session-1",
|
|
executionStatus: "RUNNING",
|
|
latestUserRequirement: "",
|
|
recentAgentReplies: [],
|
|
initialUserRequirement: "",
|
|
initialTurn: null,
|
|
latestTurn: null,
|
|
recentTurns: []
|
|
}),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({
|
|
users: [{ userId: "user-1", name: "Wang" }]
|
|
})
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("session");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "session",
|
|
groups: [
|
|
{
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "session",
|
|
targetId: "runtime-session-1",
|
|
title: "Canonical title"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(loadSessionMessages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("loads default issue items in browse mode when switching to the issue tab", async () => {
|
|
const queryIssues = vi.fn().mockResolvedValue({
|
|
issues: [
|
|
{
|
|
issueId: "issue-1",
|
|
workspaceId: "room-1",
|
|
title: "修复 room status",
|
|
content: JSON.stringify({
|
|
type: "doc",
|
|
content: [
|
|
{
|
|
type: "paragraph",
|
|
content: [{ type: "text", text: "补齐 statusBatch 的错误处理" }]
|
|
}
|
|
]
|
|
}),
|
|
status: "running",
|
|
creatorUserId: "user-2",
|
|
creatorDisplayName: "Alice",
|
|
issueCount: 0,
|
|
notStartedCount: 0,
|
|
runningCount: 0,
|
|
pendingAcceptanceCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
canceledCount: 0,
|
|
updatedAtUnix: 20
|
|
}
|
|
],
|
|
totalCount: 1,
|
|
statusCounts: undefined
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues,
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("issue");
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
mode: "browse",
|
|
filter: "issue",
|
|
query: ""
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "issue",
|
|
groups: [
|
|
{
|
|
id: "issues",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "workspace-issue",
|
|
targetId: "issue-1",
|
|
contentPreview: "补齐 statusBatch 的错误处理"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(queryIssues).toHaveBeenLastCalledWith({
|
|
workspaceId: "room-1",
|
|
pageSize: 25,
|
|
searchQuery: ""
|
|
});
|
|
});
|
|
|
|
it("loads grouped session browse without querying other tabs", async () => {
|
|
const queryIssues = vi.fn().mockResolvedValue({
|
|
issues: [
|
|
{
|
|
issueId: "issue-1",
|
|
workspaceId: "room-1",
|
|
title: "修复 room status",
|
|
content: JSON.stringify({
|
|
type: "doc",
|
|
content: [
|
|
{
|
|
type: "paragraph",
|
|
content: [{ type: "text", text: "补齐 statusBatch 的错误处理" }]
|
|
}
|
|
]
|
|
}),
|
|
status: "running",
|
|
creatorUserId: "user-2",
|
|
creatorDisplayName: "Alice",
|
|
issueCount: 0,
|
|
notStartedCount: 0,
|
|
runningCount: 0,
|
|
pendingAcceptanceCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
canceledCount: 0,
|
|
updatedAtUnix: 20
|
|
}
|
|
],
|
|
totalCount: 1,
|
|
statusCounts: undefined
|
|
});
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
});
|
|
const querySessions = vi.fn().mockResolvedValue({
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
...{
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: []
|
|
},
|
|
agentSessionId: "session-1",
|
|
workspaceId: "room-1",
|
|
userId: "user-1",
|
|
provider: "codex",
|
|
title: "看看项目有什么文件",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 10
|
|
},
|
|
{
|
|
agentSessionId: "session-2",
|
|
workspaceId: "room-1",
|
|
userId: "user-2",
|
|
provider: "nexight",
|
|
title: "room status 接口整理",
|
|
createdAtUnixMs: 2,
|
|
updatedAtUnixMs: 9
|
|
}
|
|
]
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues,
|
|
querySessions,
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn().mockImplementation(({ agentSessionId }) =>
|
|
Promise.resolve({
|
|
workspaceId: "room-1",
|
|
agentSessionId,
|
|
executionStatus:
|
|
agentSessionId === "session-1" ? "RUNNING" : "COMPLETED",
|
|
latestUserRequirement:
|
|
agentSessionId === "session-1"
|
|
? "看看项目有什么文件"
|
|
: "整理 room status",
|
|
recentAgentReplies:
|
|
agentSessionId === "session-1"
|
|
? ["已读取 workspace 结构"]
|
|
: ["输出了 statusBatch 调用链"],
|
|
initialUserRequirement: "",
|
|
initialTurn: null,
|
|
latestTurn: null,
|
|
recentTurns: []
|
|
})
|
|
),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({
|
|
users: [
|
|
{ userId: "user-1", name: "Wang" },
|
|
{ userId: "user-2", name: "Alice" }
|
|
]
|
|
})
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
mode: "browse",
|
|
filter: "session",
|
|
query: ""
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "session",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
targetId: "session-1",
|
|
kind: "session"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
})
|
|
);
|
|
|
|
expect(querySessions).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME"
|
|
});
|
|
expect(queryFiles).not.toHaveBeenCalled();
|
|
expect(queryIssues).not.toHaveBeenCalled();
|
|
expect(
|
|
(
|
|
states.at(-1) as {
|
|
groups: Array<{ id: string }>;
|
|
}
|
|
).groups.map((group) => group.id)
|
|
).toEqual(["my_sessions"]);
|
|
});
|
|
|
|
it("keeps the default session empty state scoped to sessions", async () => {
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "session",
|
|
groups: [
|
|
{ id: "my_sessions", items: [], emptyLabel: "No sessions yet" }
|
|
]
|
|
})
|
|
);
|
|
});
|
|
|
|
it("omits empty file subgroups while searching on the file tab when another subgroup has matches", async () => {
|
|
vi.useFakeTimers();
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: [
|
|
{
|
|
path: "/workspace/quickPhrases.ts",
|
|
name: "quickPhrases.ts",
|
|
kind: "file"
|
|
}
|
|
]
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
debounceMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("file");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "file"
|
|
})
|
|
);
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: "quick"
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "results",
|
|
filter: "file",
|
|
groups: [
|
|
{
|
|
id: "opened_files",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
name: "quickPhrases.ts",
|
|
path: "/workspace/quickPhrases.ts"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(
|
|
(states.at(-1) as { groups: Array<{ id: string }> }).groups.some(
|
|
(group) => group.id === "agent_generated_files"
|
|
)
|
|
).toBe(false);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("searches only the default session filter for typed queries", async () => {
|
|
vi.useFakeTimers();
|
|
const queryFiles = vi.fn().mockResolvedValue([
|
|
{
|
|
label: "automation.md",
|
|
href: "/workspace/docs/automation.md"
|
|
}
|
|
]);
|
|
const queryApps = vi.fn().mockResolvedValue([
|
|
{
|
|
appId: "automation",
|
|
name: "Automation"
|
|
}
|
|
]);
|
|
const querySessions = vi.fn().mockResolvedValue([
|
|
{
|
|
agentName: "Codex",
|
|
id: "session-automation",
|
|
initiatorName: "local",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Automation cleanup",
|
|
updatedAtUnixMs: 30,
|
|
userId: "local",
|
|
workspaceId: "room-1"
|
|
}
|
|
]);
|
|
const controller = new AgentMentionSearchController({
|
|
contextMentionProviders: [
|
|
{
|
|
id: FILE_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: queryFiles,
|
|
getItemKey: (item) => item.href,
|
|
getItemLabel: (item) => item.label,
|
|
toInsertResult: (item) => ({
|
|
kind: "markdown-link",
|
|
label: item.label,
|
|
href: item.href
|
|
})
|
|
},
|
|
{
|
|
id: WORKSPACE_APP_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: queryApps,
|
|
getItemKey: (item) => item.appId,
|
|
getItemLabel: (item) => item.name,
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.appId,
|
|
label: item.name,
|
|
scope: { workspaceId: "room-1" }
|
|
}
|
|
})
|
|
},
|
|
{
|
|
id: AGENT_SESSION_PROVIDER_ID,
|
|
trigger: "@",
|
|
query: querySessions,
|
|
getItemKey: (item) => item.id,
|
|
getItemLabel: (item) => item.title,
|
|
toInsertResult: (item) => ({
|
|
kind: "mention",
|
|
mention: {
|
|
entityId: item.id,
|
|
label: item.title,
|
|
scope: {
|
|
scope: "my_sessions",
|
|
userId: item.userId,
|
|
workspaceId: item.workspaceId
|
|
},
|
|
presentation: {
|
|
subtitle: item.agentName
|
|
}
|
|
}
|
|
})
|
|
}
|
|
],
|
|
debounceMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "local",
|
|
query: "automation"
|
|
});
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "results",
|
|
filter: "session",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "session",
|
|
targetId: "session-automation"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
})
|
|
);
|
|
expect(querySessions).toHaveBeenCalledTimes(1);
|
|
expect(queryFiles).not.toHaveBeenCalled();
|
|
expect(queryApps).not.toHaveBeenCalled();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("searches the app provider with the existing query after switching to the app tab", async () => {
|
|
vi.useFakeTimers();
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
});
|
|
const queryIssues = vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
});
|
|
const queryWorkspaceApps = vi.fn().mockResolvedValue({
|
|
apps: [
|
|
{
|
|
appId: "vibe-design",
|
|
description: "Design prototypes in Tutti.",
|
|
name: "Vibe Design",
|
|
workspaceId: "room-1"
|
|
}
|
|
]
|
|
});
|
|
const querySessions = vi
|
|
.fn()
|
|
.mockResolvedValue({ presences: [], sessions: [] });
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues,
|
|
querySessions,
|
|
queryWorkspaceApps,
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
debounceMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "local",
|
|
query: "design"
|
|
});
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "results",
|
|
filter: "session"
|
|
})
|
|
);
|
|
controller.setFilter("app");
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "loading",
|
|
mode: "results",
|
|
filter: "app",
|
|
query: "design"
|
|
});
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "results",
|
|
filter: "app",
|
|
query: "design",
|
|
groups: [
|
|
expect.objectContaining({
|
|
id: "apps",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "workspace-app",
|
|
appId: "vibe-design"
|
|
})
|
|
]
|
|
})
|
|
]
|
|
})
|
|
);
|
|
expect(queryWorkspaceApps).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
query: "design",
|
|
limit: undefined
|
|
});
|
|
expect(queryFiles).not.toHaveBeenCalled();
|
|
expect(queryIssues).not.toHaveBeenCalled();
|
|
expect(querySessions).toHaveBeenCalledTimes(1);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("keeps file query misses scoped to the file tab", async () => {
|
|
vi.useFakeTimers();
|
|
const queryFiles = vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles,
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] }),
|
|
debounceMs: 20
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.setFilter("file");
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: "readme"
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "results",
|
|
filter: "file",
|
|
groups: []
|
|
})
|
|
);
|
|
expect(queryFiles).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
workspaceId: "room-1",
|
|
query: "readme",
|
|
limit: 30
|
|
})
|
|
);
|
|
});
|
|
|
|
it("expands the issue group without changing the selected tab", async () => {
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: Array.from({ length: 14 }, (_, index) => ({
|
|
issueId: `issue-${index + 1}`,
|
|
workspaceId: "room-1",
|
|
title: `Issue ${index + 1}`,
|
|
content: "",
|
|
status: "running",
|
|
creatorUserId: "user-2",
|
|
creatorDisplayName: "Alice",
|
|
issueCount: 0,
|
|
notStartedCount: 0,
|
|
runningCount: 0,
|
|
pendingAcceptanceCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
canceledCount: 0,
|
|
updatedAtUnix: 20 + index
|
|
})),
|
|
totalCount: 14,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({ presences: [], sessions: [] }),
|
|
loadSessionMessages: vi
|
|
.fn()
|
|
.mockResolvedValue({ messages: [], latestVersion: 0, hasMore: false }),
|
|
loadSessionSummary: vi.fn(),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({ users: [] })
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("issue");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "issue",
|
|
groups: [
|
|
{
|
|
id: "issues",
|
|
visibleCount: 10,
|
|
hasMore: true
|
|
}
|
|
]
|
|
})
|
|
);
|
|
|
|
controller.expandGroup("issues");
|
|
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
mode: "browse",
|
|
filter: "issue",
|
|
groups: [
|
|
{
|
|
id: "issues",
|
|
visibleCount: 14,
|
|
hasMore: false
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
it("falls back to the first user message when session title and summary are empty", async () => {
|
|
const loadSessionMessages = vi.fn().mockResolvedValue({
|
|
messages: [
|
|
{
|
|
id: 1,
|
|
messageId: "message-user-1",
|
|
agentSessionId: "session-1",
|
|
role: "user",
|
|
kind: "text",
|
|
payload: { text: "hi" },
|
|
version: 1,
|
|
occurredAtUnixMs: 10
|
|
}
|
|
],
|
|
latestVersion: 1,
|
|
hasMore: false
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
...{
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: []
|
|
},
|
|
agentSessionId: "session-1",
|
|
workspaceId: "room-1",
|
|
userId: "user-1",
|
|
provider: "codex",
|
|
title: "",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 10
|
|
}
|
|
]
|
|
}),
|
|
loadSessionMessages,
|
|
loadSessionSummary: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
agentSessionId: "session-1",
|
|
executionStatus: "COMPLETED",
|
|
latestUserRequirement: "",
|
|
recentAgentReplies: [],
|
|
initialUserRequirement: "",
|
|
initialTurn: null,
|
|
latestTurn: null,
|
|
recentTurns: []
|
|
}),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({
|
|
users: [{ userId: "user-1", name: "Wang" }]
|
|
})
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("session");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
groups: [
|
|
{
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "session",
|
|
targetId: "session-1",
|
|
agentName: "Codex",
|
|
name: "hi"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(loadSessionMessages).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
|
|
agentSessionId: "session-1",
|
|
afterVersion: 0,
|
|
limit: 20
|
|
});
|
|
});
|
|
|
|
it("falls back to the first user message when the session title is missing", async () => {
|
|
const loadSessionMessages = vi.fn().mockResolvedValue({
|
|
messages: [
|
|
{
|
|
id: 1,
|
|
messageId: "message-user-1",
|
|
agentSessionId: "session-1",
|
|
role: "user",
|
|
kind: "text",
|
|
payload: { text: "如何做excel的数据清理" },
|
|
version: 1,
|
|
occurredAtUnixMs: 10
|
|
}
|
|
],
|
|
latestVersion: 1,
|
|
hasMore: false
|
|
});
|
|
const controller = new AgentMentionSearchController({
|
|
queryFiles: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
root: "/workspace",
|
|
entries: []
|
|
}),
|
|
queryIssues: vi.fn().mockResolvedValue({
|
|
issues: [],
|
|
totalCount: 0,
|
|
statusCounts: undefined
|
|
}),
|
|
querySessions: vi.fn().mockResolvedValue({
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
...{
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: []
|
|
},
|
|
agentSessionId: "session-1",
|
|
workspaceId: "room-1",
|
|
userId: "user-1",
|
|
provider: "codex",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
|
|
title: "",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 10
|
|
}
|
|
]
|
|
}),
|
|
loadSessionMessages,
|
|
loadSessionSummary: vi.fn().mockResolvedValue({
|
|
workspaceId: "room-1",
|
|
agentSessionId: "session-1",
|
|
executionStatus: "COMPLETED",
|
|
latestUserRequirement: "",
|
|
recentAgentReplies: [],
|
|
initialUserRequirement: "",
|
|
initialTurn: null,
|
|
latestTurn: null,
|
|
recentTurns: []
|
|
}),
|
|
loadUserProfiles: vi.fn().mockResolvedValue({
|
|
users: [{ userId: "user-1", name: "Wang" }]
|
|
})
|
|
});
|
|
const states: unknown[] = [];
|
|
controller.subscribe((state) => states.push(state));
|
|
|
|
controller.updateQuery({
|
|
workspaceId: "room-1",
|
|
currentUserId: "user-1",
|
|
query: ""
|
|
});
|
|
controller.setFilter("session");
|
|
|
|
await waitForFast(() =>
|
|
expect(states.at(-1)).toMatchObject({
|
|
status: "ready",
|
|
query: "",
|
|
groups: [
|
|
{
|
|
id: "my_sessions",
|
|
items: [
|
|
expect.objectContaining({
|
|
kind: "session",
|
|
targetId: "session-1",
|
|
agentName: "Codex",
|
|
name: "如何做excel的数据清理",
|
|
title: "如何做excel的数据清理"
|
|
})
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(loadSessionMessages).toHaveBeenCalledWith({
|
|
workspaceId: "room-1",
|
|
|
|
agentSessionId: "session-1",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
|
|
afterVersion: 0,
|
|
limit: 20
|
|
});
|
|
});
|
|
});
|