aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-skill-create.js
blob: 0c59f9da38d42dcc32f90de005005c2203a39aad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// modules/test/test-skill-create.js — run: node
// MAINT-024: skill-create must not report success when nothing was written, must reject an
// invalid name instead of rewriting it, and must not use an unreadable path as the body.
const fsMod = require('fs'), path = require('path');
const ROOT = path.join(__dirname, '..', '..');
const store = {};
global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } };
global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } };
global.window = global;

const load = (rel, cls) => new Function(fsMod.readFileSync(path.join(ROOT, rel), 'utf8') + '\nreturn ' + cls + ';')();
const VFS = load('modules/core/virtual-fs.js', 'VirtualFileSystem');
const SkillPack = load('modules/ext/skill-pack-core.js', 'SkillPackCoreModule');

function mk(opts) {
    const out = [];
    const vfs = new VFS();
    vfs.saveToStorage = () => true;
    if (opts && opts.breakWrite) vfs.writeFile = () => false;
    const sp = Object.create(SkillPack.prototype);
    sp.terminal = { print: (m, t) => out.push({ text: String(m), type: t || '' }), fs: vfs };
    sp.commands = {};
    return { sp, vfs, out };
}
const text = out => out.map(o => o.text).join(' | ');

let fails = 0, passes = 0;
function ok(name, cond, detail) {
    if (cond) { passes++; return; }
    fails++;
    console.log('FAIL ' + name + (detail ? ' — ' + detail : ''));
}

// A. first skill on a VFS with no /skills directory: created, for real
{
    const { sp, vfs, out } = mk();
    sp._cmdSkillCreate(['first-skill', 'does a thing', 'body text here']);
    ok('A1 success message printed', /✅ Skill "first-skill" created/.test(text(out)), text(out));
    ok('A2 the file exists', typeof vfs.readFile('/skills/first-skill.md') === 'string');
    ok('A3 the index was written', !!vfs.fsReadJSON('/skills/index.json')['first-skill']);
}

// B. an invalid name is refused, not silently rewritten
{
    const { sp, vfs, out } = mk();
    // Uppercase is a predictable normalisation and is reported back in the success line.
    sp._cmdSkillCreate(['My-Skill', 'desc', 'body']);
    ok('B1 uppercase is normalised and reported', /✅ Skill "my-skill" created/.test(text(out)), text(out));
    ok('B2 the old truncated name never appears', !/y-kill/.test(text(out)) && !vfs.readFile('/skills/y-kill.md'), text(out));
}
{
    const { sp, vfs, out } = mk();
    // An underscore cannot be normalised away, so the name is rejected outright.
    sp._cmdSkillCreate(['DevOps_Thing', 'desc', 'body']);
    ok('B3 invalid name refused', /Invalid skill name "DevOps_Thing"/.test(text(out)), text(out));
    ok('B4 nothing was created under a guessed name', vfs.readFile('/skills/devops_thing.md') === null && vfs.readFile('/skills/evpshing.md') === null);
    ok('B5 no success line', !/✅/.test(text(out)), text(out));
}

// C. a path that cannot be read is an error, not a body
{
    const { sp, vfs, out } = mk();
    sp._cmdSkillCreate(['path-body', 'desc', '/skills/does-not-exist.md']);
    ok('C1 unreadable body refused', /Cannot read skill body/.test(text(out)), text(out));
    ok('C2 nothing created', vfs.readFile('/skills/path-body.md') === null);
}

// D. a failed write must not print ✅
{
    const { sp, out } = mk({ breakWrite: true });
    sp._cmdSkillCreate(['blocked-skill', 'desc', 'body']);
    ok('D1 failed write is reported', /Failed to write \/skills\/blocked-skill\.md/.test(text(out)), text(out));
    ok('D2 no success line on a failed write', !/✅/.test(text(out)), text(out));
}

// E. a readable path body is used verbatim, comma text preserved
{
    const { sp, vfs, out } = mk();
    vfs.mkdirRecursive('/skills');
    vfs.writeFile('/body.md', 'line one, with a comma\nline two\nline three');
    sp._cmdSkillCreate(['from-file', 'desc', '/body.md', '--tags=a,b', '--category=testing']);
    const content = vfs.readFile('/skills/from-file.md') || '';
    ok('E1 path body used verbatim', content.includes('line one, with a comma\nline two\nline three'), content.slice(0, 120));
    ok('E2 tags parsed', content.includes('tags: [a, b]') && content.includes('category: testing'), content.slice(0, 160));
    ok('E3 success printed', /✅ Skill "from-file" created/.test(text(out)), text(out));
}

// F. too few arguments still shows usage
{
    const { sp, out } = mk();
    sp._cmdSkillCreate(['only-name', 'desc']);
    ok('F1 usage shown', /Usage: skill-create/.test(text(out)), text(out));
}

if (fails === 0) {
    console.log('test-skill-create: ' + passes + ' passed, 0 failed');
} else {
    console.log('test-skill-create: ' + passes + ' passed, ' + fails + ' FAILED');
    process.exit(1);
}