Bot settings could export a bot as JSON, but nothing could read that file back, so an export was not a way to recover or move a bot. bots.import creates a new bot from an export manifest: profile and instructions, bot memory, home files and routines. Team Computer paths are rewritten to the new bot's workspace, unsafe paths reject the whole import, imported routines start paused and one-shot schedules are left out. History is not replayed; the bot starts its own conversation. A failed import removes the half-created bot. The New bot panel gets an import button that takes the exported file. Claude-Session: https://claude.ai/code/session_01Sbaz56zNFZBJ1whrpasMMs Co-authored-by: Claude <noreply@anthropic.com>
75 lines
2.3 KiB
TypeScript
75 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { importableRoutines, relativeImportedFiles, safeImportPath } from "./bot-import.js";
|
|
|
|
describe("safeImportPath", () => {
|
|
it("normalizes separators and dot segments", () => {
|
|
expect(safeImportPath("notes\\./result.txt")).toBe("notes/result.txt");
|
|
expect(safeImportPath("MEMORY.md")).toBe("MEMORY.md");
|
|
});
|
|
|
|
it("rejects paths that could leave the bot home", () => {
|
|
expect(safeImportPath("../secrets")).toBeNull();
|
|
expect(safeImportPath("notes/../../x")).toBeNull();
|
|
expect(safeImportPath("/etc/passwd")).toBeNull();
|
|
expect(safeImportPath("C:\\Windows")).toBeNull();
|
|
expect(safeImportPath("./")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("relativeImportedFiles", () => {
|
|
it("drops the old Team Computer workspace prefix", () => {
|
|
expect(
|
|
relativeImportedFiles([
|
|
{ path: "bots/old-id/notes/result.txt", content: "a" },
|
|
{ path: "bots/old-id/README.md", content: "b" },
|
|
]),
|
|
).toEqual([
|
|
{ path: "notes/result.txt", content: "a" },
|
|
{ path: "README.md", content: "b" },
|
|
]);
|
|
});
|
|
|
|
it("keeps Private Computer paths as they are", () => {
|
|
const files = [
|
|
{ path: "notes/result.txt", content: "a" },
|
|
{ path: "bots/other/file.txt", content: "b" },
|
|
];
|
|
expect(relativeImportedFiles(files)).toEqual(files);
|
|
});
|
|
|
|
it("keeps paths when files span several workspaces", () => {
|
|
const files = [
|
|
{ path: "bots/a/x.txt", content: "a" },
|
|
{ path: "bots/b/y.txt", content: "b" },
|
|
];
|
|
expect(relativeImportedFiles(files)).toEqual(files);
|
|
});
|
|
|
|
it("refuses the whole set when one path is unsafe", () => {
|
|
expect(
|
|
relativeImportedFiles([
|
|
{ path: "ok.txt", content: "a" },
|
|
{ path: "../escape.txt", content: "b" },
|
|
]),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("accepts an export without files", () => {
|
|
expect(relativeImportedFiles([])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("importableRoutines", () => {
|
|
it("leaves out one-shot schedules", () => {
|
|
const routines = [
|
|
{ name: "daily", crons: ["0 9 * * *"] },
|
|
{ name: "once", crons: ["@once"] },
|
|
{ name: "webhook", crons: [] },
|
|
];
|
|
expect(importableRoutines(routines).map((routine) => routine.name)).toEqual([
|
|
"daily",
|
|
"webhook",
|
|
]);
|
|
});
|
|
});
|