mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 02:30:24 +00:00
refactor(core): dedupe infra, media, pairing, and plugin helpers
This commit is contained in:
@@ -24,6 +24,81 @@ afterEach(async () => {
|
||||
await tempDirs.cleanup();
|
||||
});
|
||||
|
||||
async function expectWriteOpenRaceIsBlocked(params: {
|
||||
slotPath: string;
|
||||
outsideDir: string;
|
||||
runWrite: () => Promise<void>;
|
||||
}): Promise<void> {
|
||||
await withRealpathSymlinkRebindRace({
|
||||
shouldFlip: (realpathInput) => realpathInput.endsWith(path.join("slot", "target.txt")),
|
||||
symlinkPath: params.slotPath,
|
||||
symlinkTarget: params.outsideDir,
|
||||
timing: "before-realpath",
|
||||
run: async () => {
|
||||
await expect(params.runWrite()).rejects.toMatchObject({ code: "outside-workspace" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function expectSymlinkWriteRaceRejectsOutside(params: {
|
||||
slotPath: string;
|
||||
outsideDir: string;
|
||||
runWrite: (relativePath: string) => Promise<void>;
|
||||
}): Promise<void> {
|
||||
const relativePath = path.join("slot", "target.txt");
|
||||
await expectWriteOpenRaceIsBlocked({
|
||||
slotPath: params.slotPath,
|
||||
outsideDir: params.outsideDir,
|
||||
runWrite: async () => await params.runWrite(relativePath),
|
||||
});
|
||||
}
|
||||
|
||||
async function withOutsideHardlinkAlias(params: {
|
||||
aliasPath: string;
|
||||
run: (outsideFile: string) => Promise<void>;
|
||||
}): Promise<void> {
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
|
||||
const outsideFile = path.join(outside, "outside.txt");
|
||||
await fs.writeFile(outsideFile, "outside");
|
||||
try {
|
||||
try {
|
||||
await fs.link(outsideFile, params.aliasPath);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "EXDEV") {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await params.run(outsideFile);
|
||||
} finally {
|
||||
await fs.rm(params.aliasPath, { force: true });
|
||||
await fs.rm(outsideFile, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function setupSymlinkWriteRaceFixture(options?: { seedInsideTarget?: boolean }): Promise<{
|
||||
root: string;
|
||||
outside: string;
|
||||
slot: string;
|
||||
outsideTarget: string;
|
||||
}> {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
const inside = path.join(root, "inside");
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
|
||||
await fs.mkdir(inside, { recursive: true });
|
||||
if (options?.seedInsideTarget) {
|
||||
await fs.writeFile(path.join(inside, "target.txt"), "inside");
|
||||
}
|
||||
const outsideTarget = path.join(outside, "target.txt");
|
||||
await fs.writeFile(outsideTarget, "X".repeat(4096));
|
||||
const slot = path.join(root, "slot");
|
||||
await createRebindableDirectoryAlias({
|
||||
aliasPath: slot,
|
||||
targetPath: inside,
|
||||
});
|
||||
return { root, outside, slot, outsideTarget };
|
||||
}
|
||||
|
||||
describe("fs-safe", () => {
|
||||
it("reads a local file safely", async () => {
|
||||
const dir = await tempDirs.make("openclaw-fs-safe-");
|
||||
@@ -147,29 +222,18 @@ describe("fs-safe", () => {
|
||||
|
||||
it.runIf(process.platform !== "win32")("blocks hardlink aliases under root", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
|
||||
const outsideFile = path.join(outside, "outside.txt");
|
||||
const hardlinkPath = path.join(root, "link.txt");
|
||||
await fs.writeFile(outsideFile, "outside");
|
||||
try {
|
||||
try {
|
||||
await fs.link(outsideFile, hardlinkPath);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "EXDEV") {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await expect(
|
||||
openFileWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: "link.txt",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "invalid-path" });
|
||||
} finally {
|
||||
await fs.rm(hardlinkPath, { force: true });
|
||||
await fs.rm(outsideFile, { force: true });
|
||||
}
|
||||
await withOutsideHardlinkAlias({
|
||||
aliasPath: hardlinkPath,
|
||||
run: async () => {
|
||||
await expect(
|
||||
openFileWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: "link.txt",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "invalid-path" });
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("writes a file within root safely", async () => {
|
||||
@@ -245,99 +309,58 @@ describe("fs-safe", () => {
|
||||
|
||||
it.runIf(process.platform !== "win32")("rejects writing through hardlink aliases", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
|
||||
const outsideFile = path.join(outside, "outside.txt");
|
||||
const hardlinkPath = path.join(root, "alias.txt");
|
||||
await fs.writeFile(outsideFile, "outside");
|
||||
try {
|
||||
try {
|
||||
await fs.link(outsideFile, hardlinkPath);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "EXDEV") {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await expect(
|
||||
writeFileWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: "alias.txt",
|
||||
data: "pwned",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "invalid-path" });
|
||||
await expect(fs.readFile(outsideFile, "utf8")).resolves.toBe("outside");
|
||||
} finally {
|
||||
await fs.rm(hardlinkPath, { force: true });
|
||||
await fs.rm(outsideFile, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not truncate out-of-root file when symlink retarget races write open", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
const inside = path.join(root, "inside");
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
|
||||
await fs.mkdir(inside, { recursive: true });
|
||||
const insideTarget = path.join(inside, "target.txt");
|
||||
const outsideTarget = path.join(outside, "target.txt");
|
||||
await fs.writeFile(insideTarget, "inside");
|
||||
await fs.writeFile(outsideTarget, "X".repeat(4096));
|
||||
const slot = path.join(root, "slot");
|
||||
await createRebindableDirectoryAlias({
|
||||
aliasPath: slot,
|
||||
targetPath: inside,
|
||||
});
|
||||
|
||||
await withRealpathSymlinkRebindRace({
|
||||
shouldFlip: (realpathInput) => realpathInput.endsWith(path.join("slot", "target.txt")),
|
||||
symlinkPath: slot,
|
||||
symlinkTarget: outside,
|
||||
timing: "before-realpath",
|
||||
run: async () => {
|
||||
await withOutsideHardlinkAlias({
|
||||
aliasPath: hardlinkPath,
|
||||
run: async (outsideFile) => {
|
||||
await expect(
|
||||
writeFileWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: path.join("slot", "target.txt"),
|
||||
data: "new-content",
|
||||
mkdir: false,
|
||||
relativePath: "alias.txt",
|
||||
data: "pwned",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "outside-workspace" });
|
||||
).rejects.toMatchObject({ code: "invalid-path" });
|
||||
await expect(fs.readFile(outsideFile, "utf8")).resolves.toBe("outside");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not truncate out-of-root file when symlink retarget races write open", async () => {
|
||||
const { root, outside, slot, outsideTarget } = await setupSymlinkWriteRaceFixture({
|
||||
seedInsideTarget: true,
|
||||
});
|
||||
|
||||
await expectSymlinkWriteRaceRejectsOutside({
|
||||
slotPath: slot,
|
||||
outsideDir: outside,
|
||||
runWrite: async (relativePath) =>
|
||||
await writeFileWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath,
|
||||
data: "new-content",
|
||||
mkdir: false,
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("X".repeat(4096));
|
||||
});
|
||||
|
||||
it("does not clobber out-of-root file when symlink retarget races write-from-path open", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
const inside = path.join(root, "inside");
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
|
||||
const { root, outside, slot, outsideTarget } = await setupSymlinkWriteRaceFixture();
|
||||
const sourceDir = await tempDirs.make("openclaw-fs-safe-source-");
|
||||
const sourcePath = path.join(sourceDir, "source.txt");
|
||||
await fs.writeFile(sourcePath, "new-content");
|
||||
await fs.mkdir(inside, { recursive: true });
|
||||
const outsideTarget = path.join(outside, "target.txt");
|
||||
await fs.writeFile(outsideTarget, "X".repeat(4096));
|
||||
const slot = path.join(root, "slot");
|
||||
await createRebindableDirectoryAlias({
|
||||
aliasPath: slot,
|
||||
targetPath: inside,
|
||||
});
|
||||
|
||||
await withRealpathSymlinkRebindRace({
|
||||
shouldFlip: (realpathInput) => realpathInput.endsWith(path.join("slot", "target.txt")),
|
||||
symlinkPath: slot,
|
||||
symlinkTarget: outside,
|
||||
timing: "before-realpath",
|
||||
run: async () => {
|
||||
await expect(
|
||||
writeFileFromPathWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: path.join("slot", "target.txt"),
|
||||
sourcePath,
|
||||
mkdir: false,
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "outside-workspace" });
|
||||
},
|
||||
await expectSymlinkWriteRaceRejectsOutside({
|
||||
slotPath: slot,
|
||||
outsideDir: outside,
|
||||
runWrite: async (relativePath) =>
|
||||
await writeFileFromPathWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath,
|
||||
sourcePath,
|
||||
mkdir: false,
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("X".repeat(4096));
|
||||
|
||||
@@ -15,6 +15,17 @@ function passthroughPluginAutoEnable(config: unknown) {
|
||||
return { config, changes: [] as unknown[] };
|
||||
}
|
||||
|
||||
function createTelegramPlugin() {
|
||||
return {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram" },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: mocks.getChannelPlugin,
|
||||
normalizeChannelId: normalizeChannel,
|
||||
@@ -39,6 +50,13 @@ import { resolveOutboundTarget } from "./targets.js";
|
||||
|
||||
describe("resolveOutboundTarget channel resolution", () => {
|
||||
let registrySeq = 0;
|
||||
const resolveTelegramTarget = () =>
|
||||
resolveOutboundTarget({
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
cfg: { channels: { telegram: { botToken: "test-token" } } },
|
||||
mode: "explicit",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
registrySeq += 1;
|
||||
@@ -48,39 +66,20 @@ describe("resolveOutboundTarget channel resolution", () => {
|
||||
});
|
||||
|
||||
it("recovers telegram plugin resolution so announce delivery does not fail with Unsupported channel: telegram", () => {
|
||||
const telegramPlugin = {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram" },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
};
|
||||
const telegramPlugin = createTelegramPlugin();
|
||||
mocks.getChannelPlugin
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(telegramPlugin)
|
||||
.mockReturnValue(telegramPlugin);
|
||||
|
||||
const result = resolveOutboundTarget({
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
cfg: { channels: { telegram: { botToken: "test-token" } } },
|
||||
mode: "explicit",
|
||||
});
|
||||
const result = resolveTelegramTarget();
|
||||
|
||||
expect(result).toEqual({ ok: true, to: "123456" });
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries bootstrap on subsequent resolve when the first bootstrap attempt fails", () => {
|
||||
const telegramPlugin = {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram" },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
};
|
||||
const telegramPlugin = createTelegramPlugin();
|
||||
mocks.getChannelPlugin
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(undefined)
|
||||
@@ -93,18 +92,8 @@ describe("resolveOutboundTarget channel resolution", () => {
|
||||
})
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
const first = resolveOutboundTarget({
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
cfg: { channels: { telegram: { botToken: "test-token" } } },
|
||||
mode: "explicit",
|
||||
});
|
||||
const second = resolveOutboundTarget({
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
cfg: { channels: { telegram: { botToken: "test-token" } } },
|
||||
mode: "explicit",
|
||||
});
|
||||
const first = resolveTelegramTarget();
|
||||
const second = resolveTelegramTarget();
|
||||
|
||||
expect(first.ok).toBe(false);
|
||||
expect(second).toEqual({ ok: true, to: "123456" });
|
||||
|
||||
Reference in New Issue
Block a user