mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-11 07:19:03 +00:00
* feat(artifacts): inline editing for text artifacts in the panel
Add a PUT /api/threads/{id}/artifacts/{path} endpoint that atomically
replaces an existing UTF-8 text file under /mnt/user-data/outputs after
verifying its SHA-256 revision. Active runs conflict (409); binary,
symlink, oversized, and non-output paths are rejected.
Frontend: edit/save/discard buttons, draft state with conflict detection,
CodeEditor onChange/onSave, loader SHA-256 from ETag, i18n, beforeunload guard.
Backend: PUT endpoint with thread reservation, atomic temp-file replacement,
sandbox sync for non-mounted providers, rollback on failure, ETag on GET.
Tests: 8 backend + 1 blocking-IO + 3 frontend test files.
* fix(artifacts): scope replacement permissions and release sandboxes
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
import { describe, expect, it } from "@rstest/core";
|
|
|
|
import {
|
|
canEditOpenedArtifact,
|
|
createArtifactDraft,
|
|
reconcileArtifactDraft,
|
|
} from "@/core/artifacts/editing";
|
|
|
|
describe("artifact draft reconciliation", () => {
|
|
it("adopts refreshed content while the draft is clean", () => {
|
|
const draft = createArtifactDraft("/mnt/user-data/outputs/report.md");
|
|
const loaded = reconcileArtifactDraft(draft, {
|
|
content: "first",
|
|
sha256: "a".repeat(64),
|
|
});
|
|
const refreshed = reconcileArtifactDraft(loaded, {
|
|
content: "second",
|
|
sha256: "b".repeat(64),
|
|
});
|
|
|
|
expect(refreshed.baselineContent).toBe("second");
|
|
expect(refreshed.draftContent).toBe("second");
|
|
expect(refreshed.conflict).toBe(false);
|
|
});
|
|
|
|
it("preserves a dirty draft and marks a conflict after a remote change", () => {
|
|
const loaded = reconcileArtifactDraft(
|
|
createArtifactDraft("/mnt/user-data/outputs/report.md"),
|
|
{ content: "first", sha256: "a".repeat(64) },
|
|
);
|
|
const edited = { ...loaded, draftContent: "my changes" };
|
|
const refreshed = reconcileArtifactDraft(edited, {
|
|
content: "agent changes",
|
|
sha256: "b".repeat(64),
|
|
});
|
|
|
|
expect(refreshed.draftContent).toBe("my changes");
|
|
expect(refreshed.baselineContent).toBe("first");
|
|
expect(refreshed.conflict).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("opened artifact edit eligibility", () => {
|
|
const editable = {
|
|
filepath: "/mnt/user-data/outputs/report.md",
|
|
isCodeFile: true,
|
|
isWriteFile: false,
|
|
isSkillFile: false,
|
|
isMock: false,
|
|
hasRevision: true,
|
|
isStaticWebsite: false,
|
|
};
|
|
|
|
it("allows an opened formal output rendered by the code editor", () => {
|
|
expect(canEditOpenedArtifact(editable)).toBe(true);
|
|
});
|
|
|
|
it("does not expose editing for paths outside formal output artifacts", () => {
|
|
expect(
|
|
canEditOpenedArtifact({
|
|
...editable,
|
|
filepath: "/mnt/user-data/workspace/report.md",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("does not expose editing for temporary or non-editor previews", () => {
|
|
expect(canEditOpenedArtifact({ ...editable, isWriteFile: true })).toBe(
|
|
false,
|
|
);
|
|
expect(canEditOpenedArtifact({ ...editable, isCodeFile: false })).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|