feat: sync shared skills contract and graph-first discipline
This commit is contained in:
@@ -2,17 +2,17 @@ import { Scenario } from '../types';
|
||||
|
||||
export const scenario: Scenario = {
|
||||
id: 'skill-guided-write',
|
||||
name: 'Skill-guided graph write',
|
||||
description: 'Explicit policy-guided graph work should read the DB policy skill, then either create the requested node+edge or correctly reuse the existing node+edge without duplicating them.',
|
||||
tools: ['readSkill', 'queryNodes', 'createNode', 'createEdge'],
|
||||
suites: ['skills', 'internal'],
|
||||
name: 'Skill-guided skill authoring',
|
||||
description: 'Explicit skill-authoring work should read the create-skill doctrine, then write the requested reusable skill cleanly.',
|
||||
categories: ['skills'],
|
||||
tools: ['readSkill', 'writeSkill'],
|
||||
input: {
|
||||
message: 'Using your DB operations policy, create a node titled "Eval: SQLite-first retrieval audit" with an explicit description and connect it to "Building RA-H — Personal Knowledge Graph" with explanation "Improves RA-H retrieval architecture."',
|
||||
message: 'Using your create-skill guidance, create a new skill called "capture-source" for repeatable workflows where the user wants to preserve raw source text while adding a strong description.',
|
||||
},
|
||||
expect: {
|
||||
skillsReadSoft: ['db-operations'],
|
||||
toolsCalledSoft: ['readSkill'],
|
||||
responseContainsSoft: ['SQLite-first retrieval audit'],
|
||||
skillsReadSoft: ['create-skill'],
|
||||
toolsCalledSoft: ['readSkill', 'writeSkill'],
|
||||
responseContainsSoft: ['capture-source'],
|
||||
maxLatencyMs: 35000,
|
||||
maxTotalTokens: 12000,
|
||||
maxEstimatedCostUsd: 0.12,
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
let tempRoot: string;
|
||||
let tempHome: string;
|
||||
let dbPath: string;
|
||||
|
||||
function createStandaloneDb(targetPath: string) {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
const db = new Database(targetPath);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE nodes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
source TEXT,
|
||||
link TEXT,
|
||||
event_date TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata TEXT,
|
||||
embedding BLOB,
|
||||
embedding_updated_at TEXT,
|
||||
embedding_text TEXT,
|
||||
chunk_status TEXT DEFAULT 'not_chunked'
|
||||
);
|
||||
|
||||
CREATE TABLE edges (
|
||||
id INTEGER PRIMARY KEY,
|
||||
from_node_id INTEGER NOT NULL,
|
||||
to_node_id INTEGER NOT NULL,
|
||||
source TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
context TEXT,
|
||||
explanation TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE chunks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
node_id INTEGER NOT NULL,
|
||||
chunk_idx INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
embedding_type TEXT,
|
||||
metadata TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const insertNode = db.prepare(`
|
||||
INSERT INTO nodes (id, title, description, source, link, event_date, created_at, updated_at, metadata, chunk_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
insertNode.run(
|
||||
1,
|
||||
'Standalone Test Node',
|
||||
'This is a standalone MCP test node used for retrieval and orientation.',
|
||||
'A concrete source text about building a useful graph of atomic units of context.',
|
||||
null,
|
||||
null,
|
||||
now,
|
||||
now,
|
||||
JSON.stringify({ captured_by: 'human' }),
|
||||
'chunked'
|
||||
);
|
||||
|
||||
insertNode.run(
|
||||
2,
|
||||
'Connected Support Node',
|
||||
'A supporting node connected to the main standalone test node.',
|
||||
'Related source text for standalone MCP testing.',
|
||||
null,
|
||||
null,
|
||||
now,
|
||||
now,
|
||||
JSON.stringify({ captured_by: 'human' }),
|
||||
'not_chunked'
|
||||
);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO edges (id, from_node_id, to_node_id, source, created_at, context, explanation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
'test',
|
||||
now,
|
||||
JSON.stringify({ explanation: 'Supports the main test node.' }),
|
||||
'Supports the main test node.'
|
||||
);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO chunks (id, node_id, chunk_idx, text, embedding_type, metadata, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
'A concrete source text about building a useful graph of atomic units of context.',
|
||||
'text',
|
||||
JSON.stringify({}),
|
||||
now
|
||||
);
|
||||
|
||||
db.close();
|
||||
}
|
||||
|
||||
async function withStandaloneClient<T>(fn: (client: Client) => Promise<T>) {
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [path.join(process.cwd(), 'apps', 'mcp-server-standalone', 'index.js')],
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempHome,
|
||||
RAH_DB_PATH: dbPath,
|
||||
} as Record<string, string>,
|
||||
stderr: 'pipe',
|
||||
});
|
||||
|
||||
const client = new Client({ name: 'ra-h-standalone-contract-test', version: '1.0.0' });
|
||||
await client.connect(transport);
|
||||
|
||||
try {
|
||||
return await fn(client);
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
function getStructured<T>(result: unknown) {
|
||||
return (result as { structuredContent?: unknown }).structuredContent as T;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rah-standalone-test-'));
|
||||
tempHome = path.join(tempRoot, 'home');
|
||||
dbPath = path.join(tempHome, 'Library', 'Application Support', 'RA-H', 'db', 'rah.sqlite');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
createStandaloneDb(dbPath);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('standalone MCP server contract', () => {
|
||||
it('returns orientation data with skills and no guides leakage', async () => {
|
||||
await withStandaloneClient(async (client) => {
|
||||
const result = await client.callTool({
|
||||
name: 'getContext',
|
||||
arguments: {},
|
||||
});
|
||||
|
||||
const structured = getStructured<{
|
||||
stats: { nodeCount: number; edgeCount: number };
|
||||
hubNodes: Array<{ title: string }>;
|
||||
skills: Array<{ name: string }>;
|
||||
}>(result);
|
||||
|
||||
expect(structured.stats.nodeCount).toBe(2);
|
||||
expect(structured.skills).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Onboarding' }),
|
||||
expect.objectContaining({ name: 'Create Skill' }),
|
||||
expect.objectContaining({ name: 'Refine' }),
|
||||
]));
|
||||
expect(structured).not.toHaveProperty('guides');
|
||||
});
|
||||
});
|
||||
|
||||
it('supports direct lookup and broader retrieval in standalone MCP', async () => {
|
||||
await withStandaloneClient(async (client) => {
|
||||
const queryResult = await client.callTool({
|
||||
name: 'queryNodes',
|
||||
arguments: {
|
||||
query: 'Standalone Test Node',
|
||||
},
|
||||
});
|
||||
|
||||
const queryStructured = getStructured<{ count: number; nodes: Array<{ id: number; title: string }> }>(queryResult);
|
||||
expect(queryStructured.count).toBeGreaterThan(0);
|
||||
expect(queryStructured.nodes[0]?.title).toBe('Standalone Test Node');
|
||||
|
||||
const retrievalResult = await client.callTool({
|
||||
name: 'retrieveQueryContext',
|
||||
arguments: {
|
||||
query: 'help me think about atomic units of context',
|
||||
focused_node_id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const retrievalStructured = getStructured<{ focused_node_id: number | null; nodes: Array<{ id: number }> }>(retrievalResult);
|
||||
expect(retrievalStructured.focused_node_id).toBe(1);
|
||||
expect(retrievalStructured.nodes.some((node) => node.id === 1)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('lists, reads, writes, and deletes shared skills through standalone MCP', async () => {
|
||||
await withStandaloneClient(async (client) => {
|
||||
const listResult = await client.callTool({
|
||||
name: 'listSkills',
|
||||
arguments: {},
|
||||
});
|
||||
const listed = getStructured<{ count: number; skills: Array<{ name: string }> }>(listResult);
|
||||
expect(listed.count).toBe(3);
|
||||
expect(listed.skills).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Onboarding' }),
|
||||
expect.objectContaining({ name: 'Create Skill' }),
|
||||
expect.objectContaining({ name: 'Refine' }),
|
||||
]));
|
||||
|
||||
const readResult = await client.callTool({
|
||||
name: 'readSkill',
|
||||
arguments: { name: 'refine' },
|
||||
});
|
||||
const readStructured = getStructured<{ name: string; content: string }>(readResult);
|
||||
expect(readStructured.name).toBe('Refine');
|
||||
expect(readStructured.content).toContain('# Refine');
|
||||
|
||||
await client.callTool({
|
||||
name: 'writeSkill',
|
||||
arguments: {
|
||||
name: 'capture-source',
|
||||
content: '---\nname: Capture Source\ndescription: Preserve source carefully.\n---\n\n# Capture Source\n',
|
||||
},
|
||||
});
|
||||
|
||||
const afterWrite = await client.callTool({
|
||||
name: 'listSkills',
|
||||
arguments: {},
|
||||
});
|
||||
const afterWriteStructured = getStructured<{ skills: Array<{ name: string }> }>(afterWrite);
|
||||
expect(afterWriteStructured.skills).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Capture Source' }),
|
||||
]));
|
||||
|
||||
await client.callTool({
|
||||
name: 'deleteSkill',
|
||||
arguments: { name: 'capture-source' },
|
||||
});
|
||||
|
||||
const afterDelete = await client.callTool({
|
||||
name: 'listSkills',
|
||||
arguments: {},
|
||||
});
|
||||
const afterDeleteStructured = getStructured<{ skills: Array<{ name: string }> }>(afterDelete);
|
||||
expect(afterDeleteStructured.skills.some((skill) => skill.name === 'Capture Source')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,7 @@ type RequestLogEntry = {
|
||||
let server: http.Server;
|
||||
let baseUrl = '';
|
||||
let nodes: NodeRecord[] = [];
|
||||
let skills: Array<{ name: string; description: string; immutable?: boolean; content?: string }> = [];
|
||||
let requestLog: RequestLogEntry[] = [];
|
||||
let nextNodeId = 1;
|
||||
|
||||
@@ -35,6 +36,11 @@ function nowIso(): string {
|
||||
|
||||
function resetState() {
|
||||
nodes = [];
|
||||
skills = [
|
||||
{ name: 'onboarding', description: 'Initial setup guidance.', immutable: false, content: '# onboarding\n' },
|
||||
{ name: 'create-skill', description: 'Create or rewrite a reusable skill.', immutable: false, content: '# create-skill\n' },
|
||||
{ name: 'refine', description: 'Refine a node or small set of nodes.', immutable: false, content: '# refine\n' },
|
||||
];
|
||||
requestLog = [];
|
||||
nextNodeId = 1;
|
||||
}
|
||||
@@ -168,10 +174,53 @@ async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'GET' && pathname === '/api/guides') {
|
||||
if (method === 'GET' && pathname === '/api/skills') {
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
data: [{ name: 'schema' }, { name: 'creating-nodes' }],
|
||||
data: skills.map(({ content, ...skill }) => skill),
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'POST' && pathname === '/api/skills') {
|
||||
const name = typeof body?.name === 'string' ? body.name : '';
|
||||
const content = typeof body?.content === 'string' ? body.content : '';
|
||||
const existing = skills.find((skill) => skill.name === name);
|
||||
|
||||
if (existing) {
|
||||
existing.content = content;
|
||||
} else {
|
||||
skills.push({ name, description: '', immutable: false, content });
|
||||
}
|
||||
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
message: `Skill "${name}" saved`,
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'GET' && pathname.startsWith('/api/skills/')) {
|
||||
const name = decodeURIComponent(pathname.split('/').pop() || '');
|
||||
const skill = skills.find((entry) => entry.name === name);
|
||||
if (!skill) {
|
||||
return sendJson(res, 404, { success: false, error: 'Skill not found.' });
|
||||
}
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
data: {
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
immutable: !!skill.immutable,
|
||||
content: skill.content || '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'DELETE' && pathname.startsWith('/api/skills/')) {
|
||||
const name = decodeURIComponent(pathname.split('/').pop() || '');
|
||||
skills = skills.filter((entry) => entry.name !== name);
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
message: `Skill "${name}" deleted`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -343,16 +392,74 @@ describe('stdio MCP server contract', () => {
|
||||
const graphStructured = getStructured<{
|
||||
stats: { nodeCount: number; edgeCount: number };
|
||||
hubNodes: Array<{ title: string }>;
|
||||
guides: string[];
|
||||
skills: Array<{ name: string }>;
|
||||
}>(graphContextResult);
|
||||
|
||||
expect(graphStructured.stats).toEqual({ nodeCount: 1, edgeCount: 3 });
|
||||
expect(graphStructured.hubNodes).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ title: 'Work Hub Node' })])
|
||||
);
|
||||
expect(graphStructured.guides).toEqual(expect.arrayContaining(['schema', 'creating-nodes']));
|
||||
expect(graphStructured.skills).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'onboarding' }),
|
||||
expect.objectContaining({ name: 'create-skill' }),
|
||||
expect.objectContaining({ name: 'refine' }),
|
||||
]));
|
||||
expect(graphStructured).not.toHaveProperty('contexts');
|
||||
expect(graphStructured.stats).not.toHaveProperty('contextCount');
|
||||
});
|
||||
});
|
||||
|
||||
it('lists, reads, writes, and deletes shared skills through packaged MCP', async () => {
|
||||
await withMcpClient(async (client) => {
|
||||
const listResult = await client.callTool({
|
||||
name: 'rah_list_skills',
|
||||
arguments: {},
|
||||
});
|
||||
const listed = getStructured<{ count: number; skills: Array<{ name: string }> }>(listResult);
|
||||
expect(listed.count).toBe(3);
|
||||
expect(listed.skills).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'onboarding' }),
|
||||
expect.objectContaining({ name: 'create-skill' }),
|
||||
expect.objectContaining({ name: 'refine' }),
|
||||
]));
|
||||
|
||||
const readResult = await client.callTool({
|
||||
name: 'rah_read_skill',
|
||||
arguments: { name: 'refine' },
|
||||
});
|
||||
const readStructured = getStructured<{ name: string; content: string }>(readResult);
|
||||
expect(readStructured.name).toBe('refine');
|
||||
expect(readStructured.content).toContain('# refine');
|
||||
|
||||
await client.callTool({
|
||||
name: 'rah_write_skill',
|
||||
arguments: {
|
||||
name: 'capture',
|
||||
content: '---\nname: capture\ndescription: Capture guidance\n---\n\nCapture skill body.',
|
||||
},
|
||||
});
|
||||
|
||||
const writeRequest = requestLog.find((entry) => entry.method === 'POST' && entry.pathname === '/api/skills');
|
||||
expect(writeRequest?.body).toMatchObject({
|
||||
name: 'capture',
|
||||
});
|
||||
|
||||
const afterWrite = await client.callTool({
|
||||
name: 'rah_list_skills',
|
||||
arguments: {},
|
||||
});
|
||||
const afterWriteStructured = getStructured<{ skills: Array<{ name: string }> }>(afterWrite);
|
||||
expect(afterWriteStructured.skills).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'capture' }),
|
||||
]));
|
||||
|
||||
await client.callTool({
|
||||
name: 'rah_delete_skill',
|
||||
arguments: { name: 'capture' },
|
||||
});
|
||||
|
||||
const deleteRequest = requestLog.find((entry) => entry.method === 'DELETE' && entry.pathname === '/api/skills/capture');
|
||||
expect(deleteRequest).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const SKILL_FIXTURES = {
|
||||
onboarding: `---
|
||||
name: Onboarding
|
||||
description: "Initial setup guidance."
|
||||
---
|
||||
|
||||
# Onboarding
|
||||
`,
|
||||
'create-skill': `---
|
||||
name: Create Skill
|
||||
description: "Create or rewrite a reusable skill."
|
||||
---
|
||||
|
||||
# Create Skill
|
||||
`,
|
||||
refine: `---
|
||||
name: Refine
|
||||
description: "Refine a node or small set of nodes."
|
||||
---
|
||||
|
||||
# Refine
|
||||
`,
|
||||
};
|
||||
|
||||
const LEGACY_AUDIT_SKILL = `---
|
||||
name: Audit
|
||||
description: "Legacy audit skill."
|
||||
---
|
||||
|
||||
# Audit
|
||||
`;
|
||||
|
||||
const CUSTOM_SKILL = `---
|
||||
name: Custom Capture
|
||||
description: "A custom user skill."
|
||||
---
|
||||
|
||||
# Custom Capture
|
||||
`;
|
||||
|
||||
let originalCwd: string;
|
||||
let tempRoot: string;
|
||||
let tempHome: string;
|
||||
|
||||
function writeFile(filepath: string, content: string) {
|
||||
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
||||
fs.writeFileSync(filepath, content, 'utf8');
|
||||
}
|
||||
|
||||
function createCanonicalSkillSet(rootDir: string) {
|
||||
const skillDir = path.join(rootDir, 'src/config/skills');
|
||||
for (const [name, content] of Object.entries(SKILL_FIXTURES)) {
|
||||
writeFile(path.join(skillDir, `${name}.md`), content);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkillService() {
|
||||
vi.resetModules();
|
||||
vi.doMock('os', () => ({
|
||||
default: { homedir: () => tempHome },
|
||||
homedir: () => tempHome,
|
||||
}));
|
||||
|
||||
return import('@/services/skills/skillService');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalCwd = process.cwd();
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rah-skill-root-'));
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'rah-skill-home-'));
|
||||
createCanonicalSkillSet(tempRoot);
|
||||
process.chdir(tempRoot);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
vi.doUnmock('os');
|
||||
vi.resetModules();
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('skillService', () => {
|
||||
it('seeds the canonical three-skill set into the live skills folder', async () => {
|
||||
const skillService = await loadSkillService();
|
||||
|
||||
const skills = skillService.listSkills();
|
||||
|
||||
expect(skills.map((skill) => skill.name)).toEqual([
|
||||
'Create Skill',
|
||||
'Onboarding',
|
||||
'Refine',
|
||||
]);
|
||||
|
||||
const liveSkillDir = path.join(tempHome, 'Library/Application Support/RA-H/skills');
|
||||
expect(fs.readdirSync(liveSkillDir).sort()).toEqual([
|
||||
'.seed-migrated-2026-03-07-skills-overhaul',
|
||||
'create-skill.md',
|
||||
'onboarding.md',
|
||||
'refine.md',
|
||||
]);
|
||||
});
|
||||
|
||||
it('hard-prunes retired built-in skills while preserving non-retired custom skills', async () => {
|
||||
const liveSkillDir = path.join(tempHome, 'Library/Application Support/RA-H/skills');
|
||||
writeFile(path.join(liveSkillDir, 'audit.md'), LEGACY_AUDIT_SKILL);
|
||||
writeFile(path.join(liveSkillDir, 'custom-capture.md'), CUSTOM_SKILL);
|
||||
|
||||
const skillService = await loadSkillService();
|
||||
|
||||
const skills = skillService.listSkills();
|
||||
|
||||
expect(skills.map((skill) => skill.name)).toEqual([
|
||||
'Create Skill',
|
||||
'Custom Capture',
|
||||
'Onboarding',
|
||||
'Refine',
|
||||
]);
|
||||
expect(fs.existsSync(path.join(liveSkillDir, 'audit.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(liveSkillDir, 'custom-capture.md'))).toBe(true);
|
||||
});
|
||||
|
||||
it('supports normalized skill CRUD against the live skills folder', async () => {
|
||||
const skillService = await loadSkillService();
|
||||
|
||||
const content = `---
|
||||
name: Capture Source
|
||||
description: "Preserve raw source while writing a strong description."
|
||||
---
|
||||
|
||||
# Capture Source
|
||||
`;
|
||||
|
||||
expect(skillService.writeSkill('Capture Source', content)).toEqual({ success: true });
|
||||
|
||||
const created = skillService.readSkill('capture-source');
|
||||
expect(created?.name).toBe('Capture Source');
|
||||
expect(created?.description).toContain('Preserve raw source');
|
||||
expect(created?.content).toContain('# Capture Source');
|
||||
|
||||
expect(skillService.deleteSkill('Capture Source')).toEqual({ success: true });
|
||||
expect(skillService.readSkill('capture-source')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user