Files
AgentHub/client/src/lib/__tests__/agentOriginLabel.test.ts
T
nntrivi2001 e972267a42 fix(plugins): make the 10 focused plugins installable, finish the AgentHub rename
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.
2026-09-10 11:03:14 +07:00

147 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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();
});
});