feat(tree): complete rust family runtime checklist
- add tree shell runtime artifact contracts and page/filetree/picker runtime reducers - sink tree.subtree.move write operation through Rust and formalize command event plans - harden file tree search projection contract and route thin-proxy boundaries - record completed harness tasks and move design docs into process/done
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fetchKernelFileTreeProjection } from "./projection-client";
|
||||
import {
|
||||
FILE_TREE_SEARCH_MAX_RESULTS,
|
||||
buildFileTreeProjectionSearchRequestMeta,
|
||||
fetchKernelFileTreeProjection,
|
||||
} from "./projection-client";
|
||||
|
||||
describe("fetchKernelFileTreeProjection", () => {
|
||||
beforeEach(() => {
|
||||
@@ -42,6 +46,59 @@ describe("fetchKernelFileTreeProjection", () => {
|
||||
expect(result.items.map((item) => item.rowId)).toEqual(["asset:table_1"]);
|
||||
});
|
||||
|
||||
it("固定 file_tree 搜索语义边界:命中数先截断,祖先补全不计入 maxResults", () => {
|
||||
expect(FILE_TREE_SEARCH_MAX_RESULTS).toBe(80);
|
||||
expect(
|
||||
buildFileTreeProjectionSearchRequestMeta({
|
||||
workspaceId: "ws_1",
|
||||
query: " rust ",
|
||||
maxResults: 500,
|
||||
}),
|
||||
).toEqual({
|
||||
query: "rust",
|
||||
maxResults: 80,
|
||||
maxResultsRule: "matches_only_before_ancestor_completion",
|
||||
ancestorCompletion: "include_all_ancestors_after_match_truncation",
|
||||
ordering: "kernel_file_tree_preorder",
|
||||
emptyStateText: "没有匹配结果",
|
||||
asyncVisibility: {
|
||||
source: "kernel.project_view",
|
||||
requestKey: "ws_1:rust",
|
||||
},
|
||||
coveredResourceKinds: ["index", "asset", "asset_folder", "mindmap", "book", "pdf"],
|
||||
});
|
||||
});
|
||||
|
||||
it("搜索请求会把 maxResults 限制在 sidebar 使用的稳定上限内", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
result: {
|
||||
projectionId: "kernel_projection:file_tree:root",
|
||||
projection: "file_tree",
|
||||
rootNodeId: null,
|
||||
items: [],
|
||||
edges: [],
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await fetchKernelFileTreeProjection({
|
||||
workspaceId: "ws_1",
|
||||
query: "rust",
|
||||
maxResults: 500,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/tree/projections/file?workspaceId=ws_1&query=rust&maxResults=80",
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("失败时透出服务端错误消息", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import type { KernelFileTreeProjection } from "@/lib/kernel-file-tree";
|
||||
|
||||
export const FILE_TREE_SEARCH_MAX_RESULTS = 80;
|
||||
export const FILE_TREE_SEARCH_EMPTY_STATE_TEXT = "没有匹配结果";
|
||||
|
||||
export type FileTreeProjectionSearchRequestMeta = {
|
||||
query: string | null;
|
||||
maxResults: number | null;
|
||||
maxResultsRule: "matches_only_before_ancestor_completion";
|
||||
ancestorCompletion: "include_all_ancestors_after_match_truncation";
|
||||
ordering: "kernel_file_tree_preorder";
|
||||
emptyStateText: typeof FILE_TREE_SEARCH_EMPTY_STATE_TEXT;
|
||||
asyncVisibility: {
|
||||
source: "kernel.project_view";
|
||||
requestKey: string | null;
|
||||
};
|
||||
coveredResourceKinds: ["index", "asset", "asset_folder", "mindmap", "book", "pdf"];
|
||||
};
|
||||
|
||||
export type FetchKernelFileTreeProjectionInput = {
|
||||
workspaceId: string;
|
||||
rootNodeId?: string | null;
|
||||
@@ -8,6 +25,38 @@ export type FetchKernelFileTreeProjectionInput = {
|
||||
maxResults?: number | null;
|
||||
};
|
||||
|
||||
function normalizeQuery(value: string | null | undefined): string | null {
|
||||
const query = value?.trim() ?? "";
|
||||
return query.length > 0 ? query : null;
|
||||
}
|
||||
|
||||
function normalizeMaxResults(value: number | null | undefined): number | null {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
return Math.min(FILE_TREE_SEARCH_MAX_RESULTS, Math.max(1, Math.floor(value)));
|
||||
}
|
||||
|
||||
export function buildFileTreeProjectionSearchRequestMeta(
|
||||
input: Pick<FetchKernelFileTreeProjectionInput, "workspaceId" | "query" | "maxResults">,
|
||||
): FileTreeProjectionSearchRequestMeta {
|
||||
const query = normalizeQuery(input.query);
|
||||
const maxResults = normalizeMaxResults(input.maxResults);
|
||||
return {
|
||||
query,
|
||||
maxResults,
|
||||
maxResultsRule: "matches_only_before_ancestor_completion",
|
||||
ancestorCompletion: "include_all_ancestors_after_match_truncation",
|
||||
ordering: "kernel_file_tree_preorder",
|
||||
emptyStateText: FILE_TREE_SEARCH_EMPTY_STATE_TEXT,
|
||||
asyncVisibility: {
|
||||
source: "kernel.project_view",
|
||||
requestKey: query ? `${input.workspaceId}:${query}` : null,
|
||||
},
|
||||
coveredResourceKinds: ["index", "asset", "asset_folder", "mindmap", "book", "pdf"],
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchKernelFileTreeProjection(
|
||||
input: FetchKernelFileTreeProjectionInput,
|
||||
): Promise<KernelFileTreeProjection> {
|
||||
@@ -20,12 +69,13 @@ export async function fetchKernelFileTreeProjection(
|
||||
if (typeof input.depth === "number" && Number.isFinite(input.depth)) {
|
||||
params.set("depth", String(input.depth));
|
||||
}
|
||||
const query = input.query?.trim();
|
||||
const query = normalizeQuery(input.query);
|
||||
if (query) {
|
||||
params.set("query", query);
|
||||
}
|
||||
if (typeof input.maxResults === "number" && Number.isFinite(input.maxResults)) {
|
||||
params.set("maxResults", String(Math.max(1, Math.floor(input.maxResults))));
|
||||
const maxResults = normalizeMaxResults(input.maxResults);
|
||||
if (maxResults != null) {
|
||||
params.set("maxResults", String(maxResults));
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/tree/projections/file?${params.toString()}`, {
|
||||
|
||||
Reference in New Issue
Block a user