e972267a42
The ten `agenthub-*` plugins were listed by the marketplace but could never be
installed — two schema mistakes present since the entries were first authored
(57dc915), both invisible until you actually run `claude plugin install`:
- marketplace entries used `"path": "plugins/<name>"`; the CLI only accepts
`"source"`, so every one failed with
`This plugin's marketplace entry is invalid: source: Invalid input`
- each `plugin.json` carried npm's `"repository": { "type", "url" }` object;
the schema wants a plain string, so the next attempt failed with
`Validation errors: repository: Invalid input`
Verified by installing all 11 entries into a throwaway CLAUDE_CONFIG_DIR:
1/11 before, 11/11 after. `plugins-marketplace.test.js` was asserting the
broken shape, which is why CI stayed green — it now asserts `source` and
rejects `path`.
Marketplace renamed `claude-code-agent-monitor-plugins` -> `agenthub`, so the
install line is `agenthub@agenthub`. ARCHITECTURE.md documented a marketplace
name (`smartgift-claude-code-monitor`) that never existed in any revision.
The documented `/plugin marketplace add Smartgift-AI/Claude-Code-Monitor` also
never worked: the `owner/repo` shorthand resolves to github.com only, and this
repo is self-hosted on Gitea. Confirmed the CLI rejects `host/owner/repo` and
ignores GH_HOST / GITHUB_HOST / CLAUDE_CODE_MARKETPLACE_URL, so the full
https:// clone URL is the only supported form. Same GitHub-habit class fixed
elsewhere: raw.githubusercontent.com icon URLs (404, and the repo ships
favicon.svg not favicon.ico — the client had the same dead /favicon.ico),
a smartgift.github.io docs link (404), and Gitea `/blob/<ref>/` paths that
must be `/src/branch/<ref>/`.
Repository renamed to Smartgift-AI/AgentHub throughout. Gitea 301-redirects
the old name, so these were stale rather than broken; `docs/superpowers/`
keeps the old name because those are dated decision records.
Adds `repo-links.test.js` to guard both classes (it runs under the existing
pre-commit gate) plus an openapi.yaml drift check — the committed spec had
silently fallen a minor version behind package.json. `generate-openapi-yaml.js`
now exports `render()` so the test compares against one definition of the dump
options instead of a second copy.
147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
/**
|
||
* @file Tests for agentOriginLabel - verifies the parent-chain walk that
|
||
* renders nested subagent attribution as "main › coder › explorer", so
|
||
* tool events triggered by deeply nested subagents identify their full
|
||
* lineage instead of collapsing to just the leaf agent.
|
||
*
|
||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||
*/
|
||
|
||
import { describe, it, expect } from "vitest";
|
||
import { agentOriginLabel, projectFromCwd, type AgentInfo } from "../event-grouping";
|
||
|
||
function makeMap(entries: Array<[string, AgentInfo]>): Map<string, AgentInfo> {
|
||
return new Map(entries);
|
||
}
|
||
|
||
describe("agentOriginLabel - parent chain walk", () => {
|
||
it("returns 'main' for the main agent itself", () => {
|
||
const map = makeMap([
|
||
["sess-main", { type: "main", subagent_type: null, name: "Main", parent_agent_id: null }],
|
||
]);
|
||
expect(agentOriginLabel("sess-main", map)).toBe("main");
|
||
});
|
||
|
||
it("renders 'main › coder' for a direct child of main", () => {
|
||
const map = makeMap([
|
||
["sess-main", { type: "main", subagent_type: null, name: "Main", parent_agent_id: null }],
|
||
[
|
||
"sub-coder",
|
||
{
|
||
type: "subagent",
|
||
subagent_type: "coder",
|
||
name: "Coder",
|
||
parent_agent_id: "sess-main",
|
||
},
|
||
],
|
||
]);
|
||
expect(agentOriginLabel("sub-coder", map)).toBe("main › coder");
|
||
});
|
||
|
||
it("renders 'main › coder › explorer' for a 2-deep subagent", () => {
|
||
const map = makeMap([
|
||
["sess-main", { type: "main", subagent_type: null, name: "Main", parent_agent_id: null }],
|
||
[
|
||
"sub-coder",
|
||
{
|
||
type: "subagent",
|
||
subagent_type: "coder",
|
||
name: "Coder",
|
||
parent_agent_id: "sess-main",
|
||
},
|
||
],
|
||
[
|
||
"sub-explorer",
|
||
{
|
||
type: "subagent",
|
||
subagent_type: "explorer",
|
||
name: "Explorer",
|
||
parent_agent_id: "sub-coder",
|
||
},
|
||
],
|
||
]);
|
||
expect(agentOriginLabel("sub-explorer", map)).toBe("main › coder › explorer");
|
||
});
|
||
|
||
it("falls back to single-segment when the map is undefined (legacy callers)", () => {
|
||
const info: AgentInfo = {
|
||
type: "subagent",
|
||
subagent_type: "coder",
|
||
name: "Coder",
|
||
parent_agent_id: "sess-main",
|
||
};
|
||
expect(agentOriginLabel("sub-coder", info)).toBe("coder");
|
||
});
|
||
|
||
it("uses subagent name when subagent_type is null", () => {
|
||
const map = makeMap([
|
||
["sess-main", { type: "main", subagent_type: null, name: "Main", parent_agent_id: null }],
|
||
[
|
||
"sub-1",
|
||
{
|
||
type: "subagent",
|
||
subagent_type: null,
|
||
name: "Helper",
|
||
parent_agent_id: "sess-main",
|
||
},
|
||
],
|
||
]);
|
||
expect(agentOriginLabel("sub-1", map)).toBe("main › Helper");
|
||
});
|
||
|
||
it("breaks cleanly on a parent-chain cycle", () => {
|
||
// Pathological: a → b → a. Should not loop forever.
|
||
const map = makeMap([
|
||
[
|
||
"a",
|
||
{
|
||
type: "subagent",
|
||
subagent_type: "a",
|
||
name: "A",
|
||
parent_agent_id: "b",
|
||
},
|
||
],
|
||
[
|
||
"b",
|
||
{
|
||
type: "subagent",
|
||
subagent_type: "b",
|
||
name: "B",
|
||
parent_agent_id: "a",
|
||
},
|
||
],
|
||
]);
|
||
const result = agentOriginLabel("a", map);
|
||
expect(result).toBe("b › a");
|
||
});
|
||
|
||
it("returns null for null agentId regardless of map shape", () => {
|
||
expect(agentOriginLabel(null, makeMap([]))).toBeNull();
|
||
expect(agentOriginLabel(null, undefined)).toBeNull();
|
||
});
|
||
|
||
it("falls back to 'main' for IDs ending in -main when no info is available", () => {
|
||
expect(agentOriginLabel("session-xyz-main", makeMap([]))).toBe("main");
|
||
});
|
||
});
|
||
|
||
describe("projectFromCwd - session-cwd fallback for the dir prefix", () => {
|
||
it("returns the last path segment of a POSIX cwd", () => {
|
||
expect(projectFromCwd("/Users/dev/WebstormProjects/AgentHub")).toBe("AgentHub");
|
||
});
|
||
|
||
it("ignores a trailing slash", () => {
|
||
expect(projectFromCwd("/Users/dev/my-app/")).toBe("my-app");
|
||
});
|
||
|
||
it("handles Windows backslash paths", () => {
|
||
expect(projectFromCwd("C:\\Users\\dev\\my-app")).toBe("my-app");
|
||
});
|
||
|
||
it("returns null for null, undefined, or empty cwd", () => {
|
||
expect(projectFromCwd(null)).toBeNull();
|
||
expect(projectFromCwd(undefined)).toBeNull();
|
||
expect(projectFromCwd("")).toBeNull();
|
||
});
|
||
});
|