mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
test(daemon): failing tests for PluginVendorService (slice 3.2 Phase 2)
Per IMPL-PRD §2.5 + DESIGN.md §5.5 + orch-lead 2026-05-10 (vendored copy is source of truth at v0; auto-fetch tolerates 404 + vendored fallback; repo currently empty per founder authorization). 10 tests, all currently FAIL (TDD red — PluginVendorService class doesn't exist yet at packages/daemon/src/domain/plugin-vendor-service.ts). Tests cover (HG-2.3, HG-2.4, HG-2.5): - ensureVendored copies asset tree to user plugin dir on first launch - ensureVendored idempotent (hash-skip when content matches) - ensureVendored skips silently when asset doesn't exist - attemptAutoFetch tolerates 404 silently (HG-2.5 explicit) - attemptAutoFetch tolerates network errors silently (DNS/refused/etc) - attemptAutoFetch tolerates 5s timeout silently - attemptAutoFetch logs outcome (operator observability) - attemptAutoFetch hits github.com/mvschwarz/openrig-plugins URL - ensureLatest orchestrates vendored-first then fetch-attempt - ensureLatest returns successfully even when fetch 404s + vendored exists All test fixtures use injectable fs + httpClient mocks; service is fully unit-testable without real network or filesystem. Per IMPL-PRD §2 plugin-primitive Phase 3a slice 3.2 + per banked feedback_tdd_scope (failing-tests-first; restored discipline).
This commit is contained in:
1 parent
7e773347d6
commit
63f7268702
1 file changed
+227
@@ -0,0 +1,227 @@
|
||||
// Test suite for plugin-primitive Phase 3a slice 3.2 Phase 2 — PluginVendorService
|
||||
// (vendoring + auto-fetch with 404-tolerant fallback).
|
||||
//
|
||||
// Per IMPL-PRD §2.5 + DESIGN.md §5.5 + orch-lead 2026-05-10:
|
||||
// - vendored copy is source of truth at v0
|
||||
// - auto-fetch tolerates 404 + falls back to vendored
|
||||
// - repo (github.com/mvschwarz/openrig-plugins) currently empty (LICENSE only)
|
||||
// - 5s network timeout per IMPL-PRD §2.5
|
||||
// - silent fallback on any failure
|
||||
//
|
||||
// Service responsibilities (HG-2.3, HG-2.4, HG-2.5):
|
||||
// 1. ensureVendored(): copy from packages/daemon/assets/plugins/<name>/
|
||||
// to ~/.openrig/plugins/<name>/ on first launch (idempotent: hash-skip
|
||||
// if already vendored at same content)
|
||||
// 2. attemptAutoFetch(): try fetch from github.com/mvschwarz/openrig-plugins;
|
||||
// tolerate 404/network/timeout; log outcome; never throw
|
||||
// 3. ensureLatest(): orchestrates ensureVendored + attemptAutoFetch
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { PluginVendorService } from "../src/domain/plugin-vendor-service.js";
|
||||
|
||||
// Injectable fs ops for test mock
|
||||
function mockFs(initialFiles?: Record<string, string>) {
|
||||
const store: Record<string, string> = { ...initialFiles };
|
||||
return {
|
||||
readFile: (p: string) => { if (p in store) return store[p]!; throw new Error(`Not found: ${p}`); },
|
||||
writeFile: (p: string, c: string) => { store[p] = c; },
|
||||
exists: (p: string) => p in store || Object.keys(store).some((k) => k.startsWith(p + "/")),
|
||||
mkdirp: () => {},
|
||||
listFiles: (dir: string) => Object.keys(store).filter((k) => k.startsWith(dir + "/")).map((k) => k.slice(dir.length + 1)),
|
||||
rmrf: (p: string) => {
|
||||
for (const k of Object.keys(store)) {
|
||||
if (k === p || k.startsWith(p + "/")) delete store[k];
|
||||
}
|
||||
},
|
||||
_store: store,
|
||||
};
|
||||
}
|
||||
|
||||
const VENDORED_OPENRIG_CORE = {
|
||||
"/asset-root/openrig-core/.claude-plugin/plugin.json": '{"name":"openrig-core","version":"0.1.0"}',
|
||||
"/asset-root/openrig-core/.codex-plugin/plugin.json": '{"name":"openrig-core","version":"0.1.0","description":"v"}',
|
||||
"/asset-root/openrig-core/skills/openrig-user/SKILL.md": "# openrig-user vendored",
|
||||
"/asset-root/openrig-core/hooks/claude.json": '{"hooks":{}}',
|
||||
};
|
||||
|
||||
describe("PluginVendorService — vendoring (HG-2.3)", () => {
|
||||
it("ensureVendored copies vendored asset tree to user plugin dir on first launch", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger: vi.fn(),
|
||||
});
|
||||
|
||||
await svc.ensureVendored("openrig-core");
|
||||
|
||||
expect(fs._store["/home/test/.openrig/plugins/openrig-core/.claude-plugin/plugin.json"]).toBe('{"name":"openrig-core","version":"0.1.0"}');
|
||||
expect(fs._store["/home/test/.openrig/plugins/openrig-core/.codex-plugin/plugin.json"]).toBe('{"name":"openrig-core","version":"0.1.0","description":"v"}');
|
||||
expect(fs._store["/home/test/.openrig/plugins/openrig-core/skills/openrig-user/SKILL.md"]).toBe("# openrig-user vendored");
|
||||
expect(fs._store["/home/test/.openrig/plugins/openrig-core/hooks/claude.json"]).toBe('{"hooks":{}}');
|
||||
});
|
||||
|
||||
it("ensureVendored is idempotent — re-running with same content does not re-write (hash-skip)", async () => {
|
||||
const fs = mockFs({
|
||||
...VENDORED_OPENRIG_CORE,
|
||||
"/home/test/.openrig/plugins/openrig-core/.claude-plugin/plugin.json": '{"name":"openrig-core","version":"0.1.0"}',
|
||||
"/home/test/.openrig/plugins/openrig-core/.codex-plugin/plugin.json": '{"name":"openrig-core","version":"0.1.0","description":"v"}',
|
||||
"/home/test/.openrig/plugins/openrig-core/skills/openrig-user/SKILL.md": "# openrig-user vendored",
|
||||
"/home/test/.openrig/plugins/openrig-core/hooks/claude.json": '{"hooks":{}}',
|
||||
});
|
||||
const writeCounts: Record<string, number> = {};
|
||||
const origWrite = fs.writeFile;
|
||||
fs.writeFile = (p: string, c: string) => { writeCounts[p] = (writeCounts[p] ?? 0) + 1; origWrite(p, c); };
|
||||
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient: vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
||||
logger: vi.fn(),
|
||||
});
|
||||
|
||||
await svc.ensureVendored("openrig-core");
|
||||
|
||||
// Hash-match → no writes
|
||||
expect(Object.values(writeCounts).reduce((a, b) => a + b, 0)).toBe(0);
|
||||
});
|
||||
|
||||
it("ensureVendored skips silently when vendored asset doesn't exist (no source to copy)", async () => {
|
||||
const fs = mockFs({});
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient: vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
||||
logger: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(svc.ensureVendored("nonexistent-plugin")).resolves.not.toThrow();
|
||||
expect(fs._store["/home/test/.openrig/plugins/nonexistent-plugin/anything"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PluginVendorService — auto-fetch (HG-2.4 + HG-2.5)", () => {
|
||||
it("attemptAutoFetch tolerates 404 silently — does not throw, falls back to vendored (HG-2.5)", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
||||
const logger = vi.fn();
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
await expect(svc.attemptAutoFetch("openrig-core")).resolves.not.toThrow();
|
||||
// Vendored copy still available (404-tolerant fallback contract)
|
||||
expect(fs._store["/asset-root/openrig-core/.claude-plugin/plugin.json"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("attemptAutoFetch tolerates network errors silently (DNS / connection refused / etc.)", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockRejectedValue(new Error("ENOTFOUND github.com"));
|
||||
const logger = vi.fn();
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
await expect(svc.attemptAutoFetch("openrig-core")).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("attemptAutoFetch tolerates 5s timeout silently (slow network)", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockImplementation(() => new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)));
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(svc.attemptAutoFetch("openrig-core")).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("attemptAutoFetch logs outcome (success or fallback) for operator observability", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
||||
const logger = vi.fn();
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
await svc.attemptAutoFetch("openrig-core");
|
||||
|
||||
// Some log call describing the outcome (404, fallback, etc.)
|
||||
expect(logger).toHaveBeenCalled();
|
||||
const allLogs = logger.mock.calls.map((c) => c.join(" ")).join("\n");
|
||||
expect(allLogs).toMatch(/openrig-core|404|fallback|fetch/i);
|
||||
});
|
||||
|
||||
it("attemptAutoFetch hits the github.com/mvschwarz/openrig-plugins URL (or release tarball pattern)", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger: vi.fn(),
|
||||
});
|
||||
|
||||
await svc.attemptAutoFetch("openrig-core");
|
||||
|
||||
expect(httpClient).toHaveBeenCalled();
|
||||
const url = httpClient.mock.calls[0]?.[0] as string;
|
||||
expect(url).toMatch(/github\.com\/mvschwarz\/openrig-plugins|api\.github\.com.*mvschwarz\/openrig-plugins/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PluginVendorService — ensureLatest orchestration", () => {
|
||||
it("ensureLatest calls ensureVendored first then attemptAutoFetch (vendored fallback ALWAYS available)", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger: vi.fn(),
|
||||
});
|
||||
|
||||
await svc.ensureLatest("openrig-core");
|
||||
|
||||
// Vendored copy lands first (so fallback is always there even if fetch fails)
|
||||
expect(fs._store["/home/test/.openrig/plugins/openrig-core/.claude-plugin/plugin.json"]).toBeDefined();
|
||||
// And fetch was attempted
|
||||
expect(httpClient).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ensureLatest returns successfully even when fetch 404s + vendored exists", async () => {
|
||||
const fs = mockFs(VENDORED_OPENRIG_CORE);
|
||||
const httpClient = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
||||
const svc = new PluginVendorService({
|
||||
vendoredAssetsDir: "/asset-root",
|
||||
userPluginsDir: "/home/test/.openrig/plugins",
|
||||
fs,
|
||||
httpClient,
|
||||
logger: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(svc.ensureLatest("openrig-core")).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user