Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"format": "npx prettier --config-precedence prefer-file --write . && eslint -c eslint.config.js . --fix",
"prepare": "husky",
"pretest": "npx eslint-config-prettier eslint.config.js",
"test": "find src -name '*.js' -exec node --check {} + && npx eslint -c eslint.config.js ."
"test": "find src -name '*.js' -exec node --check {} + && node --test test/**/*.test.js && npx eslint -c eslint.config.js ."
},
"devDependencies": {
"@github/prettier-config": "^0.0.6",
Expand Down
51 changes: 51 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {describe, test} from 'node:test'
import assert from 'node:assert/strict'
import {execFile} from 'node:child_process'
import {mkdtemp, rm, readFile, stat} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import {promisify} from 'node:util'

const execFileAsync = promisify(execFile)
const cliPath = path.resolve(import.meta.dirname, '../cli.js')

describe('CLI', () => {
test('shows help text for default command', async () => {
const {stdout, stderr} = await execFileAsync(process.execPath, [cliPath, '--help'])
assert.equal(stderr, '')
assert.match(stdout, /Usage: .*validate/)
assert.match(stdout, /--fail-level/)
})

test('creates a skill with init and exits zero', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-cli-'))

try {
const {stdout, stderr} = await execFileAsync(process.execPath, [cliPath, 'init', 'demo-skill'], {
cwd: tmpDir,
})

assert.equal(stderr, '')
assert.match(stdout, /Created skill directory structure/i)

const skillPath = path.join(tmpDir, 'demo-skill')
const statResult = await stat(path.join(skillPath, 'SKILL.md'))
assert.ok(statResult.isFile())
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})

test('returns a non-zero exit code for an unknown command', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-cli-'))

try {
await assert.rejects(
() => execFileAsync(process.execPath, [cliPath, 'does-not-exist'], {cwd: tmpDir}),
/Command failed/,
)
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})
})
81 changes: 81 additions & 0 deletions test/commands/init.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {describe, test} from 'node:test'
import assert from 'node:assert/strict'
import {mkdtemp, readFile, rm} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'

import {initCommand} from '../../src/commands/init.js'

async function withSilencedConsole(callback) {
const originalLog = console.log
const originalError = console.error
console.log = () => {}
console.error = () => {}

try {
return await callback()
} finally {
console.log = originalLog
console.error = originalError
}
}

describe('commands/init', () => {
test('creates a valid skill scaffold', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-init-'))

try {
const exitCode = await withSilencedConsole(async () =>
initCommand({
name: 'demo-skill',
outputDir: '.',
force: false,
baseDir: tmpDir,
}),
)

assert.equal(exitCode, 0)
const content = await readFile(path.join(tmpDir, 'demo-skill', 'SKILL.md'), 'utf8')
assert.match(content, /^---\nname: demo-skill/m)
assert.match(content, /# Skill Name/)
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})

test('rejects invalid names', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-init-'))

try {
const exitCode = await withSilencedConsole(async () =>
initCommand({
name: 'Bad Name',
outputDir: '.',
baseDir: tmpDir,
}),
)

assert.equal(exitCode, 1)
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})

test('rejects path traversal outside the base directory', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-init-'))

try {
const exitCode = await withSilencedConsole(async () =>
initCommand({
name: 'demo-skill',
outputDir: '../../outside',
baseDir: tmpDir,
}),
)

assert.equal(exitCode, 1)
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})
})
79 changes: 79 additions & 0 deletions test/commands/validate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {describe, test} from 'node:test'
import assert from 'node:assert/strict'
import {mkdtemp, writeFile, mkdir, rm} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'

import {validateCommand} from '../../src/commands/validate.js'

async function withSilencedConsole(callback) {
const originalLog = console.log
const originalError = console.error
console.log = () => {}
console.error = () => {}

try {
return await callback()
} finally {
console.log = originalLog
console.error = originalError
}
}

describe('commands/validate', () => {
test('returns zero for a valid skill directory', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-validate-'))
const skillDir = path.join(tmpDir, 'valid-skill')

try {
await mkdir(skillDir, {recursive: true})
await writeFile(
path.join(skillDir, 'SKILL.md'),
`---
name: valid-skill
description: Provides a sample skill. Use when validating skills in a project.
license: MIT. See LICENSE file for details.
---

# Valid Skill

This skill validates other skill content and is designed for local testing.
`,
)

const exitCode = await withSilencedConsole(async () =>
validateCommand({
skill: skillDir,
all: false,
format: 'pretty',
failLevel: 'error',
}),
)

assert.equal(exitCode, 0)
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})

test('returns one for a missing SKILL.md file', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-validate-'))
const skillDir = path.join(tmpDir, 'broken-skill')

try {
await mkdir(skillDir, {recursive: true})
const exitCode = await withSilencedConsole(async () =>
validateCommand({
skill: skillDir,
all: false,
format: 'pretty',
failLevel: 'error',
}),
)

assert.equal(exitCode, 1)
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})
})
41 changes: 41 additions & 0 deletions test/core/discover.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {describe, test} from 'node:test'
import assert from 'node:assert/strict'
import {mkdtemp, mkdir, writeFile, rm} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'

import {discoverSkills, resolveSkillPaths} from '../../src/core/discover.js'

describe('core/discover', () => {
test('discovers a directory containing SKILL.md', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-discover-'))
const skillDir = path.join(tmpDir, 'sample-skill')

try {
await mkdir(skillDir, {recursive: true})
await writeFile(path.join(skillDir, 'SKILL.md'), '---\nname: sample-skill\ndescription: sample\n---\n')

const skills = await discoverSkills(tmpDir)
assert.deepEqual(skills, [skillDir])
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})

test('resolves a single skill path from a directory or SKILL.md file', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-discover-'))
const skillDir = path.join(tmpDir, 'sample-skill')

try {
await mkdir(skillDir, {recursive: true})
await writeFile(path.join(skillDir, 'SKILL.md'), '---\nname: sample-skill\ndescription: sample\n---\n')

const fromDir = await resolveSkillPaths([skillDir], tmpDir)
const fromFile = await resolveSkillPaths([path.join(skillDir, 'SKILL.md')], tmpDir)
assert.deepEqual(fromDir, [skillDir])
assert.deepEqual(fromFile, [skillDir])
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})
})
29 changes: 29 additions & 0 deletions test/core/frontmatter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {describe, test} from 'node:test'
import assert from 'node:assert/strict'

import {extractFrontmatter} from '../../src/core/frontmatter.js'

describe('core/frontmatter', () => {
test('extracts simple frontmatter fields', () => {
const content = `---
name: demo-skill
description: Provides a sample skill. Use when validating skills.
license: MIT. See LICENSE file for details.
---

# Demo
`

const result = extractFrontmatter(content)
assert.deepEqual(result, {
name: 'demo-skill',
description: 'Provides a sample skill. Use when validating skills.',
license: 'MIT. See LICENSE file for details.',
})
})

test('returns null when no frontmatter is present', () => {
const result = extractFrontmatter('# Demo\n\nNo frontmatter here')
assert.equal(result, null)
})
})
52 changes: 52 additions & 0 deletions test/core/fsx.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {describe, test} from 'node:test'
import assert from 'node:assert/strict'
import {mkdtemp, mkdir, writeFile, rm} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'

import {readText, writeText, fileExists, listDir, walk, resolveSafeSkillPath} from '../../src/core/fsx.js'

describe('core/fsx', () => {
test('reads and writes text files', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-fsx-'))
const filePath = path.join(tmpDir, 'alpha.txt')

try {
await writeText(filePath, 'hello world')
const result = await readText(filePath)
assert.equal(result.text, 'hello world')
assert.ok(result.mtimeMs > 0)
assert.equal(await fileExists(filePath), true)
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})

test('lists and walks directories', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'skill-authoring-fsx-'))
const nestedDir = path.join(tmpDir, 'nested')

try {
await mkdir(nestedDir, {recursive: true})
await writeFile(path.join(nestedDir, 'x.js'), 'console.log(1)')
const entries = await listDir(tmpDir)
assert.ok(entries.includes('nested'))

const files = await walk(tmpDir, {patterns: ['.js']})
assert.equal(files.length, 1)
assert.equal(files[0].name, 'x.js')
} finally {
await rm(tmpDir, {recursive: true, force: true})
}
})

test('resolves only safe skill paths', () => {
const safe = resolveSafeSkillPath('/tmp/base', '.', 'demo-skill')
assert.equal(safe.safe, true)
assert.match(safe.skillPath, /demo-skill$/)

const unsafe = resolveSafeSkillPath('/tmp/base', '../../outside', 'demo-skill')
assert.equal(unsafe.safe, false)
assert.match(unsafe.reason, /escapes the approved base directory/i)
})
})
15 changes: 15 additions & 0 deletions test/core/log.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {describe, test} from 'node:test'
import assert from 'node:assert/strict'

import {createLogger} from '../../src/core/log.js'

describe('core/log', () => {
test('creates a logger with the expected methods', () => {
const logger = createLogger('info')
assert.equal(typeof logger.info, 'function')
assert.equal(typeof logger.warn, 'function')
assert.equal(typeof logger.error, 'function')
assert.equal(typeof logger.success, 'function')
assert.equal(typeof logger.debug, 'function')
})
})
Loading