@spzhongwin/skill-logger-plugin 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,165 +1,165 @@
1
- import { describe, it, beforeEach, afterEach } from "node:test";
2
- import assert from "node:assert/strict";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import os from "node:os";
6
- import { ConfigSync, parseSkillVersion } from "./config-sync.ts";
7
- import type { SkillStandardConfig } from "./types.ts";
8
-
9
- let dir: string;
10
- let extensionsDir: string;
11
- let syncStatePath: string;
12
-
13
- const sampleConfigs: SkillStandardConfig[] = [
14
- { skillName: "foo", version: "1", functions: [{ id: "run", name: "运行", match: { type: "script", script: "scripts/run.py" } }] },
15
- { skillName: "bar", version: "1", functions: [{ id: "x", name: "x", match: { type: "tool", toolName: "bar_x" } }] },
16
- ];
17
-
18
- async function writeSkill(name: string, withScripts = true): Promise<void> {
19
- const skillDir = path.join(extensionsDir, "myext", "skills", name);
20
- await fs.mkdir(skillDir, { recursive: true });
21
- await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\n---\n# ${name}\n`);
22
- if (withScripts) {
23
- await fs.mkdir(path.join(skillDir, "scripts"), { recursive: true });
24
- await fs.writeFile(path.join(skillDir, "scripts", "run.py"), "print(1)\n");
25
- }
26
- }
27
-
28
- function makeSync() {
29
- return new ConfigSync({
30
- paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
31
- getConfig: () => ({}), // 无 platformBaseUrl → 用静态桩
32
- sampleConfigs,
33
- // 测试注入固定扫描目录,跳过 openclaw.json 解析;skill 实际写在 extensionsDir 下。
34
- resolveSkillDirs: () => [path.join(os.tmpdir(), "openclaw-test-skills")],
35
- });
36
- }
37
-
38
- beforeEach(async () => {
39
- dir = path.join(os.tmpdir(), `slp-sync-${Date.now()}-${Math.random().toString(36).slice(2)}`);
40
- extensionsDir = path.join(dir, "extensions");
41
- syncStatePath = path.join(dir, "sync.json");
42
- await fs.mkdir(extensionsDir, { recursive: true });
43
- });
44
-
45
- afterEach(async () => {
46
- await fs.rm(dir, { recursive: true, force: true });
47
- });
48
-
49
- describe("parseSkillVersion(中英文兼容、纯正则)", () => {
50
- const fm = (body: string) => `---\n${body}\n---\n# skill\n`;
51
- it("英文 version", () => assert.equal(parseSkillVersion(fm("name: x\nversion: 1.2.3")), "1.2.3"));
52
- it("大写 Version", () => assert.equal(parseSkillVersion(fm("Version: 2.0.0")), "2.0.0"));
53
- it("中文 版本号 + 全角冒号", () => assert.equal(parseSkillVersion(fm("版本号:3.1")), "3.1"));
54
- it("中文 版本 + 半角冒号", () => assert.equal(parseSkillVersion(fm("版本: v4")), "v4"));
55
- it("去除包裹引号", () => assert.equal(parseSkillVersion(fm('version: "1.0.0"')), "1.0.0"));
56
- it("去除行尾注释", () => assert.equal(parseSkillVersion(fm("version: 1.0.0 # latest")), "1.0.0"));
57
- it("无版本字段返回 undefined", () => assert.equal(parseSkillVersion(fm("name: x")), undefined));
58
- it("无 frontmatter 时全文宽松查找", () => assert.equal(parseSkillVersion("版本号: 9.9\n正文"), "9.9"));
59
- it("版本写在正文单独一行 → 能取到", () =>
60
- assert.equal(parseSkillVersion(fm("name: x") + "\n版本号:7.7\n"), "7.7"));
61
- it("Markdown 标题 ## Version 不误取(# 前缀)", () =>
62
- assert.equal(parseSkillVersion(fm("name: x") + "\n## Version: 5.5\n"), undefined));
63
- it("frontmatter 与正文都有版本 → frontmatter 优先", () =>
64
- assert.equal(parseSkillVersion(fm("version: 1.0.0") + "\nversion: 2.0.0\n"), "1.0.0"));
65
- });
66
-
67
- describe("ConfigSync.scanInstalledSkills", () => {
68
- it("扫描出 SKILL.md 并解析 name", async () => {
69
- await writeSkill("foo");
70
- const skills = await makeSync().scanInstalledSkills();
71
- const foo = skills.find((s) => s.name === "foo");
72
- assert.ok(foo);
73
- assert.ok(foo!.signature.length > 0);
74
- });
75
-
76
- it("跳过 node_modules", async () => {
77
- const nm = path.join(extensionsDir, "myext", "node_modules", "pkg", "skills", "ghost");
78
- await fs.mkdir(nm, { recursive: true });
79
- await fs.writeFile(path.join(nm, "SKILL.md"), `---\nname: ghost\n---\n`);
80
- const skills = await makeSync().scanInstalledSkills();
81
- assert.equal(skills.find((s) => s.name === "ghost"), undefined);
82
- });
83
- });
84
-
85
- describe("ConfigSync.diffAgainstState", () => {
86
- it("初次全部需拉取", async () => {
87
- await writeSkill("foo");
88
- const cs = makeSync();
89
- const installed = await cs.scanInstalledSkills();
90
- const { toFetch } = cs.diffAgainstState(installed);
91
- assert.equal(toFetch.length, 1);
92
- assert.equal(toFetch[0].name, "foo");
93
- });
94
- });
95
-
96
- describe("ConfigSync.pullConfigs(静态桩)", () => {
97
- it("仅返回请求到的 skill(ok=true)", async () => {
98
- const got = await makeSync().pullConfigs([{ name: "foo" }]);
99
- assert.equal(got.ok, true);
100
- assert.equal(got.configs.length, 1);
101
- assert.equal(got.configs[0].skillName, "foo");
102
- });
103
- });
104
-
105
- describe("ConfigSync 多 workspace 映射与过期检测", () => {
106
- async function writeSkillIn(wsDir: string, name: string, version: string): Promise<string> {
107
- const skillDir = path.join(wsDir, "skills", name);
108
- await fs.mkdir(skillDir, { recursive: true });
109
- await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\nversion: ${version}\n---\n`);
110
- return skillDir;
111
- }
112
-
113
- it("同名 skill 在多个 workspace → 映射记录全部副本,去重后只返回一份", async () => {
114
- const wsA = path.join(dir, "workspace-a");
115
- const wsB = path.join(dir, "workspace-b");
116
- await writeSkillIn(wsA, "demo", "1.0.0");
117
- await writeSkillIn(wsB, "demo", "1.0.0");
118
- const cs = new ConfigSync({
119
- paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
120
- getConfig: () => ({}),
121
- sampleConfigs,
122
- resolveSkillDirs: () => [path.join(wsA, "skills"), path.join(wsB, "skills")],
123
- });
124
- const list = await cs.scanInstalledSkills();
125
- assert.equal(list.filter((s) => s.name === "demo").length, 1); // 去重
126
- assert.equal(cs.getInstallations().get("demo")?.length, 2); // 映射保留两份
127
- });
128
-
129
- it("detectOutdated:本地版本落后于平台最新版本时被标记", async () => {
130
- const wsA = path.join(dir, "workspace-a");
131
- await writeSkillIn(wsA, "demo", "1.0.0");
132
- const cs = new ConfigSync({
133
- paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
134
- getConfig: () => ({}),
135
- // 平台最新版本 2.0.0(桩无 latestVersion 字段 → 回退用 version 当最新)
136
- sampleConfigs: [{ skillName: "demo", version: "2.0.0", functions: [] }],
137
- resolveSkillDirs: () => [path.join(wsA, "skills")],
138
- });
139
- await cs.checkVersionsAndUpdate(); // 刷新 latestVersions + 检测(无 updater 注入则不覆盖文件)
140
- const outdated = cs.detectOutdated();
141
- assert.equal(outdated.length, 1);
142
- assert.equal(outdated[0].skillName, "demo");
143
- assert.equal(outdated[0].localVersion, "1.0.0");
144
- assert.equal(outdated[0].latestVersion, "2.0.0");
145
-
146
- // agent↔skill 映射应本地持久化到 sync.json
147
- const state = JSON.parse(await fs.readFile(syncStatePath, "utf-8"));
148
- assert.ok(state.installations.demo, "sync.json 应含 installations 映射");
149
- assert.equal(state.installations.demo[0].version, "1.0.0");
150
- });
151
- });
152
-
153
- describe("ConfigSync.reconcile 端到端", () => {
154
- it("对账后索引含该 skill 功能点,并持久化", async () => {
155
- await writeSkill("foo");
156
- const cs = makeSync();
157
- await cs.reconcile();
158
- // foo 的 run.py 应进入 script 索引
159
- assert.ok(cs.getIndex().scriptByBasename.has("run.py"));
160
- // 持久化文件存在且含 foo
161
- const state = JSON.parse(await fs.readFile(syncStatePath, "utf-8"));
162
- assert.ok(state.active.foo);
163
- assert.ok(state.configPool["foo@unknown"]);
164
- });
165
- });
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { ConfigSync, parseSkillVersion } from "./config-sync.ts";
7
+ import type { SkillStandardConfig } from "./types.ts";
8
+
9
+ let dir: string;
10
+ let extensionsDir: string;
11
+ let syncStatePath: string;
12
+
13
+ const sampleConfigs: SkillStandardConfig[] = [
14
+ { skillName: "foo", version: "1", functions: [{ id: "run", name: "运行", match: { type: "script", script: "scripts/run.py" } }] },
15
+ { skillName: "bar", version: "1", functions: [{ id: "x", name: "x", match: { type: "tool", toolName: "bar_x" } }] },
16
+ ];
17
+
18
+ async function writeSkill(name: string, withScripts = true): Promise<void> {
19
+ const skillDir = path.join(extensionsDir, "myext", "skills", name);
20
+ await fs.mkdir(skillDir, { recursive: true });
21
+ await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\n---\n# ${name}\n`);
22
+ if (withScripts) {
23
+ await fs.mkdir(path.join(skillDir, "scripts"), { recursive: true });
24
+ await fs.writeFile(path.join(skillDir, "scripts", "run.py"), "print(1)\n");
25
+ }
26
+ }
27
+
28
+ function makeSync() {
29
+ return new ConfigSync({
30
+ paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
31
+ getConfig: () => ({}), // 无 platformBaseUrl → 用静态桩
32
+ sampleConfigs,
33
+ // 测试注入固定扫描目录,跳过 openclaw.json 解析;skill 实际写在 extensionsDir 下。
34
+ resolveSkillDirs: () => [path.join(os.tmpdir(), "openclaw-test-skills")],
35
+ });
36
+ }
37
+
38
+ beforeEach(async () => {
39
+ dir = path.join(os.tmpdir(), `slp-sync-${Date.now()}-${Math.random().toString(36).slice(2)}`);
40
+ extensionsDir = path.join(dir, "extensions");
41
+ syncStatePath = path.join(dir, "sync.json");
42
+ await fs.mkdir(extensionsDir, { recursive: true });
43
+ });
44
+
45
+ afterEach(async () => {
46
+ await fs.rm(dir, { recursive: true, force: true });
47
+ });
48
+
49
+ describe("parseSkillVersion(中英文兼容、纯正则)", () => {
50
+ const fm = (body: string) => `---\n${body}\n---\n# skill\n`;
51
+ it("英文 version", () => assert.equal(parseSkillVersion(fm("name: x\nversion: 1.2.3")), "1.2.3"));
52
+ it("大写 Version", () => assert.equal(parseSkillVersion(fm("Version: 2.0.0")), "2.0.0"));
53
+ it("中文 版本号 + 全角冒号", () => assert.equal(parseSkillVersion(fm("版本号:3.1")), "3.1"));
54
+ it("中文 版本 + 半角冒号", () => assert.equal(parseSkillVersion(fm("版本: v4")), "v4"));
55
+ it("去除包裹引号", () => assert.equal(parseSkillVersion(fm('version: "1.0.0"')), "1.0.0"));
56
+ it("去除行尾注释", () => assert.equal(parseSkillVersion(fm("version: 1.0.0 # latest")), "1.0.0"));
57
+ it("无版本字段返回 undefined", () => assert.equal(parseSkillVersion(fm("name: x")), undefined));
58
+ it("无 frontmatter 时全文宽松查找", () => assert.equal(parseSkillVersion("版本号: 9.9\n正文"), "9.9"));
59
+ it("版本写在正文单独一行 → 能取到", () =>
60
+ assert.equal(parseSkillVersion(fm("name: x") + "\n版本号:7.7\n"), "7.7"));
61
+ it("Markdown 标题 ## Version 不误取(# 前缀)", () =>
62
+ assert.equal(parseSkillVersion(fm("name: x") + "\n## Version: 5.5\n"), undefined));
63
+ it("frontmatter 与正文都有版本 → frontmatter 优先", () =>
64
+ assert.equal(parseSkillVersion(fm("version: 1.0.0") + "\nversion: 2.0.0\n"), "1.0.0"));
65
+ });
66
+
67
+ describe("ConfigSync.scanInstalledSkills", () => {
68
+ it("扫描出 SKILL.md 并解析 name", async () => {
69
+ await writeSkill("foo");
70
+ const skills = await makeSync().scanInstalledSkills();
71
+ const foo = skills.find((s) => s.name === "foo");
72
+ assert.ok(foo);
73
+ assert.ok(foo!.signature.length > 0);
74
+ });
75
+
76
+ it("跳过 node_modules", async () => {
77
+ const nm = path.join(extensionsDir, "myext", "node_modules", "pkg", "skills", "ghost");
78
+ await fs.mkdir(nm, { recursive: true });
79
+ await fs.writeFile(path.join(nm, "SKILL.md"), `---\nname: ghost\n---\n`);
80
+ const skills = await makeSync().scanInstalledSkills();
81
+ assert.equal(skills.find((s) => s.name === "ghost"), undefined);
82
+ });
83
+ });
84
+
85
+ describe("ConfigSync.diffAgainstState", () => {
86
+ it("初次全部需拉取", async () => {
87
+ await writeSkill("foo");
88
+ const cs = makeSync();
89
+ const installed = await cs.scanInstalledSkills();
90
+ const { toFetch } = cs.diffAgainstState(installed);
91
+ assert.equal(toFetch.length, 1);
92
+ assert.equal(toFetch[0].name, "foo");
93
+ });
94
+ });
95
+
96
+ describe("ConfigSync.pullConfigs(静态桩)", () => {
97
+ it("仅返回请求到的 skill(ok=true)", async () => {
98
+ const got = await makeSync().pullConfigs([{ name: "foo" }]);
99
+ assert.equal(got.ok, true);
100
+ assert.equal(got.configs.length, 1);
101
+ assert.equal(got.configs[0].skillName, "foo");
102
+ });
103
+ });
104
+
105
+ describe("ConfigSync 多 workspace 映射与过期检测", () => {
106
+ async function writeSkillIn(wsDir: string, name: string, version: string): Promise<string> {
107
+ const skillDir = path.join(wsDir, "skills", name);
108
+ await fs.mkdir(skillDir, { recursive: true });
109
+ await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\nversion: ${version}\n---\n`);
110
+ return skillDir;
111
+ }
112
+
113
+ it("同名 skill 在多个 workspace → 映射记录全部副本,去重后只返回一份", async () => {
114
+ const wsA = path.join(dir, "workspace-a");
115
+ const wsB = path.join(dir, "workspace-b");
116
+ await writeSkillIn(wsA, "demo", "1.0.0");
117
+ await writeSkillIn(wsB, "demo", "1.0.0");
118
+ const cs = new ConfigSync({
119
+ paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
120
+ getConfig: () => ({}),
121
+ sampleConfigs,
122
+ resolveSkillDirs: () => [path.join(wsA, "skills"), path.join(wsB, "skills")],
123
+ });
124
+ const list = await cs.scanInstalledSkills();
125
+ assert.equal(list.filter((s) => s.name === "demo").length, 1); // 去重
126
+ assert.equal(cs.getInstallations().get("demo")?.length, 2); // 映射保留两份
127
+ });
128
+
129
+ it("detectOutdated:本地版本落后于平台最新版本时被标记", async () => {
130
+ const wsA = path.join(dir, "workspace-a");
131
+ await writeSkillIn(wsA, "demo", "1.0.0");
132
+ const cs = new ConfigSync({
133
+ paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
134
+ getConfig: () => ({}),
135
+ // 平台最新版本 2.0.0(桩无 latestVersion 字段 → 回退用 version 当最新)
136
+ sampleConfigs: [{ skillName: "demo", version: "2.0.0", functions: [] }],
137
+ resolveSkillDirs: () => [path.join(wsA, "skills")],
138
+ });
139
+ await cs.checkVersionsAndUpdate(); // 刷新 latestVersions + 检测(无 updater 注入则不覆盖文件)
140
+ const outdated = cs.detectOutdated();
141
+ assert.equal(outdated.length, 1);
142
+ assert.equal(outdated[0].skillName, "demo");
143
+ assert.equal(outdated[0].localVersion, "1.0.0");
144
+ assert.equal(outdated[0].latestVersion, "2.0.0");
145
+
146
+ // agent↔skill 映射应本地持久化到 sync.json
147
+ const state = JSON.parse(await fs.readFile(syncStatePath, "utf-8"));
148
+ assert.ok(state.installations.demo, "sync.json 应含 installations 映射");
149
+ assert.equal(state.installations.demo[0].version, "1.0.0");
150
+ });
151
+ });
152
+
153
+ describe("ConfigSync.reconcile 端到端", () => {
154
+ it("对账后索引含该 skill 功能点,并持久化", async () => {
155
+ await writeSkill("foo");
156
+ const cs = makeSync();
157
+ await cs.reconcile();
158
+ // foo 的 run.py 应进入 script 索引
159
+ assert.ok(cs.getIndex().scriptByBasename.has("run.py"));
160
+ // 持久化文件存在且含 foo
161
+ const state = JSON.parse(await fs.readFile(syncStatePath, "utf-8"));
162
+ assert.ok(state.active.foo);
163
+ assert.ok(state.configPool["foo@unknown"]);
164
+ });
165
+ });