wangchuan 3.0.0 → 4.0.0
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.
- package/README.md +146 -174
- package/README.zh-CN.md +254 -0
- package/dist/bin/wangchuan.d.ts +10 -2
- package/dist/bin/wangchuan.d.ts.map +1 -1
- package/dist/bin/wangchuan.js +30 -233
- package/dist/bin/wangchuan.js.map +1 -1
- package/dist/src/agents/codex.d.ts +9 -0
- package/dist/src/agents/codex.d.ts.map +1 -0
- package/dist/src/agents/codex.js +20 -0
- package/dist/src/agents/codex.js.map +1 -0
- package/dist/src/agents/index.d.ts.map +1 -1
- package/dist/src/agents/index.js +2 -0
- package/dist/src/agents/index.js.map +1 -1
- package/dist/src/commands/completions.js +2 -2
- package/dist/src/commands/completions.js.map +1 -1
- package/dist/src/commands/doctor.d.ts +17 -6
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +233 -65
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/list.d.ts.map +1 -1
- package/dist/src/commands/list.js +1 -0
- package/dist/src/commands/list.js.map +1 -1
- package/dist/src/commands/status.d.ts +20 -3
- package/dist/src/commands/status.d.ts.map +1 -1
- package/dist/src/commands/status.js +321 -51
- package/dist/src/commands/status.js.map +1 -1
- package/dist/src/commands/sync.d.ts.map +1 -1
- package/dist/src/commands/sync.js +51 -0
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/template.js +1 -1
- package/dist/src/commands/template.js.map +1 -1
- package/dist/src/core/config.d.ts +9 -9
- package/dist/src/core/config.d.ts.map +1 -1
- package/dist/src/core/config.js +11 -11
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/crypto.d.ts +10 -10
- package/dist/src/core/crypto.d.ts.map +1 -1
- package/dist/src/core/crypto.js +19 -19
- package/dist/src/core/crypto.js.map +1 -1
- package/dist/src/core/git.d.ts +2 -2
- package/dist/src/core/git.js +9 -9
- package/dist/src/core/git.js.map +1 -1
- package/dist/src/core/json-field.d.ts +5 -5
- package/dist/src/core/json-field.d.ts.map +1 -1
- package/dist/src/core/json-field.js +5 -5
- package/dist/src/core/json-field.js.map +1 -1
- package/dist/src/core/sync.d.ts +9 -9
- package/dist/src/core/sync.js +47 -47
- package/dist/src/core/sync.js.map +1 -1
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +24 -10
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/types.d.ts +24 -23
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +3 -3
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/linediff.d.ts +6 -5
- package/dist/src/utils/linediff.d.ts.map +1 -1
- package/dist/src/utils/linediff.js +10 -9
- package/dist/src/utils/linediff.js.map +1 -1
- package/dist/test/crypto.test.d.ts +1 -1
- package/dist/test/crypto.test.js +16 -16
- package/dist/test/crypto.test.js.map +1 -1
- package/dist/test/json-field.test.d.ts +1 -1
- package/dist/test/json-field.test.js +13 -13
- package/dist/test/json-field.test.js.map +1 -1
- package/dist/test/sync.test.d.ts +8 -8
- package/dist/test/sync.test.js +122 -113
- package/dist/test/sync.test.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +40 -53
- package/skill/claude/SKILL.md +10 -41
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-field.test.js","sourceRoot":"","sources":["../../test/json-field.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEtD,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"json-field.test.js","sourceRoot":"","sources":["../../test/json-field.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEtD,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,GAAG,GAAG;YACV,UAAU,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;YAC7C,WAAW,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE;YAC9B,WAAW,EAAE,EAAE;SAChB,CAAC;QACF,MAAM,MAAM,GAAG,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QAC7F,MAAM,MAAM,GAAG,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;QACnE,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC;QAClE,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,MAAM,GAAG,SAAS,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAC3E,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,QAAQ,GAAG;YACf,UAAU,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE;YAC7B,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;YACxB,WAAW,EAAE,EAAE;YACf,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;SACpC,CAAC;QACF,oCAAoC;QACpC,MAAM,SAAS,GAAG,SAAS,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;QACpE,+BAA+B;QAC9B,SAAqC,CAAC,UAAU,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;QACtE,0CAA0C;QAC1C,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QAC1D,6CAA6C;QAC7C,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE;YAC7B,UAAU,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE;YAC7B,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;YACxB,WAAW,EAAE,EAAE;YACf,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;SACpC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/test/sync.test.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sync.test.ts —
|
|
2
|
+
* sync.test.ts — Sync engine unit tests
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - buildFileEntries
|
|
6
|
-
* - distributeShared
|
|
7
|
-
* - stageToRepo + restoreFromRepo
|
|
8
|
-
* - jsonFields
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
4
|
+
* Coverage:
|
|
5
|
+
* - buildFileEntries entry generation (agents + shared + jsonFields + dedup)
|
|
6
|
+
* - distributeShared (skills distribution, MCP distribution)
|
|
7
|
+
* - stageToRepo + restoreFromRepo round-trip consistency
|
|
8
|
+
* - jsonFields field-level extraction/merge without data loss
|
|
9
|
+
* - New skill/MCP cross-agent sharing
|
|
10
|
+
* - Behavior after skill deletion (add-only model)
|
|
11
11
|
*/
|
|
12
12
|
export {};
|
|
13
13
|
//# sourceMappingURL=sync.test.d.ts.map
|
package/dist/test/sync.test.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sync.test.ts —
|
|
2
|
+
* sync.test.ts — Sync engine unit tests
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - buildFileEntries
|
|
6
|
-
* - distributeShared
|
|
7
|
-
* - stageToRepo + restoreFromRepo
|
|
8
|
-
* - jsonFields
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
4
|
+
* Coverage:
|
|
5
|
+
* - buildFileEntries entry generation (agents + shared + jsonFields + dedup)
|
|
6
|
+
* - distributeShared (skills distribution, MCP distribution)
|
|
7
|
+
* - stageToRepo + restoreFromRepo round-trip consistency
|
|
8
|
+
* - jsonFields field-level extraction/merge without data loss
|
|
9
|
+
* - New skill/MCP cross-agent sharing
|
|
10
|
+
* - Behavior after skill deletion (add-only model)
|
|
11
11
|
*/
|
|
12
12
|
import { describe, it, before, after } from 'node:test';
|
|
13
13
|
import assert from 'node:assert/strict';
|
|
@@ -16,7 +16,7 @@ import os from 'os';
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { syncEngine, buildFileEntries } from '../src/core/sync.js';
|
|
18
18
|
import { cryptoEngine } from '../src/core/crypto.js';
|
|
19
|
-
// ──
|
|
19
|
+
// ── Test utilities ──────────────────────────────────────────────────
|
|
20
20
|
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'wc-sync-'));
|
|
21
21
|
const KEY = path.join(TMP, 'master.key');
|
|
22
22
|
const REPO = path.join(TMP, 'repo');
|
|
@@ -26,6 +26,7 @@ const WS_GE = path.join(TMP, 'gemini');
|
|
|
26
26
|
const WS_CB = path.join(TMP, 'codebuddy');
|
|
27
27
|
const WS_WB = path.join(TMP, 'workbuddy');
|
|
28
28
|
const WS_CU = path.join(TMP, 'cursor');
|
|
29
|
+
const WS_CX = path.join(TMP, 'codex');
|
|
29
30
|
function mkCfg(overrides) {
|
|
30
31
|
return {
|
|
31
32
|
repo: 'git@example.com:test.git',
|
|
@@ -97,6 +98,14 @@ function mkCfg(overrides) {
|
|
|
97
98
|
{ src: 'cli-config.json', fields: ['permissions', 'model', 'enabledPlugins'], repoName: 'cli-config-sync.json', encrypt: true },
|
|
98
99
|
],
|
|
99
100
|
},
|
|
101
|
+
codex: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
workspacePath: WS_CX,
|
|
104
|
+
syncFiles: [
|
|
105
|
+
{ src: 'MEMORY.md', encrypt: true },
|
|
106
|
+
{ src: 'instructions.md', encrypt: false },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
100
109
|
},
|
|
101
110
|
},
|
|
102
111
|
shared: {
|
|
@@ -121,7 +130,7 @@ function mkCfg(overrides) {
|
|
|
121
130
|
...overrides,
|
|
122
131
|
};
|
|
123
132
|
}
|
|
124
|
-
/**
|
|
133
|
+
/** Write file, auto-creating parent dirs */
|
|
125
134
|
function writeFile(filePath, content) {
|
|
126
135
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
127
136
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
@@ -139,7 +148,7 @@ function prepareEmptyMcpFiles() {
|
|
|
139
148
|
// ── Setup / Teardown ────────────────────────────────────────────────
|
|
140
149
|
before(() => {
|
|
141
150
|
cryptoEngine.generateKey(KEY);
|
|
142
|
-
for (const d of [REPO, WS_OC, WS_CL, WS_GE, WS_CB, WS_WB, WS_CU]) {
|
|
151
|
+
for (const d of [REPO, WS_OC, WS_CL, WS_GE, WS_CB, WS_WB, WS_CU, WS_CX]) {
|
|
143
152
|
fs.mkdirSync(d, { recursive: true });
|
|
144
153
|
}
|
|
145
154
|
// Pre-create empty MCP configs for all shared MCP source agents
|
|
@@ -148,9 +157,9 @@ before(() => {
|
|
|
148
157
|
after(() => {
|
|
149
158
|
fs.rmSync(TMP, { recursive: true, force: true });
|
|
150
159
|
});
|
|
151
|
-
// ── buildFileEntries
|
|
160
|
+
// ── buildFileEntries tests ──────────────────────────────────────────
|
|
152
161
|
describe('buildFileEntries', () => {
|
|
153
|
-
it('
|
|
162
|
+
it('generates agents/<name>/ prefixed entries for each agent', () => {
|
|
154
163
|
const cfg = mkCfg();
|
|
155
164
|
const entries = buildFileEntries(cfg);
|
|
156
165
|
const ocEntries = entries.filter(e => e.agentName === 'openclaw');
|
|
@@ -160,7 +169,7 @@ describe('buildFileEntries', () => {
|
|
|
160
169
|
assert.ok(clEntries.length >= 1);
|
|
161
170
|
assert.ok(clEntries.every(e => e.repoRel.startsWith('agents/claude/') || e.repoRel.startsWith('shared/')));
|
|
162
171
|
});
|
|
163
|
-
it('jsonFields
|
|
172
|
+
it('jsonFields entries carry jsonExtract metadata', () => {
|
|
164
173
|
const cfg = mkCfg();
|
|
165
174
|
const entries = buildFileEntries(cfg);
|
|
166
175
|
const jfEntries = entries.filter(e => e.jsonExtract);
|
|
@@ -170,14 +179,14 @@ describe('buildFileEntries', () => {
|
|
|
170
179
|
assert.deepStrictEqual(claudeJf.jsonExtract.fields, ['mcpServers']);
|
|
171
180
|
assert.ok(claudeJf.encrypt);
|
|
172
181
|
});
|
|
173
|
-
it('--agent
|
|
182
|
+
it('--agent filter returns only specified agent entries', () => {
|
|
174
183
|
const cfg = mkCfg();
|
|
175
184
|
const entries = buildFileEntries(cfg, undefined, 'gemini');
|
|
176
185
|
assert.ok(entries.length > 0);
|
|
177
186
|
assert.ok(entries.every(e => e.agentName === 'gemini'));
|
|
178
187
|
});
|
|
179
|
-
it('shared skills
|
|
180
|
-
//
|
|
188
|
+
it('shared skills entries carry shared identifier', () => {
|
|
189
|
+
// Create skill file so walkDir can find it
|
|
181
190
|
writeFile(path.join(WS_CL, 'skills', 'test.md'), '# test skill');
|
|
182
191
|
const cfg = mkCfg();
|
|
183
192
|
const entries = buildFileEntries(cfg);
|
|
@@ -185,41 +194,41 @@ describe('buildFileEntries', () => {
|
|
|
185
194
|
assert.ok(sharedSkills.length >= 1);
|
|
186
195
|
fs.rmSync(path.join(WS_CL, 'skills'), { recursive: true, force: true });
|
|
187
196
|
});
|
|
188
|
-
it('repoRel
|
|
197
|
+
it('repoRel dedup: same-name skill across agents keeps only first', () => {
|
|
189
198
|
writeFile(path.join(WS_CL, 'skills', 'dup.md'), 'claude version');
|
|
190
199
|
writeFile(path.join(WS_OC, 'skills', 'dup.md'), 'openclaw version');
|
|
191
200
|
const cfg = mkCfg();
|
|
192
201
|
const entries = buildFileEntries(cfg);
|
|
193
202
|
const dupEntries = entries.filter(e => e.repoRel === path.join('shared', 'skills', 'dup.md'));
|
|
194
|
-
assert.equal(dupEntries.length, 1, '
|
|
195
|
-
//
|
|
203
|
+
assert.equal(dupEntries.length, 1, 'same-name skill should be deduped to 1 entry');
|
|
204
|
+
// Cleanup
|
|
196
205
|
fs.rmSync(path.join(WS_CL, 'skills'), { recursive: true, force: true });
|
|
197
206
|
fs.rmSync(path.join(WS_OC, 'skills'), { recursive: true, force: true });
|
|
198
207
|
});
|
|
199
208
|
});
|
|
200
|
-
// ── stageToRepo + restoreFromRepo
|
|
201
|
-
describe('stageToRepo → restoreFromRepo
|
|
202
|
-
it('
|
|
209
|
+
// ── stageToRepo + restoreFromRepo round-trip tests ──────────────────
|
|
210
|
+
describe('stageToRepo → restoreFromRepo round-trip', () => {
|
|
211
|
+
it('whole-file sync: push then pull content matches', async () => {
|
|
203
212
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# 记忆内容 v1');
|
|
204
213
|
writeFile(path.join(WS_OC, 'SOUL.md'), '# 灵魂');
|
|
205
214
|
writeFile(path.join(WS_CL, 'CLAUDE.md'), '# Claude 指令');
|
|
206
215
|
const cfg = mkCfg({ shared: { skills: { sources: [] }, mcp: { sources: [] }, syncFiles: [] } });
|
|
207
216
|
const pushResult = await syncEngine.stageToRepo(cfg);
|
|
208
217
|
assert.ok(pushResult.synced.length >= 3);
|
|
209
|
-
//
|
|
218
|
+
// Repo files should exist
|
|
210
219
|
assert.ok(fs.existsSync(path.join(REPO, 'agents/openclaw/MEMORY.md.enc')));
|
|
211
220
|
assert.ok(fs.existsSync(path.join(REPO, 'agents/openclaw/SOUL.md')));
|
|
212
221
|
assert.ok(fs.existsSync(path.join(REPO, 'agents/claude/CLAUDE.md')));
|
|
213
|
-
//
|
|
222
|
+
// Simulate new environment: delete local files
|
|
214
223
|
fs.unlinkSync(path.join(WS_OC, 'MEMORY.md'));
|
|
215
224
|
fs.unlinkSync(path.join(WS_CL, 'CLAUDE.md'));
|
|
216
225
|
const pullResult = await syncEngine.restoreFromRepo(cfg);
|
|
217
226
|
assert.ok(pullResult.synced.length >= 3);
|
|
218
|
-
//
|
|
227
|
+
// Restored content should match
|
|
219
228
|
assert.equal(fs.readFileSync(path.join(WS_OC, 'MEMORY.md'), 'utf-8'), '# 记忆内容 v1');
|
|
220
229
|
assert.equal(fs.readFileSync(path.join(WS_CL, 'CLAUDE.md'), 'utf-8'), '# Claude 指令');
|
|
221
230
|
});
|
|
222
|
-
it('jsonFields
|
|
231
|
+
it('jsonFields: push extracts specified fields only, pull merge-back preserves others', async () => {
|
|
223
232
|
const claudeJson = {
|
|
224
233
|
mcpServers: { playwright: { type: 'stdio', cmd: 'npx' } },
|
|
225
234
|
tipsHistory: { tip1: 5 },
|
|
@@ -237,41 +246,41 @@ describe('stageToRepo → restoreFromRepo 往返', () => {
|
|
|
237
246
|
writeFile(path.join(WS_GE, 'settings.json'), JSON.stringify(geminiJson, null, 2));
|
|
238
247
|
const cfg = mkCfg({ shared: { skills: { sources: [] }, mcp: { sources: [] }, syncFiles: [] } });
|
|
239
248
|
await syncEngine.stageToRepo(cfg);
|
|
240
|
-
//
|
|
249
|
+
// Repo: Claude mcpServers.json.enc should only contain mcpServers
|
|
241
250
|
const claudeRepoFile = path.join(REPO, 'agents/claude/mcpServers.json.enc');
|
|
242
251
|
assert.ok(fs.existsSync(claudeRepoFile));
|
|
243
252
|
const decrypted = JSON.parse(cryptoEngine.decryptString(fs.readFileSync(claudeRepoFile, 'utf-8').trim(), KEY));
|
|
244
253
|
assert.deepStrictEqual(Object.keys(decrypted), ['mcpServers']);
|
|
245
|
-
assert.ok(!('tipsHistory' in decrypted), 'tipsHistory
|
|
246
|
-
//
|
|
254
|
+
assert.ok(!('tipsHistory' in decrypted), 'tipsHistory should not be extracted');
|
|
255
|
+
// Repo: Gemini settings-sync.json should only contain security + model
|
|
247
256
|
const geminiRepoFile = path.join(REPO, 'agents/gemini/settings-sync.json');
|
|
248
257
|
assert.ok(fs.existsSync(geminiRepoFile));
|
|
249
258
|
const geminiPartial = JSON.parse(fs.readFileSync(geminiRepoFile, 'utf-8'));
|
|
250
259
|
assert.deepStrictEqual(Object.keys(geminiPartial).sort(), ['model', 'security']);
|
|
251
|
-
//
|
|
260
|
+
// Simulate remote modification of mcpServers (add new server)
|
|
252
261
|
const modified = { mcpServers: { playwright: { type: 'stdio', cmd: 'npx' }, gongfeng: { type: 'sse' } } };
|
|
253
262
|
const enc = cryptoEngine.encryptString(JSON.stringify(modified, null, 2), KEY);
|
|
254
263
|
fs.writeFileSync(claudeRepoFile, enc, 'utf-8');
|
|
255
|
-
//
|
|
264
|
+
// Pull back
|
|
256
265
|
await syncEngine.restoreFromRepo(cfg);
|
|
257
|
-
// .claude.json
|
|
266
|
+
// .claude.json should preserve tipsHistory/numStartups/projects, mcpServers updated
|
|
258
267
|
const restored = JSON.parse(fs.readFileSync(path.join(WS_CL, '.claude.json'), 'utf-8'));
|
|
259
268
|
assert.deepStrictEqual(restored.mcpServers, modified.mcpServers);
|
|
260
|
-
assert.equal(restored.tipsHistory.tip1, 5, 'tipsHistory
|
|
261
|
-
assert.equal(restored.numStartups, 42, 'numStartups
|
|
269
|
+
assert.equal(restored.tipsHistory.tip1, 5, 'tipsHistory should not be destroyed');
|
|
270
|
+
assert.equal(restored.numStartups, 42, 'numStartups should not be destroyed');
|
|
262
271
|
assert.deepStrictEqual(restored.projects, { '/tmp/proj': { cost: 100 } });
|
|
263
272
|
});
|
|
264
273
|
});
|
|
265
|
-
// ── distributeShared
|
|
266
|
-
describe('skill
|
|
267
|
-
it('Claude
|
|
274
|
+
// ── distributeShared tests (verified indirectly via stageToRepo) ────
|
|
275
|
+
describe('cross-agent skill sharing', () => {
|
|
276
|
+
it('Claude adds skill → push distributes to OpenClaw', async () => {
|
|
268
277
|
writeFile(path.join(WS_CL, 'skills', 'new-skill.md'), '# New Skill');
|
|
269
|
-
// OpenClaw
|
|
278
|
+
// OpenClaw does not have this skill
|
|
270
279
|
const ocSkillPath = path.join(WS_OC, 'skills', 'new-skill.md');
|
|
271
280
|
if (fs.existsSync(ocSkillPath))
|
|
272
281
|
fs.unlinkSync(ocSkillPath);
|
|
273
282
|
const cfg = mkCfg();
|
|
274
|
-
//
|
|
283
|
+
// Prepare required workspace files
|
|
275
284
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# M');
|
|
276
285
|
writeFile(path.join(WS_OC, 'SOUL.md'), '# S');
|
|
277
286
|
writeFile(path.join(WS_CL, 'CLAUDE.md'), '# C');
|
|
@@ -279,26 +288,26 @@ describe('skill 跨 agent 共享', () => {
|
|
|
279
288
|
writeFile(path.join(WS_GE, 'settings.json'), '{"security":{},"model":{}}');
|
|
280
289
|
writeFile(path.join(WS_OC, 'config', 'mcporter.json'), '{"mcpServers":{}}');
|
|
281
290
|
await syncEngine.stageToRepo(cfg);
|
|
282
|
-
// distributeShared
|
|
283
|
-
assert.ok(fs.existsSync(ocSkillPath), 'OpenClaw
|
|
291
|
+
// distributeShared should copy skill to OpenClaw
|
|
292
|
+
assert.ok(fs.existsSync(ocSkillPath), 'OpenClaw should receive Claude new-skill.md');
|
|
284
293
|
assert.equal(fs.readFileSync(ocSkillPath, 'utf-8'), '# New Skill');
|
|
285
|
-
//
|
|
294
|
+
// Repo shared/skills/ should also have it
|
|
286
295
|
assert.ok(fs.existsSync(path.join(REPO, 'shared', 'skills', 'new-skill.md')));
|
|
287
296
|
});
|
|
288
|
-
it('
|
|
289
|
-
//
|
|
297
|
+
it('deleted skill is copied back from another agent (add-only)', async () => {
|
|
298
|
+
// Both agents have old-skill
|
|
290
299
|
writeFile(path.join(WS_CL, 'skills', 'old-skill.md'), '# Old');
|
|
291
300
|
writeFile(path.join(WS_OC, 'skills', 'old-skill.md'), '# Old');
|
|
292
|
-
//
|
|
301
|
+
// Delete from OpenClaw
|
|
293
302
|
fs.unlinkSync(path.join(WS_OC, 'skills', 'old-skill.md'));
|
|
294
303
|
const cfg = mkCfg();
|
|
295
304
|
await syncEngine.stageToRepo(cfg);
|
|
296
|
-
// distributeShared
|
|
297
|
-
assert.ok(fs.existsSync(path.join(WS_OC, 'skills', 'old-skill.md')), '
|
|
305
|
+
// distributeShared copies back from Claude to OpenClaw
|
|
306
|
+
assert.ok(fs.existsSync(path.join(WS_OC, 'skills', 'old-skill.md')), 'deleted skill is copied back from another agent');
|
|
298
307
|
});
|
|
299
308
|
});
|
|
300
|
-
describe('MCP
|
|
301
|
-
it('Claude
|
|
309
|
+
describe('cross-agent MCP sharing', () => {
|
|
310
|
+
it('Claude adds MCP server → push distributes to OpenClaw', async () => {
|
|
302
311
|
writeFile(path.join(WS_CL, '.claude.json'), JSON.stringify({
|
|
303
312
|
mcpServers: { playwright: { type: 'stdio' }, newServer: { type: 'sse' } },
|
|
304
313
|
tipsHistory: {},
|
|
@@ -312,12 +321,12 @@ describe('MCP 跨 agent 共享', () => {
|
|
|
312
321
|
writeFile(path.join(WS_CL, 'CLAUDE.md'), '# C');
|
|
313
322
|
const cfg = mkCfg();
|
|
314
323
|
await syncEngine.stageToRepo(cfg);
|
|
315
|
-
// OpenClaw
|
|
324
|
+
// OpenClaw mcporter.json should contain newServer
|
|
316
325
|
const ocMcp = JSON.parse(fs.readFileSync(path.join(WS_OC, 'config', 'mcporter.json'), 'utf-8'));
|
|
317
|
-
assert.ok('newServer' in ocMcp.mcpServers, 'OpenClaw
|
|
326
|
+
assert.ok('newServer' in ocMcp.mcpServers, 'OpenClaw should receive Claude newServer');
|
|
318
327
|
assert.deepStrictEqual(ocMcp.mcpServers.newServer, { type: 'sse' });
|
|
319
328
|
});
|
|
320
|
-
it('
|
|
329
|
+
it('MCP distribution does not overwrite existing config', async () => {
|
|
321
330
|
writeFile(path.join(WS_CL, '.claude.json'), JSON.stringify({
|
|
322
331
|
mcpServers: { playwright: { type: 'stdio', version: 'claude' } },
|
|
323
332
|
}, null, 2));
|
|
@@ -330,20 +339,20 @@ describe('MCP 跨 agent 共享', () => {
|
|
|
330
339
|
writeFile(path.join(WS_CL, 'CLAUDE.md'), '# C');
|
|
331
340
|
const cfg = mkCfg();
|
|
332
341
|
await syncEngine.stageToRepo(cfg);
|
|
333
|
-
//
|
|
342
|
+
// Both agents' playwright config should retain their own version
|
|
334
343
|
const clMcp = JSON.parse(fs.readFileSync(path.join(WS_CL, '.claude.json'), 'utf-8'));
|
|
335
344
|
const ocMcp = JSON.parse(fs.readFileSync(path.join(WS_OC, 'config', 'mcporter.json'), 'utf-8'));
|
|
336
345
|
assert.equal(clMcp.mcpServers.playwright.version, 'claude');
|
|
337
346
|
assert.equal(ocMcp.mcpServers.playwright.version, 'openclaw');
|
|
338
347
|
});
|
|
339
348
|
});
|
|
340
|
-
// ──
|
|
341
|
-
describe('
|
|
342
|
-
it('
|
|
349
|
+
// ── One-click restore (new server scenario) ─────────────────────────
|
|
350
|
+
describe('one-click restore on new environment', () => {
|
|
351
|
+
it('pull fully restores all agent configs when workspace is empty', async () => {
|
|
343
352
|
// Clean repo to avoid leftover data from previous tests
|
|
344
353
|
fs.rmSync(REPO, { recursive: true, force: true });
|
|
345
354
|
fs.mkdirSync(REPO, { recursive: true });
|
|
346
|
-
//
|
|
355
|
+
// Push a complete dataset to repo
|
|
347
356
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# 永久记忆');
|
|
348
357
|
writeFile(path.join(WS_OC, 'SOUL.md'), '# 灵魂身份');
|
|
349
358
|
writeFile(path.join(WS_CL, 'CLAUDE.md'), '# Claude 全局指令');
|
|
@@ -364,37 +373,37 @@ describe('新环境一键还原', () => {
|
|
|
364
373
|
writeFile(path.join(WS_CU, 'mcp.json'), '{"mcpServers":{}}');
|
|
365
374
|
const cfg = mkCfg();
|
|
366
375
|
await syncEngine.stageToRepo(cfg);
|
|
367
|
-
//
|
|
368
|
-
for (const d of [WS_OC, WS_CL, WS_GE, WS_CB, WS_WB, WS_CU]) {
|
|
376
|
+
// Simulate new server: clear all workspaces
|
|
377
|
+
for (const d of [WS_OC, WS_CL, WS_GE, WS_CB, WS_WB, WS_CU, WS_CX]) {
|
|
369
378
|
fs.rmSync(d, { recursive: true, force: true });
|
|
370
379
|
fs.mkdirSync(d, { recursive: true });
|
|
371
380
|
}
|
|
372
|
-
//
|
|
381
|
+
// Pull restore
|
|
373
382
|
const pullResult = await syncEngine.restoreFromRepo(cfg);
|
|
374
383
|
assert.ok(pullResult.synced.length >= 4);
|
|
375
|
-
// OpenClaw
|
|
384
|
+
// OpenClaw restored
|
|
376
385
|
assert.equal(fs.readFileSync(path.join(WS_OC, 'MEMORY.md'), 'utf-8'), '# 永久记忆');
|
|
377
386
|
assert.equal(fs.readFileSync(path.join(WS_OC, 'SOUL.md'), 'utf-8'), '# 灵魂身份');
|
|
378
|
-
// Claude
|
|
387
|
+
// Claude restored
|
|
379
388
|
assert.equal(fs.readFileSync(path.join(WS_CL, 'CLAUDE.md'), 'utf-8'), '# Claude 全局指令');
|
|
380
|
-
// Claude jsonFields merge-back
|
|
389
|
+
// Claude jsonFields merge-back (new env has no .claude.json → merge from empty {})
|
|
381
390
|
const clJson = JSON.parse(fs.readFileSync(path.join(WS_CL, '.claude.json'), 'utf-8'));
|
|
382
391
|
assert.deepStrictEqual(clJson.mcpServers, { server1: { cmd: 'test' } });
|
|
383
|
-
assert.ok(!('tipsHistory' in clJson), '
|
|
392
|
+
assert.ok(!('tipsHistory' in clJson), 'new env should not have tipsHistory');
|
|
384
393
|
// Gemini jsonFields merge-back
|
|
385
394
|
const geJson = JSON.parse(fs.readFileSync(path.join(WS_GE, 'settings.json'), 'utf-8'));
|
|
386
395
|
assert.deepStrictEqual(geJson.security, { auth: 'test' });
|
|
387
396
|
assert.deepStrictEqual(geJson.model, { name: 'gemini-2' });
|
|
388
|
-
assert.ok(!('cache' in geJson), '
|
|
389
|
-
//
|
|
390
|
-
assert.ok(fs.existsSync(path.join(WS_CL, 'skills', 'review.md')), 'Claude
|
|
391
|
-
assert.ok(fs.existsSync(path.join(WS_OC, 'skills', 'review.md')), 'OpenClaw
|
|
397
|
+
assert.ok(!('cache' in geJson), 'new env should not have cache');
|
|
398
|
+
// Shared skills distributed to all agents
|
|
399
|
+
assert.ok(fs.existsSync(path.join(WS_CL, 'skills', 'review.md')), 'Claude should restore skill');
|
|
400
|
+
assert.ok(fs.existsSync(path.join(WS_OC, 'skills', 'review.md')), 'OpenClaw should also receive shared skill');
|
|
392
401
|
});
|
|
393
402
|
});
|
|
394
|
-
// ──
|
|
395
|
-
describe('
|
|
396
|
-
it('
|
|
397
|
-
//
|
|
403
|
+
// ── Delete propagation tests ────────────────────────────────────────
|
|
404
|
+
describe('delete propagation', () => {
|
|
405
|
+
it('skill deleted from all agents → pruned from repo on push', async () => {
|
|
406
|
+
// Push a skill to repo first
|
|
398
407
|
writeFile(path.join(WS_CL, 'skills', 'obsolete.md'), '# 过时的 skill');
|
|
399
408
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# M');
|
|
400
409
|
writeFile(path.join(WS_OC, 'SOUL.md'), '# S');
|
|
@@ -405,7 +414,7 @@ describe('删除传播', () => {
|
|
|
405
414
|
const cfg = mkCfg();
|
|
406
415
|
await syncEngine.stageToRepo(cfg);
|
|
407
416
|
assert.ok(fs.existsSync(path.join(REPO, 'shared', 'skills', 'obsolete.md')));
|
|
408
|
-
//
|
|
417
|
+
// Delete from all agents (Claude has it, OpenClaw and CodeBuddy received it via distribution)
|
|
409
418
|
const clSkill = path.join(WS_CL, 'skills', 'obsolete.md');
|
|
410
419
|
const ocSkill = path.join(WS_OC, 'skills', 'obsolete.md');
|
|
411
420
|
const cbSkill = path.join(WS_CB, 'skills', 'obsolete.md');
|
|
@@ -415,14 +424,14 @@ describe('删除传播', () => {
|
|
|
415
424
|
fs.unlinkSync(ocSkill);
|
|
416
425
|
if (fs.existsSync(cbSkill))
|
|
417
426
|
fs.unlinkSync(cbSkill);
|
|
418
|
-
//
|
|
427
|
+
// Push again
|
|
419
428
|
const result = await syncEngine.stageToRepo(cfg);
|
|
420
|
-
//
|
|
421
|
-
assert.ok(!fs.existsSync(path.join(REPO, 'shared', 'skills', 'obsolete.md')), '
|
|
429
|
+
// Repo should be cleaned
|
|
430
|
+
assert.ok(!fs.existsSync(path.join(REPO, 'shared', 'skills', 'obsolete.md')), 'skill deleted from all agents should be pruned from repo');
|
|
422
431
|
assert.ok(result.deleted.includes(path.join('shared', 'skills', 'obsolete.md')));
|
|
423
432
|
});
|
|
424
|
-
it('repo
|
|
425
|
-
//
|
|
433
|
+
it('whole file in repo but absent from all local agents → pruned on push', async () => {
|
|
434
|
+
// Manually create a ghost file in repo (simulating old version leftovers)
|
|
426
435
|
writeFile(path.join(REPO, 'agents', 'openclaw', 'GHOST.md'), '幽灵');
|
|
427
436
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# M');
|
|
428
437
|
writeFile(path.join(WS_OC, 'SOUL.md'), '# S');
|
|
@@ -432,11 +441,11 @@ describe('删除传播', () => {
|
|
|
432
441
|
writeFile(path.join(WS_OC, 'config', 'mcporter.json'), '{"mcpServers":{}}');
|
|
433
442
|
const cfg = mkCfg();
|
|
434
443
|
const result = await syncEngine.stageToRepo(cfg);
|
|
435
|
-
assert.ok(!fs.existsSync(path.join(REPO, 'agents', 'openclaw', 'GHOST.md')), 'repo
|
|
444
|
+
assert.ok(!fs.existsSync(path.join(REPO, 'agents', 'openclaw', 'GHOST.md')), 'ghost file in repo should be pruned');
|
|
436
445
|
assert.ok(result.deleted.includes(path.join('agents', 'openclaw', 'GHOST.md')));
|
|
437
446
|
});
|
|
438
|
-
it('
|
|
439
|
-
//
|
|
447
|
+
it('deleted skill from repo is not redistributed on pull', async () => {
|
|
448
|
+
// Create skill and push
|
|
440
449
|
writeFile(path.join(WS_CL, 'skills', 'temp.md'), '# temp');
|
|
441
450
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# M');
|
|
442
451
|
writeFile(path.join(WS_OC, 'SOUL.md'), '# S');
|
|
@@ -446,7 +455,7 @@ describe('删除传播', () => {
|
|
|
446
455
|
writeFile(path.join(WS_OC, 'config', 'mcporter.json'), '{"mcpServers":{}}');
|
|
447
456
|
const cfg = mkCfg();
|
|
448
457
|
await syncEngine.stageToRepo(cfg);
|
|
449
|
-
//
|
|
458
|
+
// Delete from all agents and push (triggers repo cleanup)
|
|
450
459
|
for (const d of [WS_CL, WS_OC, WS_CB]) {
|
|
451
460
|
const f = path.join(d, 'skills', 'temp.md');
|
|
452
461
|
if (fs.existsSync(f))
|
|
@@ -454,19 +463,19 @@ describe('删除传播', () => {
|
|
|
454
463
|
}
|
|
455
464
|
await syncEngine.stageToRepo(cfg);
|
|
456
465
|
assert.ok(!fs.existsSync(path.join(REPO, 'shared', 'skills', 'temp.md')));
|
|
457
|
-
//
|
|
466
|
+
// Pull — temp.md should not appear in any agent
|
|
458
467
|
await syncEngine.restoreFromRepo(cfg);
|
|
459
|
-
assert.ok(!fs.existsSync(path.join(WS_CL, 'skills', 'temp.md')), 'Claude
|
|
460
|
-
assert.ok(!fs.existsSync(path.join(WS_OC, 'skills', 'temp.md')), 'OpenClaw
|
|
468
|
+
assert.ok(!fs.existsSync(path.join(WS_CL, 'skills', 'temp.md')), 'Claude should not restore deleted skill');
|
|
469
|
+
assert.ok(!fs.existsSync(path.join(WS_OC, 'skills', 'temp.md')), 'OpenClaw should not restore deleted skill');
|
|
461
470
|
});
|
|
462
471
|
});
|
|
463
|
-
// ── localOnly
|
|
464
|
-
describe('pull
|
|
465
|
-
it('
|
|
466
|
-
//
|
|
472
|
+
// ── localOnly detection tests ───────────────────────────────────────
|
|
473
|
+
describe('pull detects local-only files', () => {
|
|
474
|
+
it('files present locally but absent from repo are marked localOnly', async () => {
|
|
475
|
+
// Clear repo
|
|
467
476
|
fs.rmSync(REPO, { recursive: true, force: true });
|
|
468
477
|
fs.mkdirSync(REPO, { recursive: true });
|
|
469
|
-
//
|
|
478
|
+
// Local files exist
|
|
470
479
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# 本地记忆');
|
|
471
480
|
writeFile(path.join(WS_OC, 'SOUL.md'), '# 灵魂');
|
|
472
481
|
writeFile(path.join(WS_CL, 'CLAUDE.md'), '# 指令');
|
|
@@ -474,14 +483,14 @@ describe('pull 检测本地独有文件', () => {
|
|
|
474
483
|
writeFile(path.join(WS_GE, 'settings.json'), '{"security":{},"model":{}}');
|
|
475
484
|
const cfg = mkCfg({ shared: { skills: { sources: [] }, mcp: { sources: [] }, syncFiles: [] } });
|
|
476
485
|
const result = await syncEngine.restoreFromRepo(cfg);
|
|
477
|
-
//
|
|
478
|
-
assert.ok(result.localOnly.length >= 2,
|
|
479
|
-
assert.ok(result.localOnly.some(f => f.includes('MEMORY.md')), 'MEMORY.md
|
|
486
|
+
// Should detect local-only files
|
|
487
|
+
assert.ok(result.localOnly.length >= 2, `should have localOnly files, actual: ${result.localOnly.length}`);
|
|
488
|
+
assert.ok(result.localOnly.some(f => f.includes('MEMORY.md')), 'MEMORY.md should be marked as localOnly');
|
|
480
489
|
});
|
|
481
|
-
it('jsonFields
|
|
490
|
+
it('jsonFields localOnly no false positive — empty extracted fields not marked', async () => {
|
|
482
491
|
fs.rmSync(REPO, { recursive: true, force: true });
|
|
483
492
|
fs.mkdirSync(REPO, { recursive: true });
|
|
484
|
-
// .claude.json
|
|
493
|
+
// .claude.json exists but mcpServers field is empty
|
|
485
494
|
writeFile(path.join(WS_CL, '.claude.json'), JSON.stringify({
|
|
486
495
|
tipsHistory: { x: 1 },
|
|
487
496
|
numStartups: 5,
|
|
@@ -496,17 +505,17 @@ describe('pull 检测本地独有文件', () => {
|
|
|
496
505
|
writeFile(path.join(WS_CU, 'mcp.json'), '{}');
|
|
497
506
|
const cfg = mkCfg({ shared: { skills: { sources: [] }, mcp: { sources: [] }, syncFiles: [] } });
|
|
498
507
|
const result = await syncEngine.restoreFromRepo(cfg);
|
|
499
|
-
// .claude.json
|
|
500
|
-
assert.ok(!result.localOnly.some(f => f.includes('mcpServers')), 'mcpServers
|
|
508
|
+
// .claude.json has no mcpServers field, should not be marked as localOnly
|
|
509
|
+
assert.ok(!result.localOnly.some(f => f.includes('mcpServers')), 'empty mcpServers field should not be marked as localOnly');
|
|
501
510
|
});
|
|
502
511
|
});
|
|
503
|
-
// ── shared MCP
|
|
504
|
-
describe('
|
|
505
|
-
it('
|
|
506
|
-
//
|
|
512
|
+
// ── shared MCP cross-agent distribution (on pull) ───────────────────
|
|
513
|
+
describe('shared MCP cross-agent distribution on pull', () => {
|
|
514
|
+
it('MCP config pulled from repo is distributed to all agents', async () => {
|
|
515
|
+
// Clean repo
|
|
507
516
|
fs.rmSync(REPO, { recursive: true, force: true });
|
|
508
517
|
fs.mkdirSync(REPO, { recursive: true });
|
|
509
|
-
//
|
|
518
|
+
// Claude has MCP config, OpenClaw does not
|
|
510
519
|
writeFile(path.join(WS_CL, '.claude.json'), JSON.stringify({
|
|
511
520
|
mcpServers: { playwright: { type: 'stdio' } },
|
|
512
521
|
}, null, 2));
|
|
@@ -516,20 +525,20 @@ describe('pull 时 shared MCP 跨 agent 分发', () => {
|
|
|
516
525
|
writeFile(path.join(WS_OC, 'config', 'mcporter.json'), '{"mcpServers":{}}');
|
|
517
526
|
writeFile(path.join(WS_GE, 'settings.json'), '{"security":{},"model":{}}');
|
|
518
527
|
const cfg = mkCfg();
|
|
519
|
-
//
|
|
528
|
+
// Push first (create repo content)
|
|
520
529
|
await syncEngine.stageToRepo(cfg);
|
|
521
|
-
//
|
|
530
|
+
// Simulate new environment: reset OpenClaw mcporter.json
|
|
522
531
|
writeFile(path.join(WS_OC, 'config', 'mcporter.json'), '{"mcpServers":{}}');
|
|
523
|
-
//
|
|
532
|
+
// Pull
|
|
524
533
|
await syncEngine.restoreFromRepo(cfg);
|
|
525
|
-
// OpenClaw
|
|
534
|
+
// OpenClaw should receive playwright from shared MCP
|
|
526
535
|
const ocMcp = JSON.parse(fs.readFileSync(path.join(WS_OC, 'config', 'mcporter.json'), 'utf-8'));
|
|
527
|
-
assert.ok('playwright' in (ocMcp.mcpServers ?? {}), 'OpenClaw
|
|
536
|
+
assert.ok('playwright' in (ocMcp.mcpServers ?? {}), 'OpenClaw should receive Claude MCP config via pull');
|
|
528
537
|
});
|
|
529
538
|
});
|
|
530
|
-
// ── JSON
|
|
531
|
-
describe('JSON
|
|
532
|
-
it('
|
|
539
|
+
// ── JSON parse failure tolerance ────────────────────────────────────
|
|
540
|
+
describe('JSON parse failure tolerance', () => {
|
|
541
|
+
it('corrupted JSON source file does not crash push', async () => {
|
|
533
542
|
writeFile(path.join(WS_CL, '.claude.json'), '{ invalid json !!!');
|
|
534
543
|
writeFile(path.join(WS_CL, 'CLAUDE.md'), '# C');
|
|
535
544
|
writeFile(path.join(WS_OC, 'MEMORY.md'), '# M');
|
|
@@ -537,9 +546,9 @@ describe('JSON 解析失败容错', () => {
|
|
|
537
546
|
writeFile(path.join(WS_GE, 'settings.json'), '{"security":{},"model":{}}');
|
|
538
547
|
writeFile(path.join(WS_OC, 'config', 'mcporter.json'), '{"mcpServers":{}}');
|
|
539
548
|
const cfg = mkCfg({ shared: { skills: { sources: [] }, mcp: { sources: [] }, syncFiles: [] } });
|
|
540
|
-
//
|
|
549
|
+
// Should not throw
|
|
541
550
|
const result = await syncEngine.stageToRepo(cfg);
|
|
542
|
-
assert.ok(result.skipped.some(f => f.includes('mcpServers')), '
|
|
551
|
+
assert.ok(result.skipped.some(f => f.includes('mcpServers')), 'corrupted JSON entry should be skipped');
|
|
543
552
|
});
|
|
544
553
|
});
|
|
545
554
|
//# sourceMappingURL=sync.test.js.map
|