Files
skills-vault/test/backup.test.ts
T

192 lines
5.3 KiB
TypeScript

import { expect, test } from "bun:test";
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { runBackup } from "../src/commands/backup";
import { parseBackupArgs } from "../src/internal/args/parse-backup-args";
import { buildManifest } from "../src/internal/manifest/build-manifest";
import { defaultGlobalLockFilePath } from "../src/internal/paths/defaults";
test("rejects flag-like values for backup paths", () => {
expect(() => parseBackupArgs(["--output", "--dry-run"])).toThrow(
"--output requires a path",
);
expect(() => parseBackupArgs(["--lock-file", "--dry-run"])).toThrow(
"--lock-file requires a path",
);
});
test("treats an explicit default lock file as provided", () => {
const options = parseBackupArgs([
"--project-scope",
"--lock-file",
defaultGlobalLockFilePath,
]);
expect(options.lockFilePath).toBe(defaultGlobalLockFilePath);
expect(options.lockFileProvided).toBe(true);
});
test("defaults backup output path to the current working directory", () => {
const options = parseBackupArgs([]);
expect(options.outputPath).toBe("./skvlt.yaml");
});
test("prints deterministic manifest YAML during backup dry-run", async () => {
let writeCalls = 0;
const result = await runBackup(["--dry-run"], {
getInstalledSkillNames: async () => ["zulu", "alpha", "beta"],
readLockFile: () => ({
skills: {
alpha: { source: "beta/source" },
beta: { source: "alpha/source" },
zulu: { source: "beta/source" },
},
}),
writeManifest: () => {
writeCalls += 1;
},
});
expect(result.exitCode).toBe(0);
expect(writeCalls).toBe(0);
expect(result.errorCode).toBeUndefined();
expect(result.payload).toMatchObject({
dryRun: true,
totalSources: 2,
totalSkills: 3,
});
expect(result.stdout).toBe(
[
"total_sources: 2",
"total_skills: 3",
'scope: "global"',
"",
"sources:",
' "alpha/source":',
" count: 1",
" skills:",
' - "beta"',
"",
' "beta/source":',
" count: 2",
" skills:",
' - "alpha"',
' - "zulu"',
"",
].join("\n") + "\n",
);
});
test("creates parent directories and prints backup progress", async () => {
const workspace = mkdtempSync(join(tmpdir(), "skills-vault-backup-"));
const outputPath = join(workspace, "nested", "skvlt.yaml");
const result = await runBackup(["--output", outputPath], {
getInstalledSkillNames: async () => ["alpha", "beta"],
readLockFile: () => ({
skills: {
alpha: { source: "owner/skills" },
beta: { source: "owner/skills" },
},
}),
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Listing installed skills...");
expect(result.stdout).toContain("Reading lock file metadata...");
expect(result.stdout).toContain("Building manifest...");
expect(result.stdout).toContain(`Writing manifest to ${outputPath}...`);
expect(result.stdout).toContain(
`Backup summary: 2 skill(s) across 1 source(s) written to ${outputPath}`,
);
expect(existsSync(outputPath)).toBe(true);
expect(readFileSync(outputPath, "utf8")).toContain(' "owner/skills":');
});
test("shows help before project-scope validation", async () => {
const result = await runBackup(["--help", "--project-scope"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Usage:");
});
test("builds a grouped manifest with stable source and skill ordering", () => {
const manifest = buildManifest(["zulu", "alpha", "beta"], {
skills: {
alpha: { source: "beta/source" },
beta: { source: "alpha/source" },
zulu: { source: "beta/source" },
},
});
expect(manifest.totalSources).toBe(2);
expect(manifest.totalSkills).toBe(3);
expect(manifest.yaml).toBe(
[
"total_sources: 2",
"total_skills: 3",
'scope: "global"',
"",
"sources:",
' "alpha/source":',
" count: 1",
" skills:",
' - "beta"',
"",
' "beta/source":',
" count: 2",
" skills:",
' - "alpha"',
' - "zulu"',
"",
].join("\n") + "\n",
);
});
test("builds double-quoted yaml scalars with escaped special characters", () => {
const manifest = buildManifest(['skill "one"', "path\\skill"], {
skills: {
'skill "one"': { source: 'owner/"repo"' },
"path\\skill": { source: "owner/path\\repo" },
},
});
expect(manifest.yaml).toBe(
[
"total_sources: 2",
"total_skills: 2",
'scope: "global"',
"",
"sources:",
' "owner/\\"repo\\"":',
" count: 1",
" skills:",
' - "skill \\"one\\""',
"",
' "owner/path\\\\repo":',
" count: 1",
" skills:",
' - "path\\\\skill"',
"",
].join("\n") + "\n",
);
});
test("records project scope in the manifest output", async () => {
const result = await runBackup(
["--dry-run", "--project-scope", "--lock-file", defaultGlobalLockFilePath],
{
getInstalledSkillNames: async () => ["alpha"],
readLockFile: () => ({
skills: {
alpha: { source: "owner/skills" },
},
}),
},
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('scope: "project"');
});