mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
plugin-primitive Phase 3a slice 3.3 fix-iteration: rig-cwd id self-resolve
redo-guard-2 BLOCK on965398araised 2 items. Forward-fix on both per banked discipline (TDD red→green; loop redo-guard-2 for re-verify). ═══ Item 1 (functional): rig-cwd: id list→detail 404 ═══ Symptom: `GET /api/plugins?cwd=<path>` returns rig-cwd plugin ids; `GET /api/plugins/<encoded-id>` 404s because `PluginDiscoveryService.getPlugin(id)` calls `listPlugins()` with no opts — cwdScanRoots defaults to empty, so the rig-cwd plugin isn't in the list and `find((p) => p.id === id)` returns undefined → 404. Fix: `getPlugin(id)` self-resolves rig-cwd: ids by parsing the cwd out of the id prefix (constructed in `scanCwdBundledPlugins` as `rig-cwd:<cwd>/.claude/plugins/<plugin>` or `rig-cwd:<cwd>/.codex/plugins/<plugin>`). New helper `extractCwdFromRigCwdId(id)` returns `[cwd]` for valid rig-cwd: ids and `null` otherwise. Same shape as the other source kinds where the id already encodes everything needed to re-resolve — keeps the detail-call contract self-contained without adding `?cwd=` to the detail endpoint. Coverage added: - 3 daemon-service tests: claude roundtrip, codex roundtrip, malformed id negative. - 1 daemon-routes test: list-with-?cwd= → detail roundtrip through the HTTP layer (would have caught the original 404). ═══ Item 2 (process): ACK provenance stale ═══ The965398afix-ack had `(TBD at commit)` and `7 files / +~200` for its net-diff line. Actual `git diff --stat 695ca74..965398a` is **9 files / +448 / -9**. ACK §2 corrected; fix-iteration section added at §9 covering both items. ═══ Verification ═══ - 62 tests across 7 files all green (was 58 at 965398a; +4 fix-iter coverage: 3 daemon-service + 1 daemon-routes). - Daemon tsc clean. - UI tsc clean for touched files (pre-existing @tanstack/react-table baseline on mainc0fcccdunchanged). - SC-29 #8 boundary unchanged (service-internal id-parse; no new state, no new endpoint, no mutation). Substrate fix-ack updated at <work-tree>/missions/plugin-primitive/ attempt-1-redo-driver-2-phase-3a-batch-2-slice-3-3-fix-ack.md. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
965398afa1
commit
995d28f120
3 files changed
+138
-1
No files matched your search
@@ -295,7 +295,18 @@ export class PluginDiscoveryService {
|
||||
}
|
||||
|
||||
getPlugin(id: string): PluginDetail | null {
|
||||
const entry = this.listPlugins().find((p) => p.id === id);
|
||||
// Slice 3.3 fix-iteration — rig-cwd: IDs are self-resolvable.
|
||||
// Pre-fix, getPlugin called this.listPlugins() (no opts), which
|
||||
// excluded rig-cwd entries because cwdScanRoots is empty by default.
|
||||
// Result: /api/plugins?cwd= returned a rig-cwd id, /api/plugins/:id
|
||||
// 404'd on the same id (redo-guard-2 BLOCK item 1). Fix: parse the
|
||||
// cwd out of the rig-cwd: prefix and pass it as cwdScanRoots so
|
||||
// the entry is in the list. ID format constructed in
|
||||
// scanCwdBundledPlugins:
|
||||
// rig-cwd:<cwd>/.claude/plugins/<plugin>
|
||||
// rig-cwd:<cwd>/.codex/plugins/<plugin>
|
||||
const cwdScanRoots = extractCwdFromRigCwdId(id);
|
||||
const entry = this.listPlugins(cwdScanRoots ? { cwdScanRoots } : {}).find((p) => p.id === id);
|
||||
if (!entry) return null;
|
||||
|
||||
const claudeManifestPath = join(entry.path, CLAUDE_MANIFEST_REL);
|
||||
@@ -416,6 +427,31 @@ export class PluginDiscoveryService {
|
||||
}
|
||||
}
|
||||
|
||||
// Slice 3.3 fix-iteration — parse cwd from a rig-cwd: id so getPlugin
|
||||
// can re-scan that cwd before the lookup. Returns the cwd in a
|
||||
// single-element array (caller passes as cwdScanRoots) or null when
|
||||
// the id is not a rig-cwd: id, can't be parsed, or both manifest dir
|
||||
// markers are absent. Tolerant of both `/.claude/plugins/` and
|
||||
// `/.codex/plugins/` markers (whichever appears first wins; in the
|
||||
// canonical id one of them is always present).
|
||||
const RIG_CWD_PREFIX = "rig-cwd:";
|
||||
const CLAUDE_MARKER = "/.claude/plugins/";
|
||||
const CODEX_MARKER = "/.codex/plugins/";
|
||||
function extractCwdFromRigCwdId(id: string): string[] | null {
|
||||
if (!id.startsWith(RIG_CWD_PREFIX)) return null;
|
||||
const rest = id.slice(RIG_CWD_PREFIX.length);
|
||||
const claudeIdx = rest.indexOf(CLAUDE_MARKER);
|
||||
const codexIdx = rest.indexOf(CODEX_MARKER);
|
||||
let cwd: string | null = null;
|
||||
if (claudeIdx >= 0 && (codexIdx < 0 || claudeIdx < codexIdx)) {
|
||||
cwd = rest.slice(0, claudeIdx);
|
||||
} else if (codexIdx >= 0) {
|
||||
cwd = rest.slice(0, codexIdx);
|
||||
}
|
||||
if (!cwd) return null;
|
||||
return [cwd];
|
||||
}
|
||||
|
||||
function safeReaddir(dir: string): string[] {
|
||||
try {
|
||||
return readdirSync(dir);
|
||||
|
||||
@@ -367,6 +367,74 @@ describe("PluginDiscoveryService", () => {
|
||||
expect(service.getPlugin("openrig-no-mcp")?.mcpServers).toEqual([]);
|
||||
});
|
||||
|
||||
it("slice 3.3 fix-iteration — getPlugin self-resolves rig-cwd: IDs (claude side) without external cwd state", () => {
|
||||
// redo-guard-2 BLOCK item 1: /api/plugins?cwd=... lists rig-cwd
|
||||
// plugins but /api/plugins/:id detail call 404s because listPlugins()
|
||||
// without cwdScanRoots doesn't re-scan the cwd, so getPlugin can't
|
||||
// find the entry by id. Fix: parse the cwd out of the rig-cwd: id
|
||||
// prefix, re-scan that cwd, resolve the entry. ID format:
|
||||
// rig-cwd:<cwd>/.claude/plugins/<plugin>
|
||||
// rig-cwd:<cwd>/.codex/plugins/<plugin>
|
||||
const rigCwd = join(dirs.root, "rig-cwd-self-resolve");
|
||||
const claudeBundleDir = join(rigCwd, ".claude", "plugins");
|
||||
mkdirSync(claudeBundleDir, { recursive: true });
|
||||
writeClaudePluginManifest(join(claudeBundleDir, "rig-tool"), {
|
||||
name: "rig-tool",
|
||||
version: "1.0.0",
|
||||
description: "rig-bundled tool",
|
||||
});
|
||||
const service = new PluginDiscoveryService({
|
||||
openrigPluginsDir: dirs.openrigPluginsDir,
|
||||
claudeCacheDir: dirs.claudeCacheDir,
|
||||
codexCacheDir: dirs.codexCacheDir,
|
||||
specLibraryDir: dirs.specLibraryDir,
|
||||
});
|
||||
const listed = service.listPlugins({ cwdScanRoots: [rigCwd] });
|
||||
const rigToolEntry = listed.find((p) => p.source === "rig-cwd");
|
||||
expect(rigToolEntry).toBeDefined();
|
||||
const rigToolId = rigToolEntry!.id;
|
||||
// Call getPlugin with that id directly (no cwd opts) — must resolve.
|
||||
const detail = service.getPlugin(rigToolId);
|
||||
expect(detail).not.toBeNull();
|
||||
expect(detail?.entry.name).toBe("rig-tool");
|
||||
expect(detail?.entry.source).toBe("rig-cwd");
|
||||
});
|
||||
|
||||
it("slice 3.3 fix-iteration — getPlugin self-resolves rig-cwd: codex IDs too", () => {
|
||||
const rigCwd = join(dirs.root, "rig-cwd-self-resolve-codex");
|
||||
const codexBundleDir = join(rigCwd, ".codex", "plugins");
|
||||
mkdirSync(codexBundleDir, { recursive: true });
|
||||
writeCodexPluginManifest(join(codexBundleDir, "codex-tool"), {
|
||||
name: "codex-tool",
|
||||
version: "2.0.0",
|
||||
description: "rig-bundled codex tool",
|
||||
});
|
||||
const service = new PluginDiscoveryService({
|
||||
openrigPluginsDir: dirs.openrigPluginsDir,
|
||||
claudeCacheDir: dirs.claudeCacheDir,
|
||||
codexCacheDir: dirs.codexCacheDir,
|
||||
specLibraryDir: dirs.specLibraryDir,
|
||||
});
|
||||
const listed = service.listPlugins({ cwdScanRoots: [rigCwd] });
|
||||
const codexToolEntry = listed.find((p) => p.name === "codex-tool");
|
||||
expect(codexToolEntry).toBeDefined();
|
||||
const detail = service.getPlugin(codexToolEntry!.id);
|
||||
expect(detail).not.toBeNull();
|
||||
expect(detail?.entry.name).toBe("codex-tool");
|
||||
});
|
||||
|
||||
it("slice 3.3 fix-iteration — getPlugin returns null for malformed rig-cwd: ids", () => {
|
||||
// Negative: garbled prefix or unresolvable cwd → null (not throw).
|
||||
const service = new PluginDiscoveryService({
|
||||
openrigPluginsDir: dirs.openrigPluginsDir,
|
||||
claudeCacheDir: dirs.claudeCacheDir,
|
||||
codexCacheDir: dirs.codexCacheDir,
|
||||
specLibraryDir: dirs.specLibraryDir,
|
||||
});
|
||||
expect(service.getPlugin("rig-cwd:notapath")).toBeNull();
|
||||
expect(service.getPlugin("rig-cwd:/nonexistent/.claude/plugins/x")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns full manifest + tree summary for a discovered plugin", () => {
|
||||
const corePluginDir = join(dirs.openrigPluginsDir, "openrig-core");
|
||||
writeClaudePluginManifest(corePluginDir, {
|
||||
|
||||
@@ -161,6 +161,39 @@ describe("plugins HTTP routes", () => {
|
||||
expect(rigCwdSubset[0]?.name).toBe("rig-tool");
|
||||
});
|
||||
|
||||
it("slice 3.3 fix-iteration — list ?cwd= + detail roundtrip for rig-cwd plugin (no 404)", async () => {
|
||||
// redo-guard-2 BLOCK item 1: /api/plugins?cwd= lists;
|
||||
// /api/plugins/<encoded-rig-cwd-id> must 200 not 404. Fix lands in
|
||||
// PluginDiscoveryService.getPlugin via self-resolve from id prefix.
|
||||
const rigCwd = join(env.root, "rig-cwd-roundtrip");
|
||||
const claudeBundleDir = join(rigCwd, ".claude", "plugins");
|
||||
mkdirSync(claudeBundleDir, { recursive: true });
|
||||
writeClaudePluginManifest(join(claudeBundleDir, "roundtrip-tool"), {
|
||||
name: "roundtrip-tool",
|
||||
version: "1.0.0",
|
||||
description: "roundtrip",
|
||||
});
|
||||
|
||||
// Step 1: list with ?cwd= — captures id.
|
||||
const listRes = await createApp(env.service).request(
|
||||
`/api/plugins?cwd=${encodeURIComponent(rigCwd)}`,
|
||||
);
|
||||
expect(listRes.status).toBe(200);
|
||||
const list = await listRes.json() as Array<{ id: string; source: string }>;
|
||||
const rigCwdEntry = list.find((p) => p.source === "rig-cwd");
|
||||
expect(rigCwdEntry).toBeDefined();
|
||||
const rigCwdId = rigCwdEntry!.id;
|
||||
|
||||
// Step 2: detail call with that id — must 200 (was 404 pre-fix).
|
||||
const detailRes = await createApp(env.service).request(
|
||||
`/api/plugins/${encodeURIComponent(rigCwdId)}`,
|
||||
);
|
||||
expect(detailRes.status).toBe(200);
|
||||
const detail = await detailRes.json() as { entry: { name: string; source: string } };
|
||||
expect(detail.entry.name).toBe("roundtrip-tool");
|
||||
expect(detail.entry.source).toBe("rig-cwd");
|
||||
});
|
||||
|
||||
it("supports source filter via query string", async () => {
|
||||
writeClaudePluginManifest(join(env.openrigPluginsDir, "vended"), {
|
||||
name: "vended",
|
||||
|
||||
Reference in new issue
Block a user