mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-05 12:28:37 +00:00
fix(artifacts): scope full previews to their thread (#4634)
This commit is contained in:
parent
c0f1cfef69
commit
7025ccee40
@ -268,6 +268,7 @@ This section accumulates work toward the **2.1.0** milestone
|
||||
|
||||
### Fixed
|
||||
|
||||
- **artifacts:** Keep explicit full-file loading scoped to the source thread, so a same-path artifact in another conversation keeps its 1 MiB preview.
|
||||
- **sandbox:** `SandboxAuditMiddleware` no longer blocks ordinary command
|
||||
substitution that only captures output. The rule now judges *position* instead
|
||||
of matching any `$(`: `x=$(curl url)`, `echo $(curl url)`, an argument, and a
|
||||
|
||||
@ -18,8 +18,13 @@ export function useArtifactContent({
|
||||
return filepath.startsWith("write-file:");
|
||||
}, [filepath]);
|
||||
const { thread, isMock } = useThread();
|
||||
const [fullFilepath, setFullFilepath] = useState<string | null>(null);
|
||||
const fullContentRequested = fullFilepath === filepath;
|
||||
const [fullContentSelection, setFullContentSelection] = useState<{
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
} | null>(null);
|
||||
const fullContentRequested =
|
||||
fullContentSelection?.filepath === filepath &&
|
||||
fullContentSelection.threadId === threadId;
|
||||
const content = useMemo(() => {
|
||||
if (isWriteFile) {
|
||||
return loadArtifactContentFromToolCall({ url: filepath, thread });
|
||||
@ -54,8 +59,8 @@ export function useArtifactContent({
|
||||
}, [enabled, isWriteFile, refetch, thread.isLoading]);
|
||||
|
||||
const loadFullContent = useCallback(() => {
|
||||
setFullFilepath(filepath);
|
||||
}, [filepath]);
|
||||
setFullContentSelection({ filepath, threadId });
|
||||
}, [filepath, threadId]);
|
||||
|
||||
return {
|
||||
content: isWriteFile ? content : data?.content,
|
||||
|
||||
102
frontend/tests/unit/core/artifacts/hooks.dom.test.tsx
Normal file
102
frontend/tests/unit/core/artifacts/hooks.dom.test.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
rs.mock("@/components/workspace/messages/context", () => ({
|
||||
useThread: rs.fn(),
|
||||
}));
|
||||
|
||||
rs.mock("@/core/artifacts/loader", () => ({
|
||||
loadArtifactContent: rs.fn(),
|
||||
loadArtifactContentFromToolCall: rs.fn(),
|
||||
}));
|
||||
|
||||
import { useThread } from "@/components/workspace/messages/context";
|
||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||
import { loadArtifactContent } from "@/core/artifacts/loader";
|
||||
|
||||
const mockedUseThread = rs.mocked(useThread);
|
||||
const mockedLoadArtifactContent = rs.mocked(loadArtifactContent);
|
||||
const filepath = "/mnt/user-data/outputs/report.md";
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return function QueryWrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe("useArtifactContent", () => {
|
||||
beforeEach(() => {
|
||||
mockedUseThread.mockReturnValue({
|
||||
thread: { isLoading: false, messages: [] },
|
||||
isMock: false,
|
||||
} as never);
|
||||
mockedLoadArtifactContent.mockImplementation(async ({ full }) => ({
|
||||
content: full ? "complete report" : "preview",
|
||||
url: filepath,
|
||||
sha256: undefined,
|
||||
truncated: !full,
|
||||
previewBytes: full ? 15 : 7,
|
||||
totalBytes: 15,
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockedUseThread.mockReset();
|
||||
mockedLoadArtifactContent.mockReset();
|
||||
});
|
||||
|
||||
it("keeps a full-content request scoped to its thread", async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ threadId }: { threadId: string }) =>
|
||||
useArtifactContent({ filepath, threadId, enabled: true }),
|
||||
{
|
||||
initialProps: { threadId: "thread-a" },
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedLoadArtifactContent).toHaveBeenLastCalledWith({
|
||||
filepath,
|
||||
threadId: "thread-a",
|
||||
isMock: false,
|
||||
full: false,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.loadFullContent();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedLoadArtifactContent).toHaveBeenLastCalledWith({
|
||||
filepath,
|
||||
threadId: "thread-a",
|
||||
isMock: false,
|
||||
full: true,
|
||||
});
|
||||
});
|
||||
|
||||
rerender({ threadId: "thread-b" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedLoadArtifactContent).toHaveBeenLastCalledWith({
|
||||
filepath,
|
||||
threadId: "thread-b",
|
||||
isMock: false,
|
||||
full: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user