Files
gitea-axi/test/setup.test.ts
alexion be7226b321
All checks were successful
CI / test (pull_request) Successful in 52s
CI / test (push) Successful in 51s
feat: add setup skill/hooks and update shadow (task 0018)
Distribute gitea-axi's ambient context via explicit user actions (ADRs
0009, 0013), with no postinstall script:

- Bundle the Agent Skill markdown at skills/gitea-axi/SKILL.md (a
  minimal pointer, not a command reference) and ship it via package.json
  files.
- Add `setup`, which installs the skill into ~/.claude/skills/
  idempotently (installed/updated/unchanged).
- Add `setup hooks`, which registers a SessionStart hook running the
  bare dashboard for Claude Code, Codex, and OpenCode via the SDK's
  installSessionStartHooks(), updating managed entries in place.
- Shadow the SDK's built-in `update` so it fails with VALIDATION_ERROR
  and points at the npm update command, keeping the ten-code error list
  intact.

Integration tests drive all three at the CLI seam against a temporary
HOME; these commands make no Gitea API calls, so there is no live-Gitea
e2e case.
2026-07-14 11:26:56 -04:00

111 lines
4.3 KiB
TypeScript

import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { runCliTest } from "./harness.js";
let tempHome: string;
afterEach(() => {
if (tempHome) {
rmSync(tempHome, { recursive: true, force: true });
}
});
describe("setup", () => {
it("installs the skill and is idempotent: installed -> unchanged -> updated", async () => {
tempHome = mkdtempSync(join(tmpdir(), "gitea-axi-setup-"));
const installedPath = join(tempHome, ".claude", "skills", "gitea-axi", "SKILL.md");
// 1. First run on a clean HOME → installed, file now on disk.
const first = await runCliTest(["setup"], { env: { HOME: tempHome } });
expect(first.exitCode).toBe(0);
expect(first.stdout).toContain("setup:");
expect(first.stdout).toContain("skill: gitea-axi");
expect(first.stdout).toContain("status: installed");
expect(existsSync(installedPath)).toBe(true);
// 2. Immediate second run, nothing changed → unchanged, still exit 0.
const second = await runCliTest(["setup"], { env: { HOME: tempHome } });
expect(second.exitCode).toBe(0);
expect(second.stdout).toContain("status: unchanged");
// 3. Tamper with the installed file → next run reports updated and restores it.
writeFileSync(installedPath, "tampered");
const third = await runCliTest(["setup"], { env: { HOME: tempHome } });
expect(third.exitCode).toBe(0);
expect(third.stdout).toContain("status: updated");
expect(readFileSync(installedPath, "utf8")).not.toBe("tampered");
});
});
describe("setup hooks", () => {
it("registers the SessionStart hook for all three integrations and reports a restart hint", async () => {
tempHome = mkdtempSync(join(tmpdir(), "gitea-axi-setup-"));
const { stdout, exitCode } = await runCliTest(["setup", "hooks"], {
env: { HOME: tempHome },
});
expect(exitCode).toBe(0);
expect(stdout).toContain("hooks:");
expect(stdout).toContain("status: installed");
expect(stdout).toContain("Claude Code");
expect(stdout).toContain("Codex");
expect(stdout).toContain("OpenCode");
expect(stdout.toLowerCase()).toContain("restart");
// Claude Code: settings.json with a non-empty SessionStart hook array.
const claudeSettingsPath = join(tempHome, ".claude", "settings.json");
expect(existsSync(claudeSettingsPath)).toBe(true);
const claudeSettings = JSON.parse(readFileSync(claudeSettingsPath, "utf8"));
expect(Array.isArray(claudeSettings.hooks?.SessionStart)).toBe(true);
expect(claudeSettings.hooks.SessionStart.length).toBeGreaterThan(0);
// Codex: hooks.json and config.toml.
expect(existsSync(join(tempHome, ".codex", "hooks.json"))).toBe(true);
expect(existsSync(join(tempHome, ".codex", "config.toml"))).toBe(true);
// OpenCode: plugin file.
expect(
existsSync(join(tempHome, ".config", "opencode", "plugins", "axi-gitea-axi.js")),
).toBe(true);
});
it("updates the managed entry in place on re-run rather than appending a duplicate", async () => {
tempHome = mkdtempSync(join(tmpdir(), "gitea-axi-setup-"));
const first = await runCliTest(["setup", "hooks"], { env: { HOME: tempHome } });
expect(first.exitCode).toBe(0);
const second = await runCliTest(["setup", "hooks"], { env: { HOME: tempHome } });
expect(second.exitCode).toBe(0);
const claudeSettingsPath = join(tempHome, ".claude", "settings.json");
const claudeSettings = JSON.parse(readFileSync(claudeSettingsPath, "utf8"));
expect(claudeSettings.hooks.SessionStart).toHaveLength(1);
expect(claudeSettings.hooks.SessionStart[0].hooks).toHaveLength(1);
});
});
describe("setup dispatch", () => {
it("prints setup usage for setup --help", async () => {
const { stdout, exitCode } = await runCliTest(["setup", "--help"]);
expect(exitCode).toBe(0);
expect(stdout).toContain("usage: gitea-axi setup");
});
it("rejects an unknown setup subcommand with a VALIDATION_ERROR", async () => {
tempHome = mkdtempSync(join(tmpdir(), "gitea-axi-setup-"));
const { stdout, exitCode } = await runCliTest(["setup", "bogus"], {
env: { HOME: tempHome },
});
expect(exitCode).toBe(2);
expect(stdout).toContain("code: VALIDATION_ERROR");
expect(stdout).toContain("bogus");
});
});