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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ jobs:

- name: Run Tests
run: pnpm run test

- name: Run E2E Tests
run: pnpm run test:e2e:run
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["packages/**", "website/**", "scripts/**", "*.ts", "*.json"]
"includes": ["packages/**", "website/**", "scripts/**", "e2e/**/*.ts", "*.ts", "*.json"]
},
"formatter": {
"enabled": true,
Expand Down
30 changes: 30 additions & 0 deletions e2e/cases/__snapshots__/add.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Rstest Snapshot v1

exports[`spm add e2e > adds, installs, and links a selected local skill for an agent 1`] = `
"status: 0
signal: <none>
stdout:
┌ spm
◇ Found 1 skill
◇ Installed skills
└ Added agent-skill
stderr:
<empty>"
`;

exports[`spm add e2e > lists skills from a local source without writing a manifest 1`] = `
"status: 0
signal: <none>
stdout:
┌ spm
◇ Found 1 skill
listed-skill /skills/listed-skill - Listed skill
└ Listed skills
stderr:
<empty>"
`;
19 changes: 19 additions & 0 deletions e2e/cases/__snapshots__/errors.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Rstest Snapshot v1

exports[`spm error e2e > reports a missing add specifier 1`] = `
"status: 1
signal: <none>
stdout:
<empty>
stderr:
Missing required specifier"
`;

exports[`spm error e2e > reports an unknown command 1`] = `
"status: 1
signal: <none>
stdout:
<empty>
stderr:
Unknown command: unknown"
`;
42 changes: 42 additions & 0 deletions e2e/cases/__snapshots__/help-version.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Rstest Snapshot v1

exports[`spm help and version e2e > prints package version from the built bin 1`] = `
"status: 0
signal: <none>
stdout:
<package-version>
stderr:
<empty>"
`;

exports[`spm help and version e2e > prints top-level help from the built bin 1`] = `
"status: 0
signal: <none>
stdout:
spm/<package-version>

Usage:
$ spm <command> [options]

Commands:
add [...positionals]
install [...args]
patch <skill>
patch-commit <editDir>
update [...skills]
init [...args]

For more info, run any command with the \`--help\` flag:
$ spm add --help
$ spm install --help
$ spm patch --help
$ spm patch-commit --help
$ spm update --help
$ spm init --help

Options:
-h, --help Display this message
-v, --version Display version number
stderr:
<empty>"
`;
19 changes: 19 additions & 0 deletions e2e/cases/__snapshots__/init.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Rstest Snapshot v1

exports[`spm init e2e > fails without overwriting an existing manifest 1`] = `
"status: 1
signal: <none>
stdout:
<empty>
stderr:
Error [EMANIFESTEXISTS]: skills.json already exists"
`;

exports[`spm init e2e > writes the default manifest with --yes 1`] = `
"status: 0
signal: <none>
stdout:
<empty>
stderr:
<empty>"
`;
22 changes: 22 additions & 0 deletions e2e/cases/__snapshots__/install.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Rstest Snapshot v1

exports[`spm install e2e > installs a linked skill and creates configured agent links 1`] = `
"status: 0
signal: <none>
stdout:
spm install: starting (1 skill)
spm install: resolving...
spm install: fetching...
spm install: Progress: resolved 1, reused 0, downloaded 0, added 1, done
stderr:
<empty>"
`;

exports[`spm install e2e > skips cleanly when skills.json is missing 1`] = `
"status: 0
signal: <none>
stdout:
<empty>
stderr:
<empty>"
`;
19 changes: 19 additions & 0 deletions e2e/cases/__snapshots__/patch.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Rstest Snapshot v1

exports[`spm patch e2e > prepares and commits a skill patch from the built bin 1`] = `
"status: 0
signal: <none>
stdout:
<project>/patch-edit
stderr:
<empty>"
`;

exports[`spm patch e2e > prepares and commits a skill patch from the built bin 2`] = `
"status: 0
signal: <none>
stdout:
patches/patch-skill.patch
stderr:
<empty>"
`;
19 changes: 19 additions & 0 deletions e2e/cases/__snapshots__/update.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Rstest Snapshot v1

exports[`spm update e2e > reports unknown update targets 1`] = `
"status: 1
signal: <none>
stdout:
<empty>
stderr:
Error [ESKILLNOTFOUND]: Unknown skill: missing-skill"
`;

exports[`spm update e2e > skips link specifiers without rewriting the manifest 1`] = `
"status: 0
signal: <none>
stdout:
<empty>
stderr:
<empty>"
`;
60 changes: 60 additions & 0 deletions e2e/cases/add.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import path from 'node:path'
import { describe, expect, it } from '@rstest/core'
import {
cleanupTempProject,
createSkillSource,
createTempProject,
formatProjectTerminalSnapshot,
isSymlink,
pathExists,
readJson,
runSpm,
} from '../helpers/cli'

describe('spm add e2e', () => {
it('lists skills from a local source without writing a manifest', () => {
const project = createTempProject('add-list')

try {
createSkillSource(project, 'listed-skill', 'Listed skill')

const result = runSpm(['add', './skill-source', '--list'], { cwd: project })

expect(result.status).toBe(0)
expect(pathExists(path.join(project, 'skills.json'))).toBe(false)
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})

it('adds, installs, and links a selected local skill for an agent', () => {
const project = createTempProject('add-agent')

try {
createSkillSource(project, 'agent-skill', 'Agent skill')

const result = runSpm(
['add', './skill-source', '--skill', 'agent-skill', '--agent', 'claude-code', '-y'],
{
cwd: project,
},
)
const manifest = readJson<{ skills: Record<string, string>; linkTargets: string[] }>(
path.join(project, 'skills.json'),
)

expect(result.status).toBe(0)
expect(manifest.skills['agent-skill'].startsWith('link:')).toBe(true)
expect(manifest.skills['agent-skill'].replace(/\\/g, '/')).toContain(
'/skill-source/skills/agent-skill',
)
expect(manifest.linkTargets).toEqual(['.claude/skills'])
expect(pathExists(path.join(project, '.agents/skills/agent-skill/SKILL.md'))).toBe(true)
expect(isSymlink(path.join(project, '.claude/skills/agent-skill'))).toBe(true)
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})
})
35 changes: 35 additions & 0 deletions e2e/cases/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from '@rstest/core'
import {
cleanupTempProject,
createTempProject,
formatProjectTerminalSnapshot,
runSpm,
} from '../helpers/cli'

describe('spm error e2e', () => {
it('reports an unknown command', () => {
const project = createTempProject('unknown-command')

try {
const result = runSpm(['unknown'], { cwd: project })

expect(result.status).not.toBe(0)
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})

it('reports a missing add specifier', () => {
const project = createTempProject('missing-add-specifier')

try {
const result = runSpm(['add'], { cwd: project })

expect(result.status).not.toBe(0)
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})
})
35 changes: 35 additions & 0 deletions e2e/cases/help-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from '@rstest/core'
import {
cleanupTempProject,
createTempProject,
formatProjectTerminalSnapshot,
runSpm,
} from '../helpers/cli'

describe('spm help and version e2e', () => {
it('prints top-level help from the built bin', () => {
const project = createTempProject('help')

try {
const result = runSpm(['--help'], { cwd: project })

expect(result.status).toBe(0)
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})

it('prints package version from the built bin', () => {
const project = createTempProject('version')

try {
const result = runSpm(['--version'], { cwd: project })

expect(result.status).toBe(0)
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})
})
52 changes: 52 additions & 0 deletions e2e/cases/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from '@rstest/core'
import {
cleanupTempProject,
createTempProject,
formatProjectTerminalSnapshot,
readJson,
runSpm,
} from '../helpers/cli'

describe('spm init e2e', () => {
it('writes the default manifest with --yes', () => {
const project = createTempProject('init')

try {
const result = runSpm(['init', '--yes'], { cwd: project })
const manifestPath = path.join(project, 'skills.json')
const manifest = readJson<Record<string, unknown>>(manifestPath)

expect(result.status).toBe(0)
expect(existsSync(manifestPath)).toBe(true)
expect(manifest).toMatchObject({
installDir: '.agents/skills',
linkTargets: [],
selfSkill: false,
skills: {},
})
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})

it('fails without overwriting an existing manifest', () => {
const project = createTempProject('init-existing')
const manifestPath = path.join(project, 'skills.json')
const originalManifest = '{"skills":{"existing":"local:*"}}\n'

try {
writeFileSync(manifestPath, originalManifest)

const result = runSpm(['init', '--yes'], { cwd: project })

expect(result.status).not.toBe(0)
expect(readFileSync(manifestPath, 'utf8')).toBe(originalManifest)
expect(formatProjectTerminalSnapshot(result, project)).toMatchSnapshot()
} finally {
cleanupTempProject(project)
}
})
})
Loading
Loading