Files
AgentCompany/tests/unit/skill-types.test.ts
T
ZacharyZhang-NY f012f8e880 refactor(skills): drop company_skills.markdown column, read from disk via IPC
`company_skills.markdown` stored the full SKILL.md body. The on-disk
file under `profile/skills/{companyId}/{slug}/SKILL.md` is what pi-
coding-agent actually reads via `loadSkills` at runtime. The DB column
was a duplicate — import wrote to both, renderer read from DB. Any
discrepancy between disk and DB was invisible until the agent behaved
differently from what the user saw in SkillDetail.

Disk becomes the single source of truth. DB keeps name / description /
metadata / source info / file inventory — the cheap lookup row. UI reads
SKILL.md content live via a new IPC channel.

## Migration v37 (rebuild company_skills without markdown)

Table-rebuild pattern matching v35/v36: `fn:` form, PRAGMA walk for
drift columns, row-count assertion before DROP, `defer_foreign_keys`
for the `agent_skill_assignments(skill_id)` reference. Indexes
recreated. Idempotent via `schema_migrations` ledger.

Existing DB rows' markdown content is discarded — but the on-disk
SKILL.md files are the authoritative copy, so no user-visible data
loss. Any agent using those skills keeps working; the UI just starts
reading from disk instead of the DB cache.

## Code changes

- `src/main/database/skills.ts`: drop `markdown` from read mapping,
  upsert input type, INSERT column/bind list, ON CONFLICT DO UPDATE.
- `src/shared/types/records-core.ts`: `SkillRecord.markdown` removed.
- `src/main/skills/import.ts` + `import-github.ts`: drop `markdown:`
  from 4 `saveCompanySkill` call sites. Disk-write via
  `materializeSkillFiles` unchanged (it's what the file agent reads).
- `src/main/skills/index.ts`: expose `skillFilesDir` for the new IPC
  handler to resolve `${profile}/skills/${companyId}/${slug}/SKILL.md`.
- `src/shared/contracts/channels.ts` + `schemas-execution.ts` +
  `desktop-api/skills.ts`: new `getSkillMarkdown` channel
  `{ companyId, slug } → DesktopResult<string>`.
- `src/preload/index.ts`: expose `getSkillMarkdown(companyId, slug)`.
- `src/main/ipc-skill-handlers.ts`: handler validates schema, ensures
  company, resolves path, `readFile`, returns content. Fails closed
  with `SKILL_NOT_FOUND` on ENOENT, `SKILL_READ_FAILED` on other
  read errors.
- `src/renderer/components/skills/SkillDetail.tsx`: `useEffect` fetches
  markdown via IPC when the skill detail loads; shows "Loading…" until
  resolved, surfaces read errors. Preserves `stripFrontmatter`.
- Tests: drop `markdown:` from 2 fixture sites
  (`skill-types.test.ts`, `skill-aware-assignment.spec.ts`).

## Verification

- typecheck / lint / 578 tests green
- `grep 'skill\.markdown' src/` → 0 DB-record reads
- `grep '\.markdown\b' src/main/database/skills.ts` → 0
- Remaining `.markdown` references are all `ghSkill.markdown` (GitHub
  fetcher's intermediate struct feeding `materializeSkillFiles` to
  disk) and parameter names in `stripFrontmatter(markdown: string)` /
  fs-writer helpers — none read from a DB row.
2026-04-17 16:22:31 -04:00

116 lines
3.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import type {
SkillSourceType,
TrustLevel,
FileInventoryEntry,
SkillRecord,
SkillImportResult,
ProjectedSkill,
SkillAssignment,
SkillUpdateStatus,
} from "@main/skill-types";
describe("skill-types", () => {
describe("type shape checks", () => {
it("SkillSourceType accepts all five source kinds", () => {
const sources: SkillSourceType[] = [
"github",
"local_path",
"url",
"skills_sh",
"managed",
];
expect(sources).toHaveLength(5);
});
it("TrustLevel accepts all three levels", () => {
const levels: TrustLevel[] = [
"markdown_only",
"assets",
"scripts_executables",
];
expect(levels).toHaveLength(3);
});
it("FileInventoryEntry has path and kind", () => {
const entry: FileInventoryEntry = { path: "SKILL.md", kind: "skill" };
expect(entry.path).toBe("SKILL.md");
expect(entry.kind).toBe("skill");
});
it("FileInventoryEntry kind accepts all six values", () => {
const kinds: FileInventoryEntry["kind"][] = [
"skill",
"reference",
"script",
"asset",
"markdown",
"other",
];
expect(kinds).toHaveLength(6);
});
it("SkillRecord has all expected properties", () => {
const record: SkillRecord = {
id: "test-id",
companyId: "company-1",
key: "owner/repo/slug",
slug: "slug",
name: "Test Skill",
description: null,
sourceType: "github",
sourceLocator: "https://github.com/owner/repo",
sourceRef: "abc123",
trustLevel: "markdown_only",
compatibility: "compatible",
fileInventory: [],
metadata: null,
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-01T00:00:00Z",
};
expect(record.id).toBe("test-id");
expect(record.companyId).toBe("company-1");
});
it("SkillImportResult has skillId, key, name, isUpdate", () => {
const result: SkillImportResult = {
skillId: "id-1",
key: "owner/repo/slug",
name: "Test",
isUpdate: false,
};
expect(result.isUpdate).toBe(false);
});
it("ProjectedSkill matches pi-coding-agent Skill projection", () => {
const entry: ProjectedSkill = {
name: "test-skill",
description: "What this skill does and when to use it.",
filePath: "/abs/path/to/test-skill/SKILL.md",
baseDir: "/abs/path/to/test-skill",
};
expect(entry.name).toBe("test-skill");
expect(entry.filePath.endsWith("SKILL.md")).toBe(true);
});
it("SkillAssignment has agentId and skillId", () => {
const assignment: SkillAssignment = {
agentId: "agent-1",
skillId: "skill-1",
};
expect(assignment.agentId).toBe("agent-1");
});
it("SkillUpdateStatus has all expected fields", () => {
const status: SkillUpdateStatus = {
skillId: "skill-1",
key: "owner/repo/slug",
currentRef: "abc",
latestRef: "def",
hasUpdate: true,
};
expect(status.hasUpdate).toBe(true);
});
});
});