Skip to content

Commit a504c3d

Browse files
committed
test: add install and packaging coverage
Protect the release flow with fixture tests, tarball smoke checks, and CI workflows that validate the package before publishing.
1 parent 2477fc6 commit a504c3d

File tree

7 files changed

+370
-0
lines changed

7 files changed

+370
-0
lines changed

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 20
17+
cache: npm
18+
- run: npm ci
19+
- run: npm run build
20+
- run: npm test
21+
- run: npm run pack:smoke

.github/workflows/publish.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Publish
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- "v*"
8+
9+
jobs:
10+
publish:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 20
18+
registry-url: https://registry.npmjs.org
19+
cache: npm
20+
- run: npm ci
21+
- run: npm run build
22+
- run: npm test
23+
- run: npm run pack:smoke
24+
- run: npm publish --access public
25+
env:
26+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

tests/config-merge.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
2+
import os from "node:os";
3+
import { join } from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import { addPluginToConfig, removePluginFromConfig } from "../src/lib/merge-opencode-config.js";
6+
import { PACKAGE_NAME } from "../src/lib/paths.js";
7+
8+
const tempRoots: string[] = [];
9+
10+
async function makeConfigDir(): Promise<string> {
11+
const root = await mkdtemp(join(os.tmpdir(), "opencode-srd-config-"));
12+
tempRoots.push(root);
13+
const configDir = join(root, "config");
14+
await mkdir(configDir, { recursive: true });
15+
return configDir;
16+
}
17+
18+
afterEach(async () => {
19+
const { rm } = await import("node:fs/promises");
20+
await Promise.all(tempRoots.splice(0).map((path) => rm(path, { recursive: true, force: true })));
21+
});
22+
23+
describe("merge-opencode-config", () => {
24+
it("adds the plugin entry only once", async () => {
25+
const configDir = await makeConfigDir();
26+
const configPath = join(configDir, "opencode.json");
27+
28+
await writeFile(
29+
configPath,
30+
`{
31+
// keep my comments
32+
"plugin": ["example-plugin"]
33+
}
34+
`,
35+
"utf8",
36+
);
37+
38+
await addPluginToConfig(configDir, PACKAGE_NAME);
39+
await addPluginToConfig(configDir, PACKAGE_NAME);
40+
41+
const content = await readFile(configPath, "utf8");
42+
expect(content.match(new RegExp(PACKAGE_NAME.replace("/", "\\/"), "g"))?.length).toBe(1);
43+
expect(content).toContain("// keep my comments");
44+
});
45+
46+
it("removes the plugin entry without disturbing the rest", async () => {
47+
const configDir = await makeConfigDir();
48+
const configPath = join(configDir, "opencode.json");
49+
50+
await writeFile(
51+
configPath,
52+
`{
53+
"plugin": ["example-plugin", "${PACKAGE_NAME}"],
54+
"theme": "dark"
55+
}
56+
`,
57+
"utf8",
58+
);
59+
60+
await removePluginFromConfig(configDir, PACKAGE_NAME);
61+
62+
const content = await readFile(configPath, "utf8");
63+
expect(content).not.toContain(PACKAGE_NAME);
64+
expect(content).toContain("example-plugin");
65+
expect(content).toContain('"theme": "dark"');
66+
});
67+
});

tests/fixtures/.gitkeep

Whitespace-only changes.

tests/install.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
2+
import os from "node:os";
3+
import { join } from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import { doctorSrdFramework } from "../src/lib/doctor.js";
6+
import { installSrdFramework } from "../src/lib/install.js";
7+
import { resolvePackageRoot } from "../src/lib/paths.js";
8+
import { uninstallSrdFramework } from "../src/lib/uninstall.js";
9+
import { updateSrdFramework } from "../src/lib/update.js";
10+
11+
const tempRoots: string[] = [];
12+
13+
async function makeTempDir(prefix: string): Promise<string> {
14+
const root = await mkdtemp(join(os.tmpdir(), prefix));
15+
tempRoots.push(root);
16+
return root;
17+
}
18+
19+
afterEach(async () => {
20+
const { rm } = await import("node:fs/promises");
21+
await Promise.all(tempRoots.splice(0).map((path) => rm(path, { recursive: true, force: true })));
22+
});
23+
24+
describe("install/update/uninstall/doctor", () => {
25+
it("installs into a temp config directory and is idempotent", async () => {
26+
const packageRoot = resolvePackageRoot(import.meta.url);
27+
const configDir = await makeTempDir("opencode-srd-install-");
28+
29+
const first = await installSrdFramework({ configDir, packageRoot });
30+
const second = await installSrdFramework({ configDir, packageRoot });
31+
32+
expect(first.copied.length).toBeGreaterThan(0);
33+
expect(second.copied).toHaveLength(0);
34+
expect(second.updated).toHaveLength(0);
35+
expect(join(configDir, "commands", "srd-assess.md")).toBeTruthy();
36+
expect(await readFile(join(configDir, "commands", "srd-assess.md"), "utf8")).toContain("SRD Assess");
37+
});
38+
39+
it("update refreshes managed files", async () => {
40+
const packageRoot = resolvePackageRoot(import.meta.url);
41+
const configDir = await makeTempDir("opencode-srd-update-");
42+
43+
await installSrdFramework({ configDir, packageRoot });
44+
await writeFile(join(configDir, "commands", "srd-quick.md"), "mutated", "utf8");
45+
46+
const result = await updateSrdFramework({ configDir, packageRoot });
47+
const content = await readFile(join(configDir, "commands", "srd-quick.md"), "utf8");
48+
49+
expect(result.updated).toContain("commands/srd-quick.md");
50+
expect(content).toContain("SRD Quick");
51+
});
52+
53+
it("uninstall removes only managed files", async () => {
54+
const packageRoot = resolvePackageRoot(import.meta.url);
55+
const configDir = await makeTempDir("opencode-srd-uninstall-");
56+
await installSrdFramework({ configDir, packageRoot });
57+
58+
await mkdir(join(configDir, "commands"), { recursive: true });
59+
await writeFile(join(configDir, "commands", "custom.md"), "keep me", "utf8");
60+
61+
await uninstallSrdFramework({ configDir });
62+
63+
await expect(readFile(join(configDir, "commands", "custom.md"), "utf8")).resolves.toBe("keep me");
64+
await expect(readFile(join(configDir, "commands", "srd-assess.md"), "utf8")).rejects.toThrow();
65+
});
66+
67+
it("doctor reports healthy and unhealthy states", async () => {
68+
const packageRoot = resolvePackageRoot(import.meta.url);
69+
const configDir = await makeTempDir("opencode-srd-doctor-");
70+
71+
await installSrdFramework({ configDir, packageRoot });
72+
const healthy = await doctorSrdFramework({ configDir, packageRoot });
73+
74+
expect(healthy.healthy).toBe(true);
75+
76+
await writeFile(join(configDir, "commands", "srd-assess.md"), "drifted", "utf8");
77+
const unhealthy = await doctorSrdFramework({ configDir, packageRoot });
78+
79+
expect(unhealthy.healthy).toBe(false);
80+
expect(unhealthy.driftedFiles).toContain("commands/srd-assess.md");
81+
});
82+
});

tests/manifest.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
2+
import os from "node:os";
3+
import { join } from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import { getInstallableAssets, syncManagedAssets } from "../src/lib/copy-assets.js";
6+
import { readInstallManifest, writeInstallManifest } from "../src/lib/manifest.js";
7+
import { defaultConfigDir, resolveConfigDir, resolvePackageRoot } from "../src/lib/paths.js";
8+
9+
const tempRoots: string[] = [];
10+
11+
async function makeTempDir(prefix: string): Promise<string> {
12+
const root = await mkdtemp(join(os.tmpdir(), prefix));
13+
tempRoots.push(root);
14+
return root;
15+
}
16+
17+
afterEach(async () => {
18+
const { rm } = await import("node:fs/promises");
19+
await Promise.all(tempRoots.splice(0).map((path) => rm(path, { recursive: true, force: true })));
20+
});
21+
22+
describe("manifest and asset sync", () => {
23+
it("tracks only installable managed files", async () => {
24+
const packageRoot = resolvePackageRoot(import.meta.url);
25+
const assets = await getInstallableAssets(packageRoot);
26+
expect(assets.every((asset) => /^(commands|agents|skills)\//.test(asset.relativePath))).toBe(true);
27+
expect(assets.some((asset) => asset.relativePath === "commands/srd-assess.md")).toBe(true);
28+
expect(assets.some((asset) => asset.relativePath === "agents/srd-guardian.md")).toBe(true);
29+
expect(assets.some((asset) => asset.relativePath === "skills/srd-analysis/SKILL.md")).toBe(true);
30+
});
31+
32+
it("detects unmanaged conflicts", async () => {
33+
const packageRoot = resolvePackageRoot(import.meta.url);
34+
const configDir = await makeTempDir("opencode-srd-conflict-");
35+
await mkdir(join(configDir, "commands"), { recursive: true });
36+
await writeFile(join(configDir, "commands", "srd-assess.md"), "user-owned", "utf8");
37+
38+
const result = await syncManagedAssets({
39+
configDir,
40+
packageRoot,
41+
});
42+
43+
expect(result.conflicts).toContain("commands/srd-assess.md");
44+
});
45+
46+
it("resolves default and custom config directories", () => {
47+
expect(defaultConfigDir("/Users/example")).toBe("/Users/example/.config/opencode");
48+
expect(resolveConfigDir("./custom-config")).toContain("custom-config");
49+
});
50+
51+
it("reads and writes install manifests", async () => {
52+
const configDir = await makeTempDir("opencode-srd-manifest-");
53+
await writeInstallManifest(configDir, {
54+
packageName: "pkg",
55+
installedVersion: "0.1.0",
56+
installedAt: new Date().toISOString(),
57+
configDir,
58+
managedFiles: [
59+
{
60+
relativePath: "commands/srd-assess.md",
61+
checksum: "abc",
62+
kind: "command",
63+
},
64+
],
65+
});
66+
67+
const manifest = await readInstallManifest(configDir);
68+
expect(manifest?.managedFiles).toHaveLength(1);
69+
expect(manifest?.managedFiles[0]?.relativePath).toBe("commands/srd-assess.md");
70+
});
71+
});

tests/smoke-pack.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
2+
import os from "node:os";
3+
import { join } from "node:path";
4+
import { afterAll, describe, expect, it } from "vitest";
5+
import { spawnSync } from "node:child_process";
6+
7+
const tempRoots: string[] = [];
8+
const repoRoot = process.cwd();
9+
10+
async function makeTempDir(prefix: string): Promise<string> {
11+
const root = await mkdtemp(join(os.tmpdir(), prefix));
12+
tempRoots.push(root);
13+
return root;
14+
}
15+
16+
afterAll(async () => {
17+
const { rm } = await import("node:fs/promises");
18+
await Promise.all(tempRoots.splice(0).map((path) => rm(path, { recursive: true, force: true })));
19+
});
20+
21+
describe("package smoke", () => {
22+
it("packs, exposes assets, and installs from a tarball", async () => {
23+
const packResult = spawnSync("npm", ["pack", "--json"], {
24+
cwd: repoRoot,
25+
encoding: "utf8",
26+
});
27+
28+
expect(packResult.status).toBe(0);
29+
const tarballName = JSON.parse(packResult.stdout)[0].filename as string;
30+
const tarballPath = join(repoRoot, tarballName);
31+
tempRoots.push(tarballPath);
32+
33+
const tarList = spawnSync("tar", ["-tf", tarballPath], {
34+
cwd: repoRoot,
35+
encoding: "utf8",
36+
});
37+
38+
expect(tarList.status).toBe(0);
39+
expect(tarList.stdout).toContain("package/assets/commands/srd-assess.md");
40+
expect(tarList.stdout).toContain("package/assets/agents/srd-analyst.md");
41+
expect(tarList.stdout).toContain("package/assets/skills/srd-analysis/SKILL.md");
42+
43+
const installRoot = await makeTempDir("opencode-srd-pack-install-");
44+
const configDir = await makeTempDir("opencode-srd-pack-config-");
45+
await writeFile(join(installRoot, "package.json"), '{"name":"smoke","private":true}\n', "utf8");
46+
47+
const installResult = spawnSync("npm", ["install", tarballPath], {
48+
cwd: installRoot,
49+
encoding: "utf8",
50+
});
51+
52+
expect(installResult.status).toBe(0);
53+
54+
const cliResult = spawnSync(
55+
"node",
56+
[
57+
join(
58+
installRoot,
59+
"node_modules",
60+
"@dojocoding",
61+
"opencode-srd-framework",
62+
"dist",
63+
"cli.js",
64+
),
65+
"install",
66+
"--config-dir",
67+
configDir,
68+
],
69+
{
70+
cwd: installRoot,
71+
encoding: "utf8",
72+
},
73+
);
74+
75+
expect(cliResult.status).toBe(0);
76+
await expect(readFile(join(configDir, "commands", "srd-assess.md"), "utf8")).resolves.toContain("SRD Assess");
77+
78+
const doctorResult = spawnSync(
79+
"node",
80+
[
81+
join(
82+
installRoot,
83+
"node_modules",
84+
"@dojocoding",
85+
"opencode-srd-framework",
86+
"dist",
87+
"cli.js",
88+
),
89+
"doctor",
90+
"--config-dir",
91+
configDir,
92+
"--json",
93+
],
94+
{
95+
cwd: installRoot,
96+
encoding: "utf8",
97+
},
98+
);
99+
100+
expect(doctorResult.status).toBe(0);
101+
expect(JSON.parse(doctorResult.stdout).healthy).toBe(true);
102+
});
103+
});

0 commit comments

Comments
 (0)