dsh-config-manager 0.1.78 → 0.1.79
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/lib/adapters/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +400 -112
- package/lib/client.js +2845 -1524
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +15 -1
- package/lib/sync/gist/gist-transport.js +66 -5
- package/lib/sync/gist/messages.d.ts +3 -0
- package/lib/sync/gist/messages.js +7 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/sync-engine.d.ts +193 -4
- package/lib/sync/sync-engine.js +450 -37
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +8 -0
- package/lib/ui/i18n.js +20 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/routes.ts +9 -0
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +219 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +234 -0
- package/src/client/sync/merge-view.ts +371 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +223 -0
- package/src/sync/gist/gist-transport.ts +73 -7
- package/src/sync/gist/messages.ts +7 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +379 -0
- package/src/sync/sync-engine.ts +535 -36
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +20 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code **插件读盘层**的回归护栏(2026-10-18)。
|
|
3
|
+
*
|
|
4
|
+
* 夹具形态取自本机真实布局(known_marketplaces.json / marketplace.json / plugins|external_plugins /
|
|
5
|
+
* .claude-plugin/plugin.json / skills / commands / agents / hooks / .mcp.json),**绝不含真凭据**:
|
|
6
|
+
* 假 token 只放在两个「绝不该被读进返回值」的地方(hooks 代码内容、仓库 URL 的 userinfo)。
|
|
7
|
+
*/
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { readClaudePlugins } from './read-claude-plugins.ts';
|
|
15
|
+
import type { ClaudePluginReadResult } from './read-claude-plugins.ts';
|
|
16
|
+
import type { ClaudePluginInput } from './types.ts';
|
|
17
|
+
|
|
18
|
+
const NL = String.fromCharCode(10);
|
|
19
|
+
const SKILL_MD = ['---', 'name: demo', 'description: fixture skill', '---', '', '# body', ''].join(NL);
|
|
20
|
+
/** 假凭据:只用于「绝不泄露」断言(hooks 内容与 repo userinfo) */
|
|
21
|
+
const FAKE_TOKEN = 'sk-ant-FAKE-0123456789abcdef';
|
|
22
|
+
|
|
23
|
+
async function withClaudeDir(fn: (claudeDir: string) => Promise<void>): Promise<void> {
|
|
24
|
+
const claudeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-claude-plugins-'));
|
|
25
|
+
try {
|
|
26
|
+
await fn(claudeDir);
|
|
27
|
+
} finally {
|
|
28
|
+
await fs.rm(claudeDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function write(file: string, text: string): Promise<void> {
|
|
33
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
34
|
+
await fs.writeFile(file, text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pluginOf(result: ClaudePluginReadResult, name: string): ClaudePluginInput | undefined {
|
|
38
|
+
return result.plugins.find((plugin) => plugin.name === name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 真机布局夹具:两个市场(其中一个 repo 带 userinfo)、启用/未启用/缺目录三种插件 */
|
|
42
|
+
async function buildFixture(claudeDir: string): Promise<{ demoDir: string; extDir: string }> {
|
|
43
|
+
const pluginsDir = path.join(claudeDir, 'plugins');
|
|
44
|
+
const marketDir = path.join(pluginsDir, 'marketplaces', 'demo-market');
|
|
45
|
+
await write(path.join(pluginsDir, 'known_marketplaces.json'), JSON.stringify({
|
|
46
|
+
'token-market': {
|
|
47
|
+
source: { source: 'git', url: 'https://user:' + FAKE_TOKEN + '@github.com/acme/secret-plugins.git' },
|
|
48
|
+
lastUpdated: '2026-10-01T00:00:00Z',
|
|
49
|
+
},
|
|
50
|
+
'demo-market': {
|
|
51
|
+
source: { source: 'github', repo: 'acme/claude-plugins' },
|
|
52
|
+
installLocation: marketDir,
|
|
53
|
+
lastUpdated: '2026-10-02T00:00:00Z',
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
await write(path.join(marketDir, '.claude-plugin', 'marketplace.json'), JSON.stringify({
|
|
57
|
+
name: 'demo-market',
|
|
58
|
+
description: 'fixture marketplace',
|
|
59
|
+
owner: { name: 'acme' },
|
|
60
|
+
plugins: [{ name: 'demo', description: 'demo plugin', author: 'acme', category: 'dev', source: { source: 'github', repo: 'acme/demo', ref: 'main' } }],
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const demoDir = path.join(marketDir, 'plugins', 'demo');
|
|
64
|
+
await write(path.join(demoDir, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'demo', version: '1.2.3', description: 'demo plugin' }));
|
|
65
|
+
await write(path.join(demoDir, 'skills', 'hello', 'SKILL.md'), SKILL_MD);
|
|
66
|
+
await write(path.join(demoDir, 'commands', 'one.md'), 'cmd one');
|
|
67
|
+
await write(path.join(demoDir, 'commands', 'nested', 'two.md'), 'cmd two');
|
|
68
|
+
await write(path.join(demoDir, 'agents', 'reviewer.md'), 'agent body');
|
|
69
|
+
await write(path.join(demoDir, 'hooks', 'hook.js'), 'console.log(' + JSON.stringify(FAKE_TOKEN) + ')');
|
|
70
|
+
await write(path.join(demoDir, 'hooks.json'), JSON.stringify({ hooks: {} }));
|
|
71
|
+
await write(path.join(demoDir, '.mcp.json'), JSON.stringify({ 'demo-server': { type: 'http', url: 'https://mcp.example.com' } }));
|
|
72
|
+
|
|
73
|
+
const extDir = path.join(marketDir, 'external_plugins', 'ext');
|
|
74
|
+
await write(path.join(extDir, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'ext' }));
|
|
75
|
+
await write(path.join(extDir, 'skills', 'ext-skill', 'SKILL.md'), SKILL_MD);
|
|
76
|
+
|
|
77
|
+
/* 未启用插件:安装在、但值不是真值 → 绝不产出 */
|
|
78
|
+
await write(path.join(marketDir, 'plugins', 'off', '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'off' }));
|
|
79
|
+
|
|
80
|
+
return { demoDir, extDir };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ENABLED_BOTH = {
|
|
84
|
+
enabledPlugins: {
|
|
85
|
+
'demo@demo-market': true,
|
|
86
|
+
'ext@demo-market': true,
|
|
87
|
+
'off@demo-market': false,
|
|
88
|
+
'ghost@demo-market': true,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
test('市场与启用插件:名字/repo 正确、userinfo 被剥离、组件与计数都对得上', async () => {
|
|
93
|
+
await withClaudeDir(async (claudeDir) => {
|
|
94
|
+
await buildFixture(claudeDir);
|
|
95
|
+
const result = await readClaudePlugins({ claudeDir, settings: ENABLED_BOTH });
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(result.marketplaces, [
|
|
98
|
+
{ name: 'demo-market', repo: 'acme/claude-plugins' },
|
|
99
|
+
{ name: 'token-market', repo: 'https://github.com/acme/secret-plugins.git' },
|
|
100
|
+
], 'repo 必须来自 source.repo 或 source.url,且 URL 的 userinfo 被剥离');
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(result.plugins.map((p) => p.name), ['demo', 'ext'], '未启用(false)与找不到安装目录的插件不得产出');
|
|
103
|
+
|
|
104
|
+
const demo = pluginOf(result, 'demo');
|
|
105
|
+
assert.ok(demo !== undefined);
|
|
106
|
+
assert.equal(demo.version, '1.2.3');
|
|
107
|
+
assert.equal(demo.marketplace, 'demo-market');
|
|
108
|
+
assert.equal(demo.repo, 'acme/claude-plugins');
|
|
109
|
+
assert.equal(demo.installRel, 'marketplaces/demo-market/plugins/demo');
|
|
110
|
+
assert.deepEqual(demo.components, ['skills', 'mcp', 'commands', 'agents', 'hooks']);
|
|
111
|
+
assert.deepEqual(demo.skills?.map((s) => s.name), ['hello']);
|
|
112
|
+
assert.deepEqual(demo.mcpJson, { 'demo-server': { type: 'http', url: 'https://mcp.example.com' } }, '.mcp.json 必须被读到(原样交翻译层)');
|
|
113
|
+
assert.equal(demo.commandCount, 2, 'commands 递归计数(含 nested/)');
|
|
114
|
+
assert.deepEqual(demo.agents, ['reviewer.md'], 'agents 只取文件名');
|
|
115
|
+
assert.equal(demo.hookFiles, 2, 'hooks/ 下的文件数 + hooks.json 各算 1');
|
|
116
|
+
assert.equal(demo.readFindings, undefined, '完整插件不得产出 finding');
|
|
117
|
+
|
|
118
|
+
const ext = pluginOf(result, 'ext');
|
|
119
|
+
assert.ok(ext !== undefined);
|
|
120
|
+
assert.equal(ext.installRel, 'marketplaces/demo-market/external_plugins/ext', 'external_plugins/ 布局必须能找到');
|
|
121
|
+
assert.deepEqual(ext.components, ['skills']);
|
|
122
|
+
|
|
123
|
+
assert.deepEqual(result.counts, {
|
|
124
|
+
marketplaces: 2,
|
|
125
|
+
// 真值才算启用:demo / ext / ghost 三条(off 的值是 false)
|
|
126
|
+
enabledPlugins: 3,
|
|
127
|
+
plugins: 2,
|
|
128
|
+
pluginSkills: 2,
|
|
129
|
+
pluginCommands: 2,
|
|
130
|
+
pluginAgents: 1,
|
|
131
|
+
pluginHooks: 2,
|
|
132
|
+
});
|
|
133
|
+
assert.deepEqual(
|
|
134
|
+
result.skipped.map((s) => [s.code, s.origin]),
|
|
135
|
+
[['source-unreadable', 'plugin:ghost']],
|
|
136
|
+
'有启用记录但没有安装目录:如实报码(不产出半成品条目)',
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const serialized = JSON.stringify(result);
|
|
140
|
+
assert.equal(serialized.includes(FAKE_TOKEN), false, '假 token(URL userinfo / hooks 内容)绝不允许出现在返回值里');
|
|
141
|
+
assert.equal(serialized.includes('sk-ant-'), false, '任何 token 形状都不得出现在返回值里');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('plugins/config.json 形状认不出来:报 plugins-config-unknown-shape,绝不瞎猜', async () => {
|
|
146
|
+
await withClaudeDir(async (claudeDir) => {
|
|
147
|
+
await write(path.join(claudeDir, 'plugins', 'config.json'), JSON.stringify({ version: 2, unknown: ['demo'] }));
|
|
148
|
+
const result = await readClaudePlugins({ claudeDir, settings: {} });
|
|
149
|
+
assert.deepEqual(result.plugins, [], '认不出来的形状绝不能被当成插件清单');
|
|
150
|
+
const skip = result.skipped.find((s) => s.detail === 'plugins-config-unknown-shape');
|
|
151
|
+
assert.ok(skip !== undefined, '必须报码:' + JSON.stringify(result.skipped));
|
|
152
|
+
assert.equal(skip.code, 'source-unreadable');
|
|
153
|
+
assert.equal(skip.origin, 'plugins/config.json');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('plugins/config.json 形状可自证({ plugins: { "name@market": true } }):认出来就用', async () => {
|
|
158
|
+
await withClaudeDir(async (claudeDir) => {
|
|
159
|
+
await buildFixture(claudeDir);
|
|
160
|
+
await write(path.join(claudeDir, 'plugins', 'config.json'), JSON.stringify({ plugins: { 'demo@demo-market': true } }));
|
|
161
|
+
const result = await readClaudePlugins({ claudeDir, settings: {} });
|
|
162
|
+
assert.deepEqual(result.plugins.map((p) => p.name), ['demo']);
|
|
163
|
+
assert.equal(result.skipped.some((s) => s.detail === 'plugins-config-unknown-shape'), false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('既无 enabledPlugins 也无 config.json:plugins 为空且不报码(诚实:没有启用的插件)', async () => {
|
|
168
|
+
await withClaudeDir(async (claudeDir) => {
|
|
169
|
+
const result = await readClaudePlugins({ claudeDir, settings: {} });
|
|
170
|
+
assert.deepEqual(result.plugins, []);
|
|
171
|
+
assert.deepEqual(result.marketplaces, []);
|
|
172
|
+
assert.deepEqual(result.skipped, []);
|
|
173
|
+
assert.equal(result.counts['plugins'], 0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('known_marketplaces.json 存在但解析失败:报 source-unreadable(不静默)', async () => {
|
|
178
|
+
await withClaudeDir(async (claudeDir) => {
|
|
179
|
+
await write(path.join(claudeDir, 'plugins', 'known_marketplaces.json'), '{ not json');
|
|
180
|
+
const result = await readClaudePlugins({ claudeDir, settings: {} });
|
|
181
|
+
const skip = result.skipped.find((s) => s.origin === 'plugins/known_marketplaces.json');
|
|
182
|
+
assert.ok(skip !== undefined, '解析失败必须报码:' + JSON.stringify(result.skipped));
|
|
183
|
+
assert.equal(skip.code, 'source-unreadable');
|
|
184
|
+
assert.equal(skip.detail, 'json-error');
|
|
185
|
+
assert.deepEqual(result.marketplaces, []);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('插件数触顶报 max-plugins-reached(不静默)', async () => {
|
|
190
|
+
await withClaudeDir(async (claudeDir) => {
|
|
191
|
+
await buildFixture(claudeDir);
|
|
192
|
+
const result = await readClaudePlugins({ claudeDir, settings: ENABLED_BOTH, limits: { maxPlugins: 1 } });
|
|
193
|
+
assert.equal(result.plugins.length, 1);
|
|
194
|
+
const skip = result.skipped.find((s) => s.detail === 'max-plugins-reached');
|
|
195
|
+
assert.ok(skip !== undefined, '触顶必须报码:' + JSON.stringify(result.skipped));
|
|
196
|
+
assert.equal(skip.origin, 'plugins');
|
|
197
|
+
assert.equal(skip.count, 1);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('单插件文件数触顶报 max-plugin-files-reached,且载荷里不超过上限', async () => {
|
|
202
|
+
await withClaudeDir(async (claudeDir) => {
|
|
203
|
+
const { demoDir } = await buildFixture(claudeDir);
|
|
204
|
+
await write(path.join(demoDir, 'skills', 'hello', 'extra1.md'), 'x');
|
|
205
|
+
await write(path.join(demoDir, 'skills', 'hello', 'extra2.md'), 'y');
|
|
206
|
+
const result = await readClaudePlugins({
|
|
207
|
+
claudeDir,
|
|
208
|
+
settings: { enabledPlugins: { 'demo@demo-market': true } },
|
|
209
|
+
limits: { maxPluginFiles: 2 },
|
|
210
|
+
});
|
|
211
|
+
const demo = pluginOf(result, 'demo');
|
|
212
|
+
assert.ok(demo !== undefined);
|
|
213
|
+
const skip = demo.readFindings?.find((s) => s.detail === 'max-plugin-files-reached');
|
|
214
|
+
assert.ok(skip !== undefined, '单插件触顶必须报码:' + JSON.stringify(demo.readFindings));
|
|
215
|
+
assert.equal(skip.origin, 'plugin:demo');
|
|
216
|
+
assert.equal(skip.count, 2);
|
|
217
|
+
const carried = demo.skills?.reduce((sum, skill) => sum + skill.files.length, 0) ?? 0;
|
|
218
|
+
assert.ok(carried <= 2, '载荷里不得带超过上限的技能文件(实际 ' + String(carried) + ')');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('plugin.json 解析失败:回落目录名并报码(绝不猜字段)', async () => {
|
|
223
|
+
await withClaudeDir(async (claudeDir) => {
|
|
224
|
+
const pluginDir = path.join(claudeDir, 'plugins', 'marketplaces', 'm', 'plugins', 'broken');
|
|
225
|
+
await write(path.join(pluginDir, '.claude-plugin', 'plugin.json'), '{ oops');
|
|
226
|
+
const result = await readClaudePlugins({ claudeDir, settings: { enabledPlugins: { 'broken@m': true } } });
|
|
227
|
+
const plugin = pluginOf(result, 'broken');
|
|
228
|
+
assert.ok(plugin !== undefined, '安装目录存在就必须产出条目:' + JSON.stringify(result.skipped));
|
|
229
|
+
assert.equal(plugin.version, undefined);
|
|
230
|
+
const skip = plugin.readFindings?.find((s) => s.origin === 'plugin:broken/plugin.json');
|
|
231
|
+
assert.ok(skip !== undefined, '解析失败必须报码:' + JSON.stringify(plugin.readFindings));
|
|
232
|
+
assert.equal(skip.detail, 'json-error');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -178,3 +178,144 @@ test('t3 产物可被 DSH 自己的 codec 解码回逻辑事件(本机无 code
|
|
|
178
178
|
assert.ok(types.includes('tool/result'));
|
|
179
179
|
assert.ok(types.includes('turn/end'));
|
|
180
180
|
});
|
|
181
|
+
/* ---------------- 会话保真(2026-10-18,task-4)---------------- */
|
|
182
|
+
|
|
183
|
+
/** 极简 JSONL 构造器(与文件顶部同形:一行一条 JSON) */
|
|
184
|
+
function jsonl(records: readonly Record<string, unknown>[]): string {
|
|
185
|
+
return records.map((r) => line(r)).join(NL) + NL;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 稳定的消息 uuid(避免各用例手写一长串) */
|
|
189
|
+
function uuidOf(n: number): string {
|
|
190
|
+
return 'aaaaaaaa-0000-4000-8000-' + String(n).padStart(12, '0');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 产物的 header(形态运用文件头的 readSessionLogShapeFromBytes:失败就断言失败,不伪装) */
|
|
194
|
+
function headerOf(bytes: Uint8Array): Record<string, unknown> {
|
|
195
|
+
const shape = readSessionLogShapeFromBytes(bytes);
|
|
196
|
+
assert.equal(shape.ok, true, '产物必须是 DSH 可扫的 zstd 帧序列');
|
|
197
|
+
if (!shape.ok) return {};
|
|
198
|
+
return shape.headerValue as Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 产物的逻辑事件行(null 行用空对象占位,方便后续断言) */
|
|
202
|
+
function rowsOf(bytes: Uint8Array): Record<string, unknown>[] {
|
|
203
|
+
const shape = readSessionLogShapeFromBytes(bytes);
|
|
204
|
+
assert.equal(shape.ok, true, '产物必须是 DSH 可扫的 zstd 帧序列');
|
|
205
|
+
if (!shape.ok) return [];
|
|
206
|
+
return shape.rows.map((r) => (r === null ? {} : (r as Record<string, unknown>)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 某条助手消息的 content 块(第一条) */
|
|
210
|
+
function assistantContentOf(bytes: Uint8Array): Record<string, unknown>[] {
|
|
211
|
+
const assistant = rowsOf(bytes).find((r) => r['type'] === 'assistant/message');
|
|
212
|
+
assert.ok(assistant !== undefined, '语料里应有助手消息');
|
|
213
|
+
return (assistant?.['data'] as { message: { content: Record<string, unknown>[] } }).message.content;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const FIXED = 1_700_000_000_000;
|
|
217
|
+
const CREATED = Date.parse('2026-09-09T09:09:09.000Z');
|
|
218
|
+
|
|
219
|
+
test('保真 t4:isSidechain 记录不进 IR(子代理正文绝不混进父对话),整类计报 isSidechain', () => {
|
|
220
|
+
const text = jsonl([
|
|
221
|
+
{ type: 'user', uuid: uuidOf(1), timestamp: '2026-10-01T10:00:00.000Z', cwd: CWD, message: { role: 'user', content: 'parent question' } },
|
|
222
|
+
{ type: 'assistant', uuid: uuidOf(2), timestamp: '2026-10-01T10:00:01.000Z', cwd: CWD, message: { model: 'claude-sonnet-4', content: [{ type: 'text', text: 'parent answer' }] } },
|
|
223
|
+
{ type: 'user', uuid: uuidOf(3), timestamp: '2026-10-01T10:00:02.000Z', cwd: CWD, isSidechain: true, message: { role: 'user', content: 'SIDECHAIN_DO_NOT_LEAK' } },
|
|
224
|
+
{ type: 'assistant', uuid: uuidOf(4), timestamp: '2026-10-01T10:00:03.000Z', cwd: CWD, isSidechain: true, message: { model: 'claude-sonnet-4', content: [{ type: 'text', text: 'SIDECHAIN_DO_NOT_LEAK' }] } },
|
|
225
|
+
]);
|
|
226
|
+
const result = transcodeClaudeSession({ id: ID, text }, { formatVersion: 3, now: FIXED });
|
|
227
|
+
assert.ok(result.session, '父对话本身必须照常转码');
|
|
228
|
+
const s = result.session;
|
|
229
|
+
assert.equal(s.info.records, 4, 'records 是源记录原始条数(含被忽略的)');
|
|
230
|
+
assert.equal(s.info.userMessages, 1, '子代理记录不算父对话的用户消息');
|
|
231
|
+
assert.equal(s.info.assistantMessages, 1);
|
|
232
|
+
assert.equal(s.info.ignored['isSidechain'], 2, '子代理记录必须整类计报(下游 detail = isSidechain)');
|
|
233
|
+
const rows = rowsOf(s.data);
|
|
234
|
+
assert.equal(JSON.stringify(rows).includes('SIDECHAIN_DO_NOT_LEAK'), false, '子代理正文绝不得进产物');
|
|
235
|
+
assert.equal(rows.filter((r) => r['type'] === 'user/message').length, 1, '只有父对话那一条用户消息');
|
|
236
|
+
assert.equal(JSON.stringify(rows).includes('parent answer'), true, '父对话正文不受影响');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('保真 t4:记录 sessionId 与文件名 stem 不一致 → findings 报 session-id-mismatch(id 仍用 stem)', () => {
|
|
240
|
+
const base = { id: ID, text: sessionText() };
|
|
241
|
+
const mismatch = transcodeClaudeSession({ ...base, recordSessionId: 'ffffffff-2222-4222-8222-222222222222' }, { formatVersion: 3, now: FIXED });
|
|
242
|
+
assert.ok(mismatch.session, '不一致不是跳过理由:会话照样产出');
|
|
243
|
+
assert.equal(mismatch.session.id, ID, '会话 id 仍以文件名 stem 为准(DSH 按目录/文件名定位)');
|
|
244
|
+
assert.deepEqual(mismatch.findings, [{ code: 'session-id-mismatch', origin: ID }]);
|
|
245
|
+
assert.deepEqual(transcodeClaudeSession({ ...base, recordSessionId: ID }, { formatVersion: 3, now: FIXED }).findings, [], '一致就不报');
|
|
246
|
+
assert.deepEqual(transcodeClaudeSession(base, { formatVersion: 3, now: FIXED }).findings, [], '读盘层没给就不猜');
|
|
247
|
+
assert.deepEqual(transcodeClaudeSession({ ...base, recordSessionId: '' }, { formatVersion: 3, now: FIXED }).findings, [], '空串视同没给');
|
|
248
|
+
const skipped = transcodeClaudeSession({ id: 'a/b', text: sessionText(), recordSessionId: 'x' }, { formatVersion: 3, now: FIXED });
|
|
249
|
+
assert.equal(skipped.skip?.code, 'session-unsafe-id');
|
|
250
|
+
assert.equal(skipped.findings.length, 1, '不一致是文件级事实:跳过面也要带上 findings');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('保真 t4:推理块计报 thinking 且不进正文;非 Claude 的 reasoning 形状保持既有口径', () => {
|
|
254
|
+
const text = jsonl([
|
|
255
|
+
{ type: 'user', uuid: uuidOf(1), timestamp: '2026-10-01T10:00:00.000Z', cwd: CWD, message: { role: 'user', content: 'q' } },
|
|
256
|
+
{ type: 'assistant', uuid: uuidOf(2), timestamp: '2026-10-01T10:00:01.000Z', cwd: CWD, message: { model: 'claude-sonnet-4', content: [
|
|
257
|
+
{ type: 'thinking', thinking: 'THINKING_DO_NOT_LEAK', signature: 'sig' },
|
|
258
|
+
{ type: 'text', text: 'answer' },
|
|
259
|
+
{ type: 'redacted_thinking', data: 'REDACTED_DO_NOT_LEAK' },
|
|
260
|
+
] } },
|
|
261
|
+
]);
|
|
262
|
+
const result = transcodeClaudeSession({ id: ID, text }, { formatVersion: 3, now: FIXED });
|
|
263
|
+
assert.ok(result.session);
|
|
264
|
+
assert.equal(result.session.info.ignored['thinking'], 2, 'thinking / redacted_thinking 逐条计报');
|
|
265
|
+
const rows = rowsOf(result.session.data);
|
|
266
|
+
assert.equal(JSON.stringify(rows).includes('DO_NOT_LEAK'), false, '推理正文绝不得进产物');
|
|
267
|
+
assert.deepEqual(
|
|
268
|
+
assistantContentOf(result.session.data),
|
|
269
|
+
[{ type: 'text', text: 'answer' }],
|
|
270
|
+
'推理块既不进 IR、也不留空文本块(空块会污染串联后的正文换行)',
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// 非 Claude 原生的 `{type:'reasoning', text:'…'}`(别的导出器的命名)刻意保持既有口径:
|
|
274
|
+
// 它命中共享 IR “对象带 text 就算文本块”的兼底分支(该分支被逐字节回归钉住),
|
|
275
|
+
// 所以本次计报口径**不含它**。谁要一并丢弃/计报,改 REASONING_BLOCK_TYPES 一个数组即可,
|
|
276
|
+
// 并同步更新 claude-sessions-bytes.test.ts 的对应基线(该案 728 → 722 字节)。
|
|
277
|
+
const legacy = transcodeClaudeSession({
|
|
278
|
+
id: ID,
|
|
279
|
+
text: jsonl([
|
|
280
|
+
{ type: 'user', uuid: uuidOf(1), timestamp: '2026-10-01T10:00:00.000Z', cwd: CWD, message: { role: 'user', content: 'q' } },
|
|
281
|
+
{ type: 'assistant', uuid: uuidOf(2), timestamp: '2026-10-01T10:00:01.000Z', cwd: CWD, message: { model: 'claude-sonnet-4', content: [{ type: 'reasoning', text: 'r' }, { type: 'text', text: 'c' }] } },
|
|
282
|
+
]),
|
|
283
|
+
}, { formatVersion: 3, now: FIXED });
|
|
284
|
+
assert.ok(legacy.session);
|
|
285
|
+
assert.equal(legacy.session.info.ignored['thinking'], undefined, 'reasoning 形状不在本次计报口径内(见注释)');
|
|
286
|
+
assert.deepEqual(assistantContentOf(legacy.session.data), [{ type: 'text', text: 'r' + NL + 'c' }], '既有文本兜底分支原样保留');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('保真 t4:幂等 —— 同一来源两次转码逐字节相同(墙钟回落只有一个确定性来源)', () => {
|
|
290
|
+
// ① 记录自带时间戳:确定性来自记录本身(**不注入 now**)
|
|
291
|
+
const a = transcodeClaudeSession({ id: ID, text: sessionText() }, { formatVersion: 3 });
|
|
292
|
+
const b = transcodeClaudeSession({ id: ID, text: sessionText() }, { formatVersion: 3 });
|
|
293
|
+
assert.ok(a.session !== undefined && b.session !== undefined);
|
|
294
|
+
assert.deepEqual(a.session.data, b.session.data, '同一输入两次转码必须逐字节相同(否则 DSH 侧“同内容 → Skip”永不成立)');
|
|
295
|
+
assert.equal(a.session.info.counts['timestampsFallback'], 0);
|
|
296
|
+
assert.equal(headerOf(a.session.data)['createdAt'], Date.parse('2026-10-01T10:00:00.000Z'), 'createdAt 取记录自身最早时间戳');
|
|
297
|
+
|
|
298
|
+
// ② 记录没有时间戳:回落 input.createdAtMs(读盘层给的确定性值)
|
|
299
|
+
const noTs = jsonl([
|
|
300
|
+
{ type: 'user', uuid: uuidOf(1), cwd: CWD, message: { role: 'user', content: 'q' } },
|
|
301
|
+
{ type: 'assistant', uuid: uuidOf(2), cwd: CWD, message: { model: 'claude-sonnet-4', content: [{ type: 'text', text: 'a' }] } },
|
|
302
|
+
]);
|
|
303
|
+
const c = transcodeClaudeSession({ id: ID, text: noTs, createdAtMs: CREATED }, { formatVersion: 3 });
|
|
304
|
+
const d = transcodeClaudeSession({ id: ID, text: noTs, createdAtMs: CREATED }, { formatVersion: 3 });
|
|
305
|
+
assert.ok(c.session !== undefined && d.session !== undefined);
|
|
306
|
+
assert.deepEqual(c.session.data, d.session.data, 'createdAtMs 回落同样必须幂等');
|
|
307
|
+
assert.equal(c.session.info.counts['timestampsFallback'], 0, '有确定性回落值就不应记 timestampsFallback');
|
|
308
|
+
assert.equal(headerOf(c.session.data)['createdAt'], CREATED);
|
|
309
|
+
// **全程只有一个墙钟来源**:所有事件的 time 都必须等于 createdAtMs
|
|
310
|
+
// (旧实现 parse 与 synthesize 各调一次 Date.now(),两处可能落在不同毫秒)
|
|
311
|
+
for (const row of rowsOf(c.session.data)) assert.equal(row['time'], CREATED);
|
|
312
|
+
|
|
313
|
+
// ③ 两者都没有:只能墙钟回落,并如实记账
|
|
314
|
+
const e = transcodeClaudeSession({ id: ID, text: noTs }, { formatVersion: 3 });
|
|
315
|
+
assert.ok(e.session);
|
|
316
|
+
assert.equal(e.session.info.counts['timestampsFallback'], 1, '没有确定性时间来源必须如实记账');
|
|
317
|
+
// 非安全整数视同“没给”(DSH 对时间字段要求安全整数),绝不把坏值写进行里
|
|
318
|
+
const f = transcodeClaudeSession({ id: ID, text: noTs, createdAtMs: Number.NaN }, { formatVersion: 3 });
|
|
319
|
+
assert.ok(f.session);
|
|
320
|
+
assert.equal(f.session.info.counts['timestampsFallback'], 1, '非法 createdAtMs 不得当成确定性值');
|
|
321
|
+
});
|