diff --git a/src/github/commands.ts b/src/github/commands.ts index b4f9d662e9..1fe6150eae 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -371,6 +371,7 @@ export function buildPublicAgentCommandComment(args: { const commandName = args.command.name as GittensoryMentionCommandName; const sections = commandSections( commandName, + args.env, args.bundle, args.officialMiner, args.maintainerDigest, @@ -707,6 +708,7 @@ function feedbackPromptSections(answerId: string | null | undefined): string[] { function commandSections( command: GittensoryMentionCommandName, + env: GittensoryFooterEnv, bundle: AgentRunBundle | null | undefined, officialMiner: GittensorContributorSnapshot | null | undefined, maintainerDigest: MaintainerQueueDigest | null | undefined, @@ -716,7 +718,7 @@ function commandSections( ): string[] { switch (command) { case "help": - return helpSections(unknownVerb); + return helpSections(env, unknownVerb); case "ask": return askSections(bundle, question); case "miner-context": @@ -761,7 +763,21 @@ function actionCommandHelpSections(): string[] { ]; } -function helpSections(unknownVerb?: string | undefined): string[] { +/** The public command-reference doc link for `@gittensory help` (#4670). `new URL(path, origin)` -- same + * idiom as the sibling `maintainerControlPanelUrl` in footer.ts -- so a `PUBLIC_SITE_ORIGIN` with or + * without a trailing slash both resolve correctly instead of risking a double slash from naive + * concatenation. Falls back to the literal path string only if origin resolution itself throws (an + * operator-misconfigured PUBLIC_SITE_ORIGIN should degrade the link, not crash comment rendering). */ +function commandReferenceUrl(env: GittensoryFooterEnv): string { + const origin = env.PUBLIC_SITE_ORIGIN ?? GITTENSORY_SITE_URL; + try { + return new URL("/docs/gittensory-commands", origin).toString(); + } catch { + return `${GITTENSORY_SITE_URL}/docs/gittensory-commands`; + } +} + +function helpSections(env: GittensoryFooterEnv, unknownVerb?: string | undefined): string[] { return [ "**Commands**", "", @@ -788,7 +804,7 @@ function helpSections(unknownVerb?: string | undefined): string[] { "", ...actionCommandHelpSections(), "", - `- Full command reference (syntax, roles, gate boundary): ${GITTENSORY_SITE_URL}/docs/gittensory-commands`, + `- Full command reference (syntax, roles, gate boundary): ${commandReferenceUrl(env)}`, ]; } @@ -1697,4 +1713,5 @@ export const githubCommandsInternals = { askSections, helpSections, actionCommandHelpSections, + commandReferenceUrl, }; diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index e2633b8dbb..01654fd228 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -133,11 +133,11 @@ describe("GitHub mention commands", () => { }); it("helpSections renders did-you-mean only for close unknown verbs (#2170)", () => { - const typo = githubCommandsInternals.helpSections("reveiw"); + const typo = githubCommandsInternals.helpSections({}, "reveiw"); expect(typo.join("\n")).toContain("Did you mean `@gittensory review`?"); - const far = githubCommandsInternals.helpSections("zzzz"); + const far = githubCommandsInternals.helpSections({}, "zzzz"); expect(far.join("\n")).not.toContain("Did you mean"); - const bare = githubCommandsInternals.helpSections(); + const bare = githubCommandsInternals.helpSections({}); expect(bare.join("\n")).not.toContain("Did you mean"); expect(bare.join("\n")).toContain("**Commands**"); }); @@ -152,7 +152,7 @@ describe("GitHub mention commands", () => { }); it("helpSections documents every PR action command with authorization notes (#2167)", () => { - const body = githubCommandsInternals.helpSections().join("\n"); + const body = githubCommandsInternals.helpSections({}).join("\n"); expect(body).toContain("**PR action commands**"); expect(body).toContain("maintainer or collaborator authorization"); expect(body).toContain("pause` and `resume` affect only auto-review"); @@ -175,11 +175,23 @@ describe("GitHub mention commands", () => { }); it("helpSections links to the public command reference doc (#2171)", () => { - const body = githubCommandsInternals.helpSections().join("\n"); + const body = githubCommandsInternals.helpSections({}).join("\n"); expect(body).toContain("https://gittensory.aethereal.dev/docs/gittensory-commands"); expect(body).toContain("Full command reference"); }); + it("helpSections' command reference link follows a self-hoster's PUBLIC_SITE_ORIGIN, trailing slash and all (#4670)", () => { + const body = githubCommandsInternals.helpSections({ PUBLIC_SITE_ORIGIN: "https://my-instance.example.com/" }).join("\n"); + expect(body).toContain("https://my-instance.example.com/docs/gittensory-commands"); + expect(body).not.toContain("gittensory.aethereal.dev"); + }); + + it("commandReferenceUrl falls back to the default site when PUBLIC_SITE_ORIGIN is malformed", () => { + expect(githubCommandsInternals.commandReferenceUrl({ PUBLIC_SITE_ORIGIN: "not a url" })).toBe( + "https://gittensory.aethereal.dev/docs/gittensory-commands", + ); + }); + it("isGittensoryActionCommand distinguishes action verbs from Q&A commands", () => { for (const action of ["gate-override", "review", "pause", "resume", "resolve", "configuration", "explain"] as const) { expect(isGittensoryActionCommand(action)).toBe(true);