chore: bump version to 2.80.27 and update core features
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Unit tests for FileSystemSkillInjectionService.
|
||||
*
|
||||
* Strategy: drive the service against a real temp directory so path-traversal
|
||||
* defenses and writeFileSync paths are exercised end-to-end. The service
|
||||
* accepts a fs override but the prod path uses node:fs — testing real fs
|
||||
* gives stronger confidence here.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
FileSystemSkillInjectionService,
|
||||
SkillInjectionError,
|
||||
sanitizeSkillName,
|
||||
} from '../src/skills/skillInjectionService';
|
||||
|
||||
function makeTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'astra-skill-test-'));
|
||||
}
|
||||
|
||||
describe('sanitizeSkillName', () => {
|
||||
test('keeps ASCII alphanumerics and -_.', () => {
|
||||
expect(sanitizeSkillName('frontend_expert-v2.alpha')).toBe('frontend_expert-v2.alpha');
|
||||
});
|
||||
|
||||
test('strips .md extension', () => {
|
||||
expect(sanitizeSkillName('foo.md')).toBe('foo');
|
||||
expect(sanitizeSkillName('foo.markdown')).toBe('foo');
|
||||
});
|
||||
|
||||
test('replaces invalid chars with underscore', () => {
|
||||
expect(sanitizeSkillName('hello world!')).toBe('hello_world');
|
||||
expect(sanitizeSkillName('foo/bar')).toBe('foo_bar');
|
||||
expect(sanitizeSkillName('파이썬')).toBe('');
|
||||
});
|
||||
|
||||
test('strips leading/trailing dots and underscores', () => {
|
||||
expect(sanitizeSkillName('___foo___')).toBe('foo');
|
||||
expect(sanitizeSkillName('...foo...')).toBe('foo');
|
||||
});
|
||||
|
||||
test('returns empty for path-traversal attempts', () => {
|
||||
// ".." after stripping leading dots collapses to ""
|
||||
expect(sanitizeSkillName('..')).toBe('');
|
||||
expect(sanitizeSkillName('../etc/passwd')).toBe('etc_passwd');
|
||||
});
|
||||
|
||||
test('caps length at 80', () => {
|
||||
const long = 'a'.repeat(200);
|
||||
expect(sanitizeSkillName(long).length).toBe(80);
|
||||
});
|
||||
|
||||
test('rejects blank / non-string input', () => {
|
||||
expect(sanitizeSkillName('')).toBe('');
|
||||
expect(sanitizeSkillName(' ')).toBe('');
|
||||
expect(sanitizeSkillName(undefined as any)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FileSystemSkillInjectionService.inject', () => {
|
||||
let dir: string;
|
||||
let service: FileSystemSkillInjectionService;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = makeTempDir();
|
||||
service = new FileSystemSkillInjectionService({
|
||||
resolveSkillsDir: () => dir,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
});
|
||||
|
||||
test('writes <name>.md and <name>.meta.json', async () => {
|
||||
const result = await service.inject({
|
||||
name: 'react_expert',
|
||||
content: '# React Expert\n\nKnow React deeply.',
|
||||
displayName: 'React Expert',
|
||||
description: 'React specialist agent.',
|
||||
source: 'ezer',
|
||||
});
|
||||
expect(result.safeName).toBe('react_expert');
|
||||
expect(fs.existsSync(result.filePath)).toBe(true);
|
||||
expect(fs.existsSync(result.metaPath)).toBe(true);
|
||||
|
||||
const md = fs.readFileSync(result.filePath, 'utf8');
|
||||
expect(md).toContain('# React Expert');
|
||||
|
||||
const meta = JSON.parse(fs.readFileSync(result.metaPath, 'utf8'));
|
||||
expect(meta.name).toBe('react_expert');
|
||||
expect(meta.displayName).toBe('React Expert');
|
||||
expect(meta.description).toBe('React specialist agent.');
|
||||
expect(meta.injectedFrom).toBe('ezer');
|
||||
expect(meta.injectedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||
});
|
||||
|
||||
test('sanitizes the name on disk', async () => {
|
||||
const result = await service.inject({
|
||||
name: 'hello world!.md',
|
||||
content: 'body',
|
||||
});
|
||||
expect(result.safeName).toBe('hello_world');
|
||||
expect(path.basename(result.filePath)).toBe('hello_world.md');
|
||||
});
|
||||
|
||||
test('rejects empty name', async () => {
|
||||
await expect(service.inject({ name: '', content: 'body' }))
|
||||
.rejects.toMatchObject({ code: 'INVALID_NAME' });
|
||||
});
|
||||
|
||||
test('rejects empty content', async () => {
|
||||
await expect(service.inject({ name: 'foo', content: '' }))
|
||||
.rejects.toMatchObject({ code: 'EMPTY_CONTENT' });
|
||||
await expect(service.inject({ name: 'foo', content: ' ' }))
|
||||
.rejects.toMatchObject({ code: 'EMPTY_CONTENT' });
|
||||
});
|
||||
|
||||
test('rejects when skills dir cannot be resolved', async () => {
|
||||
const noDirService = new FileSystemSkillInjectionService({
|
||||
resolveSkillsDir: () => '',
|
||||
});
|
||||
await expect(noDirService.inject({ name: 'foo', content: 'body' }))
|
||||
.rejects.toMatchObject({ code: 'NO_SKILLS_DIR' });
|
||||
});
|
||||
|
||||
test('creates skills dir if it does not exist', async () => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
expect(fs.existsSync(dir)).toBe(false);
|
||||
const result = await service.inject({ name: 'foo', content: 'body' });
|
||||
expect(fs.existsSync(result.filePath)).toBe(true);
|
||||
});
|
||||
|
||||
test('falls back to safeName when displayName is blank', async () => {
|
||||
const result = await service.inject({ name: 'bare', content: 'body' });
|
||||
const meta = JSON.parse(fs.readFileSync(result.metaPath, 'utf8'));
|
||||
expect(meta.displayName).toBe('bare');
|
||||
expect(meta.injectedFrom).toBe('external');
|
||||
});
|
||||
|
||||
test('overwrites existing skill on repeated inject', async () => {
|
||||
await service.inject({ name: 'foo', content: 'v1' });
|
||||
const r2 = await service.inject({ name: 'foo', content: 'v2' });
|
||||
expect(fs.readFileSync(r2.filePath, 'utf8')).toBe('v2');
|
||||
});
|
||||
|
||||
test('fires onInjected hook on success', async () => {
|
||||
const calls: any[] = [];
|
||||
const hooked = new FileSystemSkillInjectionService({
|
||||
resolveSkillsDir: () => dir,
|
||||
onInjected: (result, req) => calls.push({ result, req }),
|
||||
});
|
||||
await hooked.inject({ name: 'hooked', content: 'body', displayName: 'Hooked Skill' });
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].result.safeName).toBe('hooked');
|
||||
expect(calls[0].req.displayName).toBe('Hooked Skill');
|
||||
});
|
||||
|
||||
test('SkillInjectionError is thrown for write failures', async () => {
|
||||
// Point at a path that exists but is not a directory — mkdirSync will
|
||||
// throw. The service should wrap it as WRITE_FAILED.
|
||||
const file = path.join(dir, 'notadir');
|
||||
fs.writeFileSync(file, 'x');
|
||||
const broken = new FileSystemSkillInjectionService({
|
||||
resolveSkillsDir: () => path.join(file, 'sub'),
|
||||
});
|
||||
await expect(broken.inject({ name: 'foo', content: 'body' }))
|
||||
.rejects.toMatchObject({ name: 'SkillInjectionError', code: 'WRITE_FAILED' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user