mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* Add DeepSeek Harness as a supported skills provider npx impeccable install now detects ~/.dsh (or $DSH_HOME when it sits under home) and installs into ~/.dsh/skills, the user-level skill root DeepSeek Harness scans, with project-level .dsh/skills on the same layout as other providers. Aliases: dsh, deepseek, deepseek-harness. Engine: PROVIDER_DIRS / aliases / display / input order / global hint, $DSH_HOME-aware user skills dir, provider id resolution from the skill dir, pin harness dirs, bundle path normalization for hashing. Build: dsh transformer target emitting the frontmatter DeepSeek Harness reads (user-invocable, license, compatibility, metadata; unknown keys are ignored there) with no emitHooks (DSH hooks are in-process plugins, not on-disk manifests) and no agentFormat (no documented on-disk subagent format); placeholders (AGENTS.md config file, ask_user_question tool, / command prefix), provider block tags, universal README entry. Docs: HARNESSES.md row and frontmatter column, CLI-CONTRACT constants, README/DEVELOP/AGENTS provider lists. Validation: cargo test --workspace; node scripts/run-tests.mjs core (138 pass); bun run build (19 providers, dist/dsh artifact verified); engine smoke against a fake HOME with a local bundle: install --providers=dsh --scope=global, auto-detected install, and update all resolve the .dsh provider. Generated provider output intentionally omitted per repo policy; the sync workflow regenerates tracked .dsh/skills after merge. Prepared with AI assistance (DeepSeek Harness coding agent). * Address review: DSH_HOME-only detection, generated-output pathspecs - Detect DeepSeek Harness through the resolved $DSH_HOME (fallback ~/.dsh) instead of gating on a fixed ~/.dsh path, so a DSH_HOME-only setup is offered by a provider-less install; generalize the two env-relocated config-dir hints (OpenCode, DSH) into one shared probe. - Add .dsh to the sync workflow's GENERATED_PATHS and CI's generated drift check so the tracked .dsh/skills payload is committed and validated. - Cover both behaviors: new install_detection_tests (DSH_HOME-only, default ~/.dsh, refused outside-home override) and a CLI-CONTRACT note on the resolved detection path. Validation: cargo test --workspace; node scripts/run-tests.mjs core (138 pass); engine smoke: DSH_HOME-only fake HOME installs globally into the resolved skills dir. Prepared with AI assistance (DeepSeek Harness coding agent). * Fix DeepSeek Harness home paths on Windows Use native relative-path containment, cover case and drive boundaries, and verify relocated global install/update without changing project skills. Add DSH output coverage and correct the install documentation. AI assistance: Codex, under pbakaus maintainer direction. * Document the CLI limit on external DSH homes Clarify that outside-home manual copies are not detected or updated by the CLI. AI assistance: Codex, under pbakaus maintainer direction. --------- Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com>
221 lines
9.7 KiB
JavaScript
221 lines
9.7 KiB
JavaScript
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { PROVIDERS } from '../../../scripts/lib/transformers/providers.js';
|
|
import { createTransformer } from '../../../scripts/lib/transformers/factory.js';
|
|
import { parseFrontmatter, PROVIDER_PLACEHOLDERS } from '../../../scripts/lib/utils.js';
|
|
|
|
const TEST_DIR = path.join(process.cwd(), 'test-tmp-providers');
|
|
|
|
function providerTestDir(provider, suffix = '') {
|
|
return path.join(TEST_DIR, `${provider}${suffix}`);
|
|
}
|
|
|
|
function skillPath(config, skillName, suffix = '') {
|
|
return path.join(TEST_DIR, `${config.provider}${suffix}/${config.configDir}/skills/${skillName}/SKILL.md`);
|
|
}
|
|
|
|
// Test every provider config
|
|
for (const [key, config] of Object.entries(PROVIDERS)) {
|
|
describe(`Provider: ${config.displayName} (${key})`, () => {
|
|
const transform = createTransformer(config);
|
|
|
|
beforeEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should create correct directory structure', () => {
|
|
transform([], TEST_DIR);
|
|
expect(fs.existsSync(path.join(TEST_DIR, `${config.provider}/${config.configDir}/skills`))).toBe(true);
|
|
});
|
|
|
|
test('should replace {{model}} placeholder correctly', () => {
|
|
const skills = [{ name: 'test', description: 'Test', body: 'Ask {{model}} for help.' }];
|
|
transform(skills, TEST_DIR);
|
|
const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8');
|
|
const expected = PROVIDER_PLACEHOLDERS[config.placeholderProvider || config.provider].model;
|
|
expect(content).toContain(`Ask ${expected} for help.`);
|
|
});
|
|
|
|
test('should replace {{config_file}} placeholder correctly', () => {
|
|
const skills = [{ name: 'test', description: 'Test', body: 'See {{config_file}}.' }];
|
|
transform(skills, TEST_DIR);
|
|
const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8');
|
|
const expected = PROVIDER_PLACEHOLDERS[config.placeholderProvider || config.provider].config_file;
|
|
expect(content).toContain(`See ${expected}.`);
|
|
});
|
|
|
|
test('should copy reference files', () => {
|
|
const skills = [{
|
|
name: 'test',
|
|
description: 'Test',
|
|
body: 'Body',
|
|
references: [{ name: 'ref', content: 'Ref content', filePath: '/fake/ref.md' }]
|
|
}];
|
|
transform(skills, TEST_DIR);
|
|
const refPath = path.join(TEST_DIR, `${config.provider}/${config.configDir}/skills/test/reference/ref.md`);
|
|
expect(fs.existsSync(refPath)).toBe(true);
|
|
});
|
|
|
|
if (key === 'dsh') {
|
|
test('uses DSH tools and resource paths without installing unsupported hooks or agents', () => {
|
|
transform([{
|
|
name: 'impeccable',
|
|
description: 'Test',
|
|
userInvocable: true,
|
|
allowedTools: 'Bash',
|
|
body: '{{ask_instruction}} Run `{{scripts_path}}/impeccable context`.',
|
|
references: [{ name: 'polish', content: 'Run `{{scripts_path}}/impeccable detect`.', filePath: '/fake/polish.md' }],
|
|
}], TEST_DIR);
|
|
const content = fs.readFileSync(skillPath(config, 'impeccable'), 'utf8');
|
|
expect(content).toContain('call the ask_user_question tool');
|
|
expect(content).toContain('.dsh/skills/impeccable/scripts/impeccable context');
|
|
expect(parseFrontmatter(content).frontmatter['allowed-tools']).toBeUndefined();
|
|
const root = path.join(TEST_DIR, 'dsh', '.dsh');
|
|
expect(fs.readFileSync(path.join(root, 'skills/impeccable/reference/polish.md'), 'utf8'))
|
|
.toContain('.dsh/skills/impeccable/scripts/impeccable detect');
|
|
expect(fs.readdirSync(root)).toEqual(['skills']);
|
|
expect(config.emitHooks).toBeUndefined();
|
|
expect(config.agentFormat).toBeUndefined();
|
|
});
|
|
}
|
|
|
|
test('should emit skillsVersion in generated skill frontmatter', () => {
|
|
const skills = [{ name: 'test', description: 'Test', body: 'Body' }];
|
|
transform(skills, TEST_DIR, { skillsVersion: '1.2.3-test' });
|
|
const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8');
|
|
const parsed = parseFrontmatter(content);
|
|
if (key === 'codex' || key === 'agents') {
|
|
expect(parsed.frontmatter.version).toBeUndefined();
|
|
expect(content).toContain('metadata:\n version: 1.2.3-test');
|
|
} else {
|
|
expect(parsed.frontmatter.version).toBe('1.2.3-test');
|
|
}
|
|
});
|
|
|
|
// Field-specific tests based on provider config
|
|
if (config.frontmatterFields.includes('user-invocable')) {
|
|
test('should emit user-invocable for user-invocable skills', () => {
|
|
const skills = [{ name: 'test', description: 'Test', userInvocable: true, body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8'));
|
|
expect(parsed.frontmatter['user-invocable']).toBe(true);
|
|
});
|
|
|
|
test('should omit user-invocable for non-user-invocable skills', () => {
|
|
const skills = [{ name: 'test', description: 'Test', userInvocable: false, body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8'));
|
|
expect(parsed.frontmatter['user-invocable']).toBeUndefined();
|
|
});
|
|
}
|
|
|
|
if (config.frontmatterFields.includes('argument-hint')) {
|
|
test('should emit argument-hint for user-invocable skills', () => {
|
|
const skills = [{ name: 'test', description: 'Test', userInvocable: true, argumentHint: '[target]', body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8'));
|
|
expect(parsed.frontmatter['argument-hint']).toBe('[target]');
|
|
});
|
|
|
|
test('should not emit argument-hint for non-user-invocable skills', () => {
|
|
const skills = [{ name: 'test', description: 'Test', userInvocable: false, argumentHint: '[target]', body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8'));
|
|
expect(parsed.frontmatter['argument-hint']).toBeUndefined();
|
|
});
|
|
}
|
|
|
|
if (config.frontmatterFields.includes('license')) {
|
|
test('should emit license when present', () => {
|
|
const skills = [{ name: 'test', description: 'Test', license: 'MIT', body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8'));
|
|
expect(parsed.frontmatter.license).toBe('MIT');
|
|
});
|
|
|
|
test('should omit license when empty', () => {
|
|
const skills = [{ name: 'test', description: 'Test', license: '', body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8'));
|
|
expect(parsed.frontmatter.license).toBeUndefined();
|
|
});
|
|
}
|
|
|
|
if (config.frontmatterFields.includes('compatibility')) {
|
|
test('should emit compatibility when present', () => {
|
|
const skills = [{ name: 'test', description: 'Test', compatibility: config.provider, body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8');
|
|
expect(content).toContain(`compatibility: ${config.provider}`);
|
|
});
|
|
}
|
|
|
|
if (config.frontmatterFields.includes('metadata')) {
|
|
test('should emit metadata when present', () => {
|
|
const skills = [{ name: 'test', description: 'Test', metadata: 'some-metadata', body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8');
|
|
expect(content).toContain('metadata: some-metadata');
|
|
});
|
|
}
|
|
|
|
if (config.frontmatterFields.includes('allowed-tools')) {
|
|
test('should emit allowed-tools when present', () => {
|
|
const skills = [{ name: 'test', description: 'Test', allowedTools: 'Bash,Edit', body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8');
|
|
expect(content).toContain('allowed-tools: Bash,Edit');
|
|
});
|
|
}
|
|
|
|
// Fields NOT in this provider's allowlist should not appear
|
|
const allOptionalFields = ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'];
|
|
const excludedFields = allOptionalFields.filter(f => !config.frontmatterFields.includes(f));
|
|
|
|
if (excludedFields.length > 0) {
|
|
test('should not emit fields outside allowlist', () => {
|
|
const skills = [{
|
|
name: 'test',
|
|
description: 'Test',
|
|
userInvocable: true,
|
|
argumentHint: '[target]',
|
|
license: 'MIT',
|
|
compatibility: 'all',
|
|
metadata: 'meta',
|
|
allowedTools: 'Bash',
|
|
body: 'Body'
|
|
}];
|
|
transform(skills, TEST_DIR);
|
|
const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8');
|
|
|
|
for (const field of excludedFields) {
|
|
const yamlKey = field; // field names match yaml keys
|
|
// Check the raw content doesn't contain the yaml key
|
|
const lines = content.split('\n');
|
|
const frontmatterLines = [];
|
|
let inFrontmatter = false;
|
|
for (const line of lines) {
|
|
if (line === '---') {
|
|
if (inFrontmatter) break;
|
|
inFrontmatter = true;
|
|
continue;
|
|
}
|
|
if (inFrontmatter) frontmatterLines.push(line);
|
|
}
|
|
const hasForbiddenField = frontmatterLines.some(l => l.startsWith(`${yamlKey}:`));
|
|
expect(hasForbiddenField).toBe(false);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|