


























@@ -1,14 +1,25 @@
11import { randomUUID } from "node:crypto";
2-import { describe, expect, it } from "vitest";
2+import fs from "node:fs";
3+import os from "node:os";
4+import path from "node:path";
5+import { afterEach, describe, expect, it } from "vitest";
36import { getSkillsSnapshotVersion, resetSkillsRefreshForTest } from "../agents/skills/refresh.js";
7+import type { OpenClawConfig } from "../config/types.openclaw.js";
8+import type { NodeRegistry } from "../gateway/node-registry.js";
49import {
510getRemoteSkillEligibility,
611recordRemoteNodeBins,
712recordRemoteNodeInfo,
813removeRemoteNodeInfo,
14+refreshRemoteNodeBins,
15+setSkillsRemoteRegistry,
916} from "./skills-remote.js";
10171118describe("skills-remote", () => {
19+afterEach(() => {
20+setSkillsRemoteRegistry(null);
21+});
22+1223it("removes disconnected nodes from remote skill eligibility", () => {
1324const nodeId = `node-${randomUUID()}`;
1425const bin = `bin-${randomUUID()}`;
@@ -134,4 +145,156 @@ describe("skills-remote", () => {
134145removeRemoteNodeInfo(nodeId);
135146}
136147});
148+149+it("does not expose bins for nodes that only have cached paired metadata", () => {
150+const nodeId = `node-${randomUUID()}`;
151+const bin = `bin-${randomUUID()}`;
152+try {
153+recordRemoteNodeBins(nodeId, [bin]);
154+155+expect(getRemoteSkillEligibility()?.hasBin(bin) ?? false).toBe(false);
156+} finally {
157+removeRemoteNodeInfo(nodeId);
158+}
159+});
160+161+it("clears stale bins when a connected node probe times out", async () => {
162+await resetSkillsRefreshForTest();
163+const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-remote-skills-"));
164+const nodeId = `node-${randomUUID()}`;
165+const bin = `bin-${randomUUID()}`;
166+try {
167+fs.mkdirSync(path.join(workspaceDir, "remote-skill"), { recursive: true });
168+fs.writeFileSync(
169+path.join(workspaceDir, "remote-skill", "SKILL.md"),
170+[
171+"---",
172+"name: remote-skill",
173+"description: Needs a remote bin",
174+`metadata: { "openclaw": { "os": ["darwin"], "requires": { "bins": ["${bin}"] } } }`,
175+"---",
176+"# Remote Skill",
177+"",
178+].join("\n"),
179+);
180+const cfg = {
181+agents: {
182+defaults: {
183+workspace: workspaceDir,
184+},
185+},
186+} satisfies OpenClawConfig;
187+const invokeCalls: string[] = [];
188+setSkillsRemoteRegistry({
189+listConnected: () => [],
190+get: () => undefined,
191+invoke: async (params: { command: string }) => {
192+invokeCalls.push(params.command);
193+return {
194+ok: false,
195+error: { code: "TIMEOUT", message: "node invoke timed out" },
196+};
197+},
198+} as unknown as NodeRegistry);
199+recordRemoteNodeInfo({
200+ nodeId,
201+displayName: "Remote Mac",
202+platform: "darwin",
203+commands: ["system.run", "system.which"],
204+});
205+recordRemoteNodeBins(nodeId, [bin]);
206+const before = getSkillsSnapshotVersion(workspaceDir);
207+208+await refreshRemoteNodeBins({
209+ nodeId,
210+platform: "darwin",
211+commands: ["system.run", "system.which"],
212+ cfg,
213+timeoutMs: 10,
214+});
215+216+expect(invokeCalls).toEqual(["system.which"]);
217+expect(getRemoteSkillEligibility()?.hasBin(bin) ?? false).toBe(false);
218+expect(getSkillsSnapshotVersion(workspaceDir)).toBeGreaterThan(before);
219+} finally {
220+removeRemoteNodeInfo(nodeId);
221+fs.rmSync(workspaceDir, { recursive: true, force: true });
222+}
223+});
224+225+it("coalesces overlapping bin probes for the same node", async () => {
226+const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-remote-skills-"));
227+const nodeId = `node-${randomUUID()}`;
228+const bin = `bin-${randomUUID()}`;
229+let invokeCount = 0;
230+let releaseProbe!: () => void;
231+const probeStarted = new Promise<void>((resolve) => {
232+setSkillsRemoteRegistry({
233+listConnected: () => [],
234+get: () => undefined,
235+invoke: async () => {
236+invokeCount += 1;
237+resolve();
238+await new Promise<void>((release) => {
239+releaseProbe = release;
240+});
241+return {
242+ok: false,
243+error: { code: "TIMEOUT", message: "node invoke timed out" },
244+};
245+},
246+} as unknown as NodeRegistry);
247+});
248+try {
249+fs.mkdirSync(path.join(workspaceDir, "remote-skill"), { recursive: true });
250+fs.writeFileSync(
251+path.join(workspaceDir, "remote-skill", "SKILL.md"),
252+[
253+"---",
254+"name: remote-skill",
255+"description: Needs a remote bin",
256+`metadata: { "openclaw": { "os": ["darwin"], "requires": { "bins": ["${bin}"] } } }`,
257+"---",
258+"# Remote Skill",
259+"",
260+].join("\n"),
261+);
262+const cfg = {
263+agents: {
264+defaults: {
265+workspace: workspaceDir,
266+},
267+},
268+} satisfies OpenClawConfig;
269+recordRemoteNodeInfo({
270+ nodeId,
271+displayName: "Remote Mac",
272+platform: "darwin",
273+commands: ["system.run", "system.which"],
274+});
275+276+const first = refreshRemoteNodeBins({
277+ nodeId,
278+platform: "darwin",
279+commands: ["system.run", "system.which"],
280+ cfg,
281+timeoutMs: 10,
282+});
283+await probeStarted;
284+const second = refreshRemoteNodeBins({
285+ nodeId,
286+platform: "darwin",
287+commands: ["system.run", "system.which"],
288+ cfg,
289+timeoutMs: 10,
290+});
291+releaseProbe();
292+293+await Promise.all([first, second]);
294+expect(invokeCount).toBe(1);
295+} finally {
296+removeRemoteNodeInfo(nodeId);
297+fs.rmSync(workspaceDir, { recursive: true, force: true });
298+}
299+});
137300});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。