192 lines
5.3 KiB
TypeScript
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"');
|
|
});
|