Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 4 additions & 16 deletions actions/setup/js/messages_staged.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,9 @@ const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cj
*/
function getStagedTitle(ctx) {
const messages = getMessages();

// Create context with both camelCase and snake_case keys
const templateContext = toSnakeCase(ctx);

// Default staged title template
const defaultTitle = "## πŸ” Preview: {operation}";

// Use custom title if configured
return messages?.stagedTitle ? renderTemplate(messages.stagedTitle, templateContext) : renderTemplate(defaultTitle, templateContext);
const configuredTemplate = typeof messages?.stagedTitle === "string" ? messages.stagedTitle : "";
return renderTemplate(configuredTemplate || "## πŸ” Preview: {operation}", templateContext);
}

/**
Expand All @@ -40,15 +34,9 @@ function getStagedTitle(ctx) {
*/
function getStagedDescription(ctx) {
const messages = getMessages();

// Create context with both camelCase and snake_case keys
const templateContext = toSnakeCase(ctx);

// Default staged description template
const defaultDescription = "πŸ“‹ The following operations would be performed if staged mode was disabled:";

// Use custom description if configured
return messages?.stagedDescription ? renderTemplate(messages.stagedDescription, templateContext) : renderTemplate(defaultDescription, templateContext);
const configuredTemplate = typeof messages?.stagedDescription === "string" ? messages.stagedDescription : "";
return renderTemplate(configuredTemplate || "πŸ“‹ The following operations would be performed if staged mode was disabled:", templateContext);
}

module.exports = {
Expand Down
145 changes: 145 additions & 0 deletions actions/setup/js/messages_staged.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// @ts-check
import { describe, it, expect, beforeEach, vi } from "vitest";

// messages_core.cjs calls core.warning on parse failures - provide a stub
const mockCore = {
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
};
global.core = mockCore;

const { getStagedTitle, getStagedDescription } = require("./messages_staged.cjs");

const OPERATION = "Create Issues";

describe("messages_staged", () => {
beforeEach(() => {
vi.clearAllMocks();
delete process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
});

describe("getStagedTitle", () => {
it("returns the default title with operation substituted", () => {
const title = getStagedTitle({ operation: OPERATION });
expect(title).toBe(`## πŸ” Preview: ${OPERATION}`);
});

it("substitutes a different operation value", () => {
const title = getStagedTitle({ operation: "Add Comments" });
expect(title).toBe("## πŸ” Preview: Add Comments");
});

it("uses custom template from config", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedTitle: "Staging: {operation}" });
const title = getStagedTitle({ operation: OPERATION });
expect(title).toBe(`Staging: ${OPERATION}`);
});

it("falls back to default when stagedTitle is absent from config", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ runSuccess: "other" });
const title = getStagedTitle({ operation: OPERATION });
expect(title).toBe(`## πŸ” Preview: ${OPERATION}`);
});

it("falls back to default when stagedTitle is an empty string", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedTitle: "" });
const title = getStagedTitle({ operation: OPERATION });
expect(title).toBe(`## πŸ” Preview: ${OPERATION}`);
});

it("falls back to default when stagedTitle is a falsy non-string value", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedTitle: 0 });
const title = getStagedTitle({ operation: OPERATION });
expect(title).toBe(`## πŸ” Preview: ${OPERATION}`);
});

it("falls back to default when config is invalid JSON", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = "not-json";
const title = getStagedTitle({ operation: OPERATION });
expect(title).toBe(`## πŸ” Preview: ${OPERATION}`);
});
Comment on lines +35 to +63
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated implementation in messages_staged.cjs now relies on ?? for template fallback, which behaves differently for empty-string (and other falsy, non-nullish) configured values. There isn’t currently a test that locks in the intended behavior when stagedTitle/stagedDescription is set to "" (or another falsy value) in GH_AW_SAFE_OUTPUT_MESSAGES. Adding an explicit test case for this would prevent accidental behavior changes and clarify whether empty strings should fall back or be treated as valid templates.

Copilot uses AI. Check for mistakes.

it("supports snake_case placeholders from camelCase context keys", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedTitle: "{operation_name}: {operation}" });
const title = getStagedTitle({ operation: OPERATION, operationName: "Create Comment" });
expect(title).toBe(`Create Comment: ${OPERATION}`);
});

it("returns an empty-operation title when operation is an empty string", () => {
const title = getStagedTitle({ operation: "" });
expect(title).toBe("## πŸ” Preview: ");
});

it("leaves unrecognised placeholders unchanged in custom template", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedTitle: "{operation} | {unknown}" });
const title = getStagedTitle({ operation: OPERATION });
expect(title).toBe(`${OPERATION} | {unknown}`);
});
});

describe("getStagedDescription", () => {
it("returns the default description", () => {
const desc = getStagedDescription({ operation: OPERATION });
expect(desc).toBe("πŸ“‹ The following operations would be performed if staged mode was disabled:");
});

it("uses custom description template from config", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedDescription: "Preview of: {operation}" });
const desc = getStagedDescription({ operation: OPERATION });
expect(desc).toBe(`Preview of: ${OPERATION}`);
});

it("falls back to default when stagedDescription is absent from config", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedTitle: "title only" });
const desc = getStagedDescription({ operation: OPERATION });
expect(desc).toBe("πŸ“‹ The following operations would be performed if staged mode was disabled:");
});

it("falls back to default when stagedDescription is an empty string", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedDescription: "" });
const desc = getStagedDescription({ operation: OPERATION });
expect(desc).toBe("πŸ“‹ The following operations would be performed if staged mode was disabled:");
});

it("falls back to default when stagedDescription is a falsy non-string value", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedDescription: false });
const desc = getStagedDescription({ operation: OPERATION });
expect(desc).toBe("πŸ“‹ The following operations would be performed if staged mode was disabled:");
});

it("falls back to default when config is invalid JSON", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = "{bad json";
const desc = getStagedDescription({ operation: OPERATION });
expect(desc).toBe("πŸ“‹ The following operations would be performed if staged mode was disabled:");
});

it("supports custom description with operation placeholder", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ stagedDescription: "Would run: {operation}" });
const desc = getStagedDescription({ operation: "Close PRs" });
expect(desc).toBe("Would run: Close PRs");
});

it("default description does not contain unfilled placeholders", () => {
const desc = getStagedDescription({ operation: OPERATION });
expect(desc).not.toMatch(/\{[^}]+\}/);
});
});

describe("independent config keys", () => {
it("getStagedTitle and getStagedDescription use their own config keys independently", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
stagedTitle: "Custom title: {operation}",
stagedDescription: "Custom desc: {operation}",
});

const title = getStagedTitle({ operation: OPERATION });
const desc = getStagedDescription({ operation: OPERATION });

expect(title).toBe(`Custom title: ${OPERATION}`);
expect(desc).toBe(`Custom desc: ${OPERATION}`);
});
});
});
Loading