@skillsmith/core 0.11.4 → 0.11.5
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/CHANGELOG.md +13 -0
- package/README.md +10 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +15 -5
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/client.private-registry.d.ts +80 -0
- package/dist/src/api/client.private-registry.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.js +97 -0
- package/dist/src/api/client.private-registry.js.map +1 -0
- package/dist/src/api/client.private-registry.test.d.ts +9 -0
- package/dist/src/api/client.private-registry.test.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.test.js +127 -0
- package/dist/src/api/client.private-registry.test.js.map +1 -0
- package/dist/src/api/client.toSkill.test.d.ts +25 -0
- package/dist/src/api/client.toSkill.test.d.ts.map +1 -0
- package/dist/src/api/client.toSkill.test.js +97 -0
- package/dist/src/api/client.toSkill.test.js.map +1 -0
- package/dist/src/api/client.token-refresh.d.ts +8 -0
- package/dist/src/api/client.token-refresh.d.ts.map +1 -1
- package/dist/src/api/client.token-refresh.js +26 -0
- package/dist/src/api/client.token-refresh.js.map +1 -1
- package/dist/src/api/client.token-refresh.test.js +82 -0
- package/dist/src/api/client.token-refresh.test.js.map +1 -1
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.d.ts.map +1 -1
- package/dist/src/api/index.js +9 -0
- package/dist/src/api/index.js.map +1 -1
- package/dist/src/api/security-summary.d.ts +73 -0
- package/dist/src/api/security-summary.d.ts.map +1 -0
- package/dist/src/api/security-summary.js +88 -0
- package/dist/src/api/security-summary.js.map +1 -0
- package/dist/src/api/security-summary.test.d.ts +9 -0
- package/dist/src/api/security-summary.test.d.ts.map +1 -0
- package/dist/src/api/security-summary.test.js +94 -0
- package/dist/src/api/security-summary.test.js.map +1 -0
- package/dist/src/config/config-atomic-write.d.ts +30 -10
- package/dist/src/config/config-atomic-write.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.js +33 -80
- package/dist/src/config/config-atomic-write.js.map +1 -1
- package/dist/src/config/config-atomic-write.test.d.ts +8 -0
- package/dist/src/config/config-atomic-write.test.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.test.js +64 -12
- package/dist/src/config/config-atomic-write.test.js.map +1 -1
- package/dist/src/config/owned-lock.acquire.d.ts +53 -0
- package/dist/src/config/owned-lock.acquire.d.ts.map +1 -0
- package/dist/src/config/owned-lock.acquire.js +140 -0
- package/dist/src/config/owned-lock.acquire.js.map +1 -0
- package/dist/src/config/owned-lock.claim.d.ts +84 -0
- package/dist/src/config/owned-lock.claim.d.ts.map +1 -0
- package/dist/src/config/owned-lock.claim.js +310 -0
- package/dist/src/config/owned-lock.claim.js.map +1 -0
- package/dist/src/config/owned-lock.d.ts +113 -0
- package/dist/src/config/owned-lock.d.ts.map +1 -0
- package/dist/src/config/owned-lock.js +121 -0
- package/dist/src/config/owned-lock.js.map +1 -0
- package/dist/src/config/owned-lock.test.d.ts +12 -0
- package/dist/src/config/owned-lock.test.d.ts.map +1 -0
- package/dist/src/config/owned-lock.test.js +412 -0
- package/dist/src/config/owned-lock.test.js.map +1 -0
- package/dist/src/config/owned-lock.types.d.ts +78 -0
- package/dist/src/config/owned-lock.types.d.ts.map +1 -0
- package/dist/src/config/owned-lock.types.js +30 -0
- package/dist/src/config/owned-lock.types.js.map +1 -0
- package/dist/src/db/createDatabase.d.ts.map +1 -1
- package/dist/src/db/createDatabase.js +19 -7
- package/dist/src/db/createDatabase.js.map +1 -1
- package/dist/src/embeddings/hnsw-store.exports.d.ts +11 -0
- package/dist/src/embeddings/hnsw-store.exports.d.ts.map +1 -0
- package/dist/src/embeddings/hnsw-store.exports.js +10 -0
- package/dist/src/embeddings/hnsw-store.exports.js.map +1 -0
- package/dist/src/embeddings/index.d.ts +1 -2
- package/dist/src/embeddings/index.d.ts.map +1 -1
- package/dist/src/embeddings/index.js +17 -3
- package/dist/src/embeddings/index.js.map +1 -1
- package/dist/src/embeddings/probe.d.ts.map +1 -1
- package/dist/src/embeddings/probe.js +4 -8
- package/dist/src/embeddings/probe.js.map +1 -1
- package/dist/src/exports/services.d.ts +4 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +22 -0
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/index.d.ts +1 -1
- package/dist/src/install/index.d.ts.map +1 -1
- package/dist/src/install/index.js +1 -1
- package/dist/src/install/index.js.map +1 -1
- package/dist/src/install/paths.d.ts +10 -0
- package/dist/src/install/paths.d.ts.map +1 -1
- package/dist/src/install/paths.js +19 -0
- package/dist/src/install/paths.js.map +1 -1
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts +80 -0
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts.map +1 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js +287 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -0
- package/dist/src/services/recommend-guard.d.ts +28 -0
- package/dist/src/services/recommend-guard.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.js +34 -0
- package/dist/src/services/recommend-guard.js.map +1 -0
- package/dist/src/services/recommend-guard.test.d.ts +2 -0
- package/dist/src/services/recommend-guard.test.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.test.js +28 -0
- package/dist/src/services/recommend-guard.test.js.map +1 -0
- package/dist/src/services/skill-installation.content.d.ts +88 -0
- package/dist/src/services/skill-installation.content.d.ts.map +1 -0
- package/dist/src/services/skill-installation.content.js +319 -0
- package/dist/src/services/skill-installation.content.js.map +1 -0
- package/dist/src/services/skill-installation.helpers.d.ts +32 -1
- package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
- package/dist/src/services/skill-installation.helpers.js +39 -6
- package/dist/src/services/skill-installation.helpers.js.map +1 -1
- package/dist/src/services/skill-installation.helpers.test.js +34 -2
- package/dist/src/services/skill-installation.helpers.test.js.map +1 -1
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.js +74 -1
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/services/skill-installation.service.d.ts +20 -1
- package/dist/src/services/skill-installation.service.d.ts.map +1 -1
- package/dist/src/services/skill-installation.service.js +26 -4
- package/dist/src/services/skill-installation.service.js.map +1 -1
- package/dist/src/services/skill-installation.types.d.ts +42 -1
- package/dist/src/services/skill-installation.types.d.ts.map +1 -1
- package/dist/src/services/skill-installation.types.js.map +1 -1
- package/dist/src/services/skill-resolution.d.ts +67 -0
- package/dist/src/services/skill-resolution.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.js +67 -0
- package/dist/src/services/skill-resolution.js.map +1 -0
- package/dist/src/services/skill-resolution.test.d.ts +2 -0
- package/dist/src/services/skill-resolution.test.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.test.js +143 -0
- package/dist/src/services/skill-resolution.test.js.map +1 -0
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +4 -0
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/quiet-mode.d.ts +21 -0
- package/dist/src/utils/quiet-mode.d.ts.map +1 -0
- package/dist/src/utils/quiet-mode.js +26 -0
- package/dist/src/utils/quiet-mode.js.map +1 -0
- package/dist/tests/EmbeddingService.test.js +58 -1
- package/dist/tests/EmbeddingService.test.js.map +1 -1
- package/dist/tests/SecurityScanner.performance.test.js +5 -2
- package/dist/tests/SecurityScanner.performance.test.js.map +1 -1
- package/dist/tests/db/database-abstraction.test.js +45 -1
- package/dist/tests/db/database-abstraction.test.js.map +1 -1
- package/dist/tests/helpers/deterministic-dead-pid.d.ts +13 -0
- package/dist/tests/helpers/deterministic-dead-pid.d.ts.map +1 -0
- package/dist/tests/helpers/deterministic-dead-pid.js +31 -0
- package/dist/tests/helpers/deterministic-dead-pid.js.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts +12 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js +52 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts +19 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js +177 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.content.test.d.ts +10 -0
- package/dist/tests/unit/services/skill-installation.content.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.content.test.js +290 -0
- package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts +8 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js +72 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts +15 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +190 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts +17 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js +126 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.test.js +7 -112
- package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
- package/dist/tests/utils/quiet-mode.test.d.ts +12 -0
- package/dist/tests/utils/quiet-mode.test.d.ts.map +1 -0
- package/dist/tests/utils/quiet-mode.test.js +42 -0
- package/dist/tests/utils/quiet-mode.test.js.map +1 -0
- package/package.json +17 -5
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview writeInstallFiles nested sub-skill path + symlink-safety tests
|
|
3
|
+
* @see SMI-5905 final code review (Sol, GPT-5.6-Sol via NEEDLE) findings #2/#4
|
|
4
|
+
*
|
|
5
|
+
* Split out of skill-installation.io.test.ts (515/500 lines once these tests landed).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { writeInstallFiles } from '../../../src/services/skill-installation.io.js';
|
|
12
|
+
/**
|
|
13
|
+
* SMI-5905 Sol final-code-review findings #2/#4: nested sub-skill filenames (e.g.
|
|
14
|
+
* "scripts/run.sh", used by private-registry content installs) previously had no parent
|
|
15
|
+
* directory created (ENOENT on any clean install) and, once fixed, must not silently write
|
|
16
|
+
* through a pre-existing symlink at an intermediate path component.
|
|
17
|
+
*/
|
|
18
|
+
describe('writeInstallFiles nested sub-skill paths (SMI-5905 final review #2/#4)', () => {
|
|
19
|
+
it('creates the parent directory for a nested sub-skill filename', async () => {
|
|
20
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wif-nested-'));
|
|
21
|
+
try {
|
|
22
|
+
const skillsDir = path.join(root, 'skills');
|
|
23
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
24
|
+
const installPath = path.join(skillsDir, 'my-skill');
|
|
25
|
+
const result = await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [{ filename: 'scripts/run.sh', content: 'echo hi' }], undefined);
|
|
26
|
+
expect(await fs.readFile(path.join(installPath, 'scripts', 'run.sh'), 'utf8')).toBe('echo hi');
|
|
27
|
+
expect(result.writtenFiles).toContain(path.join(installPath, 'scripts', 'run.sh'));
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await fs.rm(root, { recursive: true, force: true }).catch(() => { });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it('creates parents for two nested files sharing the same new subdirectory (concurrent-write race)', async () => {
|
|
34
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wif-nested-race-'));
|
|
35
|
+
try {
|
|
36
|
+
const skillsDir = path.join(root, 'skills');
|
|
37
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
38
|
+
const installPath = path.join(skillsDir, 'my-skill');
|
|
39
|
+
await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [
|
|
40
|
+
{ filename: 'scripts/a.sh', content: 'a' },
|
|
41
|
+
{ filename: 'scripts/b.sh', content: 'b' },
|
|
42
|
+
], undefined);
|
|
43
|
+
expect(await fs.readFile(path.join(installPath, 'scripts', 'a.sh'), 'utf8')).toBe('a');
|
|
44
|
+
expect(await fs.readFile(path.join(installPath, 'scripts', 'b.sh'), 'utf8')).toBe('b');
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await fs.rm(root, { recursive: true, force: true }).catch(() => { });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
it('refuses to write through a pre-existing symlinked intermediate directory', async () => {
|
|
51
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wif-symlink-'));
|
|
52
|
+
try {
|
|
53
|
+
const skillsDir = path.join(root, 'skills');
|
|
54
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
55
|
+
const installPath = path.join(skillsDir, 'my-skill');
|
|
56
|
+
await fs.mkdir(installPath, { recursive: true });
|
|
57
|
+
// A directory OUTSIDE skillsDir the attack would try to write into.
|
|
58
|
+
const outsideTarget = path.join(root, 'outside-target');
|
|
59
|
+
await fs.mkdir(outsideTarget, { recursive: true });
|
|
60
|
+
// Plant "scripts" as a symlink pointing outside skillsDir, as if left behind by a
|
|
61
|
+
// prior force-reinstall or planted directly by an attacker with local write access.
|
|
62
|
+
await fs.symlink(outsideTarget, path.join(installPath, 'scripts'));
|
|
63
|
+
await expect(writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [{ filename: 'scripts/run.sh', content: 'malicious' }], undefined)).rejects.toThrow(/symlink/);
|
|
64
|
+
// Nothing was written to the symlink target.
|
|
65
|
+
await expect(fs.access(path.join(outsideTarget, 'run.sh'))).rejects.toThrow();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await fs.rm(root, { recursive: true, force: true }).catch(() => { });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=skill-installation.io.symlink.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-installation.io.symlink.test.js","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.io.symlink.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-4795: errorCode taxonomy on install failures
|
|
3
|
+
*
|
|
4
|
+
* Each failing return path inside install() must populate `errorCode` with
|
|
5
|
+
* a code from InstallErrorCode. Telemetry (emitInstallEvent) reads this
|
|
6
|
+
* field; without it the install funnel cannot classify failures.
|
|
7
|
+
*
|
|
8
|
+
* Split out of skill-installation.service.test.ts (CLAUDE.md's 500-line
|
|
9
|
+
* file cap) after SMI-5894 Wave 1 added its own multi-client suite there —
|
|
10
|
+
* this block predates that change and is unrelated to it, but moving it
|
|
11
|
+
* out (rather than the newly-added block alone) was the smallest change
|
|
12
|
+
* that got the original file back under the cap.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=skill-installation.service.error-codes.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-installation.service.error-codes.test.d.ts","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.service.error-codes.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-4795: errorCode taxonomy on install failures
|
|
3
|
+
*
|
|
4
|
+
* Each failing return path inside install() must populate `errorCode` with
|
|
5
|
+
* a code from InstallErrorCode. Telemetry (emitInstallEvent) reads this
|
|
6
|
+
* field; without it the install funnel cannot classify failures.
|
|
7
|
+
*
|
|
8
|
+
* Split out of skill-installation.service.test.ts (CLAUDE.md's 500-line
|
|
9
|
+
* file cap) after SMI-5894 Wave 1 added its own multi-client suite there —
|
|
10
|
+
* this block predates that change and is unrelated to it, but moving it
|
|
11
|
+
* out (rather than the newly-added block alone) was the smallest change
|
|
12
|
+
* that got the original file back under the cap.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
|
+
import * as fs from 'fs/promises';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import { SkillInstallationService } from '../../../src/services/skill-installation.service.js';
|
|
19
|
+
import { SkillRepository } from '../../../src/repositories/SkillRepository.js';
|
|
20
|
+
import { SkillDependencyRepository } from '../../../src/repositories/SkillDependencyRepository.js';
|
|
21
|
+
import { createTestDatabase } from '../../helpers/database.js';
|
|
22
|
+
const VALID_SKILL_MD = `---
|
|
23
|
+
name: test-skill
|
|
24
|
+
description: A test skill for unit testing
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Test Skill
|
|
28
|
+
|
|
29
|
+
This is a valid skill file with enough content to pass the 100-character minimum
|
|
30
|
+
validation threshold that the service checks during installation.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Use this skill by saying "Use the test-skill skill to..."
|
|
35
|
+
`;
|
|
36
|
+
const SHORT_SKILL_MD = '# Short\nToo short.';
|
|
37
|
+
let tmpDir;
|
|
38
|
+
let skillsDir;
|
|
39
|
+
let manifestPath;
|
|
40
|
+
async function createTmpDirs() {
|
|
41
|
+
tmpDir = path.join(os.tmpdir(), 'skillsmith-test-' + Date.now() + '-' + Math.random().toString(36).slice(2));
|
|
42
|
+
skillsDir = path.join(tmpDir, 'skills');
|
|
43
|
+
manifestPath = path.join(tmpDir, 'manifest.json');
|
|
44
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
async function cleanupTmpDirs() {
|
|
47
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
48
|
+
}
|
|
49
|
+
function createMockRegistryLookup(skills) {
|
|
50
|
+
return {
|
|
51
|
+
async lookup(skillId) {
|
|
52
|
+
const entry = skills[skillId];
|
|
53
|
+
if (!entry)
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
repoUrl: entry.repoUrl,
|
|
57
|
+
name: entry.name,
|
|
58
|
+
trustTier: entry.trustTier ?? 'community',
|
|
59
|
+
quarantined: entry.quarantined,
|
|
60
|
+
contentHash: entry.contentHash,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createService(db, overrides = {}) {
|
|
66
|
+
const skillRepo = new SkillRepository(db);
|
|
67
|
+
const skillDependencyRepo = new SkillDependencyRepository(db);
|
|
68
|
+
return new SkillInstallationService({
|
|
69
|
+
db,
|
|
70
|
+
skillRepo,
|
|
71
|
+
skillDependencyRepo,
|
|
72
|
+
skillsDir,
|
|
73
|
+
manifestPath,
|
|
74
|
+
...overrides,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
describe('SMI-4795: install failures populate errorCode', () => {
|
|
78
|
+
let db;
|
|
79
|
+
beforeEach(async () => {
|
|
80
|
+
db = await createTestDatabase();
|
|
81
|
+
await createTmpDirs();
|
|
82
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
83
|
+
});
|
|
84
|
+
afterEach(async () => {
|
|
85
|
+
db.close();
|
|
86
|
+
await cleanupTmpDirs();
|
|
87
|
+
vi.restoreAllMocks();
|
|
88
|
+
});
|
|
89
|
+
it('REGISTRY_LOOKUP_UNAVAILABLE when registry id supplied without lookup adapter', async () => {
|
|
90
|
+
const service = createService(db); // no registryLookup
|
|
91
|
+
const result = await service.install('author/some-skill');
|
|
92
|
+
expect(result.success).toBe(false);
|
|
93
|
+
expect(result.errorCode).toBe('REGISTRY_LOOKUP_UNAVAILABLE');
|
|
94
|
+
});
|
|
95
|
+
it('REGISTRY_SKILL_NOT_FOUND when registry returns null', async () => {
|
|
96
|
+
const service = createService(db, {
|
|
97
|
+
registryLookup: createMockRegistryLookup({}), // empty registry
|
|
98
|
+
});
|
|
99
|
+
const result = await service.install('author/missing-skill');
|
|
100
|
+
expect(result.success).toBe(false);
|
|
101
|
+
expect(result.errorCode).toBe('REGISTRY_SKILL_NOT_FOUND');
|
|
102
|
+
});
|
|
103
|
+
it('QUARANTINED when registry flags the skill', async () => {
|
|
104
|
+
const service = createService(db, {
|
|
105
|
+
registryLookup: createMockRegistryLookup({
|
|
106
|
+
'author/quarantined-skill': {
|
|
107
|
+
repoUrl: 'https://github.com/author/quarantined-skill',
|
|
108
|
+
name: 'quarantined-skill',
|
|
109
|
+
quarantined: true,
|
|
110
|
+
trustTier: 'community',
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
const result = await service.install('author/quarantined-skill');
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
expect(result.errorCode).toBe('QUARANTINED');
|
|
117
|
+
expect(result.trustTier).toBe('community');
|
|
118
|
+
});
|
|
119
|
+
it('FETCH_FAILED when SKILL.md is unreachable', async () => {
|
|
120
|
+
const mockFetch = vi.mocked(fetch);
|
|
121
|
+
mockFetch.mockResolvedValue(new Response('Not found', { status: 404 }));
|
|
122
|
+
const service = createService(db);
|
|
123
|
+
const result = await service.install('https://github.com/owner/missing-repo');
|
|
124
|
+
expect(result.success).toBe(false);
|
|
125
|
+
expect(result.errorCode).toBe('FETCH_FAILED');
|
|
126
|
+
});
|
|
127
|
+
it('VALIDATION_FAILED when SKILL.md is malformed/short', async () => {
|
|
128
|
+
const mockFetch = vi.mocked(fetch);
|
|
129
|
+
mockFetch.mockImplementation(async (url) => {
|
|
130
|
+
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
131
|
+
if (urlStr.includes('SKILL.md')) {
|
|
132
|
+
return new Response(SHORT_SKILL_MD, { status: 200 });
|
|
133
|
+
}
|
|
134
|
+
return new Response('Not found', { status: 404 });
|
|
135
|
+
});
|
|
136
|
+
const service = createService(db);
|
|
137
|
+
const result = await service.install('https://github.com/owner/short-repo');
|
|
138
|
+
expect(result.success).toBe(false);
|
|
139
|
+
expect(result.errorCode).toBe('VALIDATION_FAILED');
|
|
140
|
+
});
|
|
141
|
+
it('ALREADY_INSTALLED on repeat install without force', async () => {
|
|
142
|
+
const mockFetch = vi.mocked(fetch);
|
|
143
|
+
mockFetch.mockImplementation(async (url) => {
|
|
144
|
+
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
145
|
+
if (urlStr.includes('SKILL.md')) {
|
|
146
|
+
return new Response(VALID_SKILL_MD, { status: 200 });
|
|
147
|
+
}
|
|
148
|
+
return new Response('Not found', { status: 404 });
|
|
149
|
+
});
|
|
150
|
+
const service = createService(db);
|
|
151
|
+
const first = await service.install('https://github.com/owner/dup-repo');
|
|
152
|
+
expect(first.success).toBe(true);
|
|
153
|
+
const second = await service.install('https://github.com/owner/dup-repo');
|
|
154
|
+
expect(second.success).toBe(false);
|
|
155
|
+
expect(second.errorCode).toBe('ALREADY_INSTALLED');
|
|
156
|
+
});
|
|
157
|
+
it('SKIP_SCAN_FORBIDDEN when skipScan requested on unknown tier', async () => {
|
|
158
|
+
const mockFetch = vi.mocked(fetch);
|
|
159
|
+
mockFetch.mockImplementation(async (url) => {
|
|
160
|
+
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
161
|
+
if (urlStr.includes('SKILL.md')) {
|
|
162
|
+
return new Response(VALID_SKILL_MD, { status: 200 });
|
|
163
|
+
}
|
|
164
|
+
return new Response('Not found', { status: 404 });
|
|
165
|
+
});
|
|
166
|
+
// Direct GitHub URL → trust tier defaults to 'unknown', skipScan disallowed
|
|
167
|
+
const service = createService(db);
|
|
168
|
+
const result = await service.install('https://github.com/owner/skip-repo', {
|
|
169
|
+
skipScan: true,
|
|
170
|
+
});
|
|
171
|
+
expect(result.success).toBe(false);
|
|
172
|
+
expect(result.errorCode).toBe('SKIP_SCAN_FORBIDDEN');
|
|
173
|
+
expect(result.trustTier).toBe('unknown');
|
|
174
|
+
});
|
|
175
|
+
it('successful install does NOT carry errorCode', async () => {
|
|
176
|
+
const mockFetch = vi.mocked(fetch);
|
|
177
|
+
mockFetch.mockImplementation(async (url) => {
|
|
178
|
+
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
179
|
+
if (urlStr.includes('SKILL.md')) {
|
|
180
|
+
return new Response(VALID_SKILL_MD, { status: 200 });
|
|
181
|
+
}
|
|
182
|
+
return new Response('Not found', { status: 404 });
|
|
183
|
+
});
|
|
184
|
+
const service = createService(db);
|
|
185
|
+
const result = await service.install('https://github.com/owner/ok-repo');
|
|
186
|
+
expect(result.success).toBe(true);
|
|
187
|
+
expect(result.errorCode).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
//# sourceMappingURL=skill-installation.service.error-codes.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-installation.service.error-codes.test.js","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.service.error-codes.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5894 Wave 1 Step 3: multi-client manifest re-keying
|
|
3
|
+
*
|
|
4
|
+
* getInstalledSkills() dedupes same-named skills across clients with
|
|
5
|
+
* Claude Code precedence; before this fix, SkillInstallationService wrote
|
|
6
|
+
* every manifest entry under a bare skill-name key regardless of client,
|
|
7
|
+
* so installing the same skill name under two clients silently collided
|
|
8
|
+
* (the second install's manifest write overwrote the first). This suite
|
|
9
|
+
* is the required cross-cutting regression scenario from the plan's
|
|
10
|
+
* Verification section (both Wave 1 and Wave 2 require coverage).
|
|
11
|
+
*
|
|
12
|
+
* Split out of skill-installation.service.test.ts (CLAUDE.md's 500-line
|
|
13
|
+
* file cap) rather than left inline -- this scenario is also cohesive
|
|
14
|
+
* enough to stand alone.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=skill-installation.service.multi-client.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-installation.service.multi-client.test.d.ts","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.service.multi-client.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5894 Wave 1 Step 3: multi-client manifest re-keying
|
|
3
|
+
*
|
|
4
|
+
* getInstalledSkills() dedupes same-named skills across clients with
|
|
5
|
+
* Claude Code precedence; before this fix, SkillInstallationService wrote
|
|
6
|
+
* every manifest entry under a bare skill-name key regardless of client,
|
|
7
|
+
* so installing the same skill name under two clients silently collided
|
|
8
|
+
* (the second install's manifest write overwrote the first). This suite
|
|
9
|
+
* is the required cross-cutting regression scenario from the plan's
|
|
10
|
+
* Verification section (both Wave 1 and Wave 2 require coverage).
|
|
11
|
+
*
|
|
12
|
+
* Split out of skill-installation.service.test.ts (CLAUDE.md's 500-line
|
|
13
|
+
* file cap) rather than left inline -- this scenario is also cohesive
|
|
14
|
+
* enough to stand alone.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
17
|
+
import * as fs from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import { SkillInstallationService } from '../../../src/services/skill-installation.service.js';
|
|
21
|
+
import { SkillRepository } from '../../../src/repositories/SkillRepository.js';
|
|
22
|
+
import { SkillDependencyRepository } from '../../../src/repositories/SkillDependencyRepository.js';
|
|
23
|
+
import { createTestDatabase } from '../../helpers/database.js';
|
|
24
|
+
const VALID_SKILL_MD = `---
|
|
25
|
+
name: test-skill
|
|
26
|
+
description: A test skill for unit testing
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Test Skill
|
|
30
|
+
|
|
31
|
+
This is a valid skill file with enough content to pass the 100-character minimum
|
|
32
|
+
validation threshold that the service checks during installation.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Use this skill by saying "Use the test-skill skill to..."
|
|
37
|
+
`;
|
|
38
|
+
let tmpDir;
|
|
39
|
+
let skillsDir;
|
|
40
|
+
let manifestPath;
|
|
41
|
+
async function createTmpDirs() {
|
|
42
|
+
tmpDir = path.join(os.tmpdir(), 'skillsmith-test-' + Date.now() + '-' + Math.random().toString(36).slice(2));
|
|
43
|
+
skillsDir = path.join(tmpDir, 'skills');
|
|
44
|
+
manifestPath = path.join(tmpDir, 'manifest.json');
|
|
45
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
async function cleanupTmpDirs() {
|
|
48
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
49
|
+
}
|
|
50
|
+
function createService(db, overrides = {}) {
|
|
51
|
+
const skillRepo = new SkillRepository(db);
|
|
52
|
+
const skillDependencyRepo = new SkillDependencyRepository(db);
|
|
53
|
+
return new SkillInstallationService({
|
|
54
|
+
db,
|
|
55
|
+
skillRepo,
|
|
56
|
+
skillDependencyRepo,
|
|
57
|
+
skillsDir,
|
|
58
|
+
manifestPath,
|
|
59
|
+
...overrides,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
describe('SMI-5894 Wave 1 Step 3: multi-client manifest re-keying', () => {
|
|
63
|
+
let db;
|
|
64
|
+
let cursorSkillsDir;
|
|
65
|
+
beforeEach(async () => {
|
|
66
|
+
db = await createTestDatabase();
|
|
67
|
+
await createTmpDirs();
|
|
68
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
69
|
+
const mockFetch = vi.mocked(fetch);
|
|
70
|
+
mockFetch.mockImplementation(async (url) => {
|
|
71
|
+
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
72
|
+
if (urlStr.includes('SKILL.md')) {
|
|
73
|
+
return new Response(VALID_SKILL_MD, { status: 200 });
|
|
74
|
+
}
|
|
75
|
+
return new Response('Not found', { status: 404 });
|
|
76
|
+
});
|
|
77
|
+
cursorSkillsDir = path.join(tmpDir, 'cursor-skills');
|
|
78
|
+
await fs.mkdir(cursorSkillsDir, { recursive: true });
|
|
79
|
+
});
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
db.close();
|
|
82
|
+
await cleanupTmpDirs();
|
|
83
|
+
vi.restoreAllMocks();
|
|
84
|
+
});
|
|
85
|
+
it('installs the same skill name independently under two clients without collision', async () => {
|
|
86
|
+
const claudeService = createService(db, { client: 'claude-code' });
|
|
87
|
+
const cursorService = createService(db, { client: 'cursor', skillsDir: cursorSkillsDir });
|
|
88
|
+
const claudeResult = await claudeService.install('https://github.com/owner/test-repo', {
|
|
89
|
+
skipOptimize: true,
|
|
90
|
+
});
|
|
91
|
+
const cursorResult = await cursorService.install('https://github.com/owner/test-repo', {
|
|
92
|
+
skipOptimize: true,
|
|
93
|
+
});
|
|
94
|
+
expect(claudeResult.success).toBe(true);
|
|
95
|
+
expect(cursorResult.success).toBe(true);
|
|
96
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
97
|
+
// Canonical client keeps the legacy bare-name key (backward compat).
|
|
98
|
+
expect(manifest.installedSkills['test-repo']).toBeDefined();
|
|
99
|
+
expect(manifest.installedSkills['test-repo'].installPath).toBe(path.join(skillsDir, 'test-repo'));
|
|
100
|
+
expect(manifest.installedSkills['test-repo'].client).toBe('claude-code');
|
|
101
|
+
// Non-canonical client gets a distinct composite key — does NOT
|
|
102
|
+
// overwrite the claude-code entry above.
|
|
103
|
+
expect(manifest.installedSkills['test-repo::cursor']).toBeDefined();
|
|
104
|
+
expect(manifest.installedSkills['test-repo::cursor'].installPath).toBe(path.join(cursorSkillsDir, 'test-repo'));
|
|
105
|
+
expect(manifest.installedSkills['test-repo::cursor'].client).toBe('cursor');
|
|
106
|
+
// Both physical directories exist independently.
|
|
107
|
+
await expect(fs.access(path.join(skillsDir, 'test-repo'))).resolves.toBeUndefined();
|
|
108
|
+
await expect(fs.access(path.join(cursorSkillsDir, 'test-repo'))).resolves.toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
it('uninstalling the Cursor copy leaves the Claude Code copy untouched (and vice versa)', async () => {
|
|
111
|
+
const claudeService = createService(db, { client: 'claude-code' });
|
|
112
|
+
const cursorService = createService(db, { client: 'cursor', skillsDir: cursorSkillsDir });
|
|
113
|
+
await claudeService.install('https://github.com/owner/test-repo', { skipOptimize: true });
|
|
114
|
+
await cursorService.install('https://github.com/owner/test-repo', { skipOptimize: true });
|
|
115
|
+
const uninstallResult = await cursorService.uninstall('test-repo');
|
|
116
|
+
expect(uninstallResult.success).toBe(true);
|
|
117
|
+
// Cursor copy removed from disk and the manifest...
|
|
118
|
+
await expect(fs.access(path.join(cursorSkillsDir, 'test-repo'))).rejects.toThrow();
|
|
119
|
+
const manifestAfterCursorRemove = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
120
|
+
expect(manifestAfterCursorRemove.installedSkills['test-repo::cursor']).toBeUndefined();
|
|
121
|
+
// ...but the Claude Code copy is untouched on disk and in the manifest.
|
|
122
|
+
await expect(fs.access(path.join(skillsDir, 'test-repo'))).resolves.toBeUndefined();
|
|
123
|
+
expect(manifestAfterCursorRemove.installedSkills['test-repo']).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=skill-installation.service.multi-client.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-installation.service.multi-client.test.js","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.service.multi-client.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -312,6 +312,9 @@ describe('SMI-3483: SkillInstallationService', () => {
|
|
|
312
312
|
expect(manifest.installedSkills['test-repo'].source).toBe('github:owner/test-repo');
|
|
313
313
|
});
|
|
314
314
|
});
|
|
315
|
+
// SMI-5894 Wave 1 Step 3's multi-client manifest re-keying suite now
|
|
316
|
+
// lives in skill-installation.service.multi-client.test.ts (split out
|
|
317
|
+
// to stay under CLAUDE.md's 500-line file cap).
|
|
315
318
|
// ==========================================================================
|
|
316
319
|
// Uninstall
|
|
317
320
|
// ==========================================================================
|
|
@@ -338,117 +341,9 @@ describe('SMI-3483: SkillInstallationService', () => {
|
|
|
338
341
|
expect(forceResult.warning).toContain('not in the manifest');
|
|
339
342
|
});
|
|
340
343
|
});
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
// a code from InstallErrorCode. Telemetry (emitInstallEvent) reads this
|
|
346
|
-
// field; without it the install funnel cannot classify failures.
|
|
347
|
-
// ==========================================================================
|
|
348
|
-
describe('SMI-4795: install failures populate errorCode', () => {
|
|
349
|
-
beforeEach(() => {
|
|
350
|
-
vi.stubGlobal('fetch', vi.fn());
|
|
351
|
-
});
|
|
352
|
-
it('REGISTRY_LOOKUP_UNAVAILABLE when registry id supplied without lookup adapter', async () => {
|
|
353
|
-
const service = createService(db); // no registryLookup
|
|
354
|
-
const result = await service.install('author/some-skill');
|
|
355
|
-
expect(result.success).toBe(false);
|
|
356
|
-
expect(result.errorCode).toBe('REGISTRY_LOOKUP_UNAVAILABLE');
|
|
357
|
-
});
|
|
358
|
-
it('REGISTRY_SKILL_NOT_FOUND when registry returns null', async () => {
|
|
359
|
-
const service = createService(db, {
|
|
360
|
-
registryLookup: createMockRegistryLookup({}), // empty registry
|
|
361
|
-
});
|
|
362
|
-
const result = await service.install('author/missing-skill');
|
|
363
|
-
expect(result.success).toBe(false);
|
|
364
|
-
expect(result.errorCode).toBe('REGISTRY_SKILL_NOT_FOUND');
|
|
365
|
-
});
|
|
366
|
-
it('QUARANTINED when registry flags the skill', async () => {
|
|
367
|
-
const service = createService(db, {
|
|
368
|
-
registryLookup: createMockRegistryLookup({
|
|
369
|
-
'author/quarantined-skill': {
|
|
370
|
-
repoUrl: 'https://github.com/author/quarantined-skill',
|
|
371
|
-
name: 'quarantined-skill',
|
|
372
|
-
quarantined: true,
|
|
373
|
-
trustTier: 'community',
|
|
374
|
-
},
|
|
375
|
-
}),
|
|
376
|
-
});
|
|
377
|
-
const result = await service.install('author/quarantined-skill');
|
|
378
|
-
expect(result.success).toBe(false);
|
|
379
|
-
expect(result.errorCode).toBe('QUARANTINED');
|
|
380
|
-
expect(result.trustTier).toBe('community');
|
|
381
|
-
});
|
|
382
|
-
it('FETCH_FAILED when SKILL.md is unreachable', async () => {
|
|
383
|
-
const mockFetch = vi.mocked(fetch);
|
|
384
|
-
mockFetch.mockResolvedValue(new Response('Not found', { status: 404 }));
|
|
385
|
-
const service = createService(db);
|
|
386
|
-
const result = await service.install('https://github.com/owner/missing-repo');
|
|
387
|
-
expect(result.success).toBe(false);
|
|
388
|
-
expect(result.errorCode).toBe('FETCH_FAILED');
|
|
389
|
-
});
|
|
390
|
-
it('VALIDATION_FAILED when SKILL.md is malformed/short', async () => {
|
|
391
|
-
const mockFetch = vi.mocked(fetch);
|
|
392
|
-
mockFetch.mockImplementation(async (url) => {
|
|
393
|
-
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
394
|
-
if (urlStr.includes('SKILL.md')) {
|
|
395
|
-
return new Response(SHORT_SKILL_MD, { status: 200 });
|
|
396
|
-
}
|
|
397
|
-
return new Response('Not found', { status: 404 });
|
|
398
|
-
});
|
|
399
|
-
const service = createService(db);
|
|
400
|
-
const result = await service.install('https://github.com/owner/short-repo');
|
|
401
|
-
expect(result.success).toBe(false);
|
|
402
|
-
expect(result.errorCode).toBe('VALIDATION_FAILED');
|
|
403
|
-
});
|
|
404
|
-
it('ALREADY_INSTALLED on repeat install without force', async () => {
|
|
405
|
-
const mockFetch = vi.mocked(fetch);
|
|
406
|
-
mockFetch.mockImplementation(async (url) => {
|
|
407
|
-
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
408
|
-
if (urlStr.includes('SKILL.md')) {
|
|
409
|
-
return new Response(VALID_SKILL_MD, { status: 200 });
|
|
410
|
-
}
|
|
411
|
-
return new Response('Not found', { status: 404 });
|
|
412
|
-
});
|
|
413
|
-
const service = createService(db);
|
|
414
|
-
const first = await service.install('https://github.com/owner/dup-repo');
|
|
415
|
-
expect(first.success).toBe(true);
|
|
416
|
-
const second = await service.install('https://github.com/owner/dup-repo');
|
|
417
|
-
expect(second.success).toBe(false);
|
|
418
|
-
expect(second.errorCode).toBe('ALREADY_INSTALLED');
|
|
419
|
-
});
|
|
420
|
-
it('SKIP_SCAN_FORBIDDEN when skipScan requested on unknown tier', async () => {
|
|
421
|
-
const mockFetch = vi.mocked(fetch);
|
|
422
|
-
mockFetch.mockImplementation(async (url) => {
|
|
423
|
-
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
424
|
-
if (urlStr.includes('SKILL.md')) {
|
|
425
|
-
return new Response(VALID_SKILL_MD, { status: 200 });
|
|
426
|
-
}
|
|
427
|
-
return new Response('Not found', { status: 404 });
|
|
428
|
-
});
|
|
429
|
-
// Direct GitHub URL → trust tier defaults to 'unknown', skipScan disallowed
|
|
430
|
-
const service = createService(db);
|
|
431
|
-
const result = await service.install('https://github.com/owner/skip-repo', {
|
|
432
|
-
skipScan: true,
|
|
433
|
-
});
|
|
434
|
-
expect(result.success).toBe(false);
|
|
435
|
-
expect(result.errorCode).toBe('SKIP_SCAN_FORBIDDEN');
|
|
436
|
-
expect(result.trustTier).toBe('unknown');
|
|
437
|
-
});
|
|
438
|
-
it('successful install does NOT carry errorCode', async () => {
|
|
439
|
-
const mockFetch = vi.mocked(fetch);
|
|
440
|
-
mockFetch.mockImplementation(async (url) => {
|
|
441
|
-
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
442
|
-
if (urlStr.includes('SKILL.md')) {
|
|
443
|
-
return new Response(VALID_SKILL_MD, { status: 200 });
|
|
444
|
-
}
|
|
445
|
-
return new Response('Not found', { status: 404 });
|
|
446
|
-
});
|
|
447
|
-
const service = createService(db);
|
|
448
|
-
const result = await service.install('https://github.com/owner/ok-repo');
|
|
449
|
-
expect(result.success).toBe(true);
|
|
450
|
-
expect(result.errorCode).toBeUndefined();
|
|
451
|
-
});
|
|
452
|
-
});
|
|
344
|
+
// SMI-4795's errorCode-taxonomy suite now lives in
|
|
345
|
+
// skill-installation.service.error-codes.test.ts (split out to stay
|
|
346
|
+
// under CLAUDE.md's 500-line file cap once SMI-5894 Wave 1 added its
|
|
347
|
+
// own multi-client suite here).
|
|
453
348
|
});
|
|
454
349
|
//# sourceMappingURL=skill-installation.service.test.js.map
|