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
22 changes: 18 additions & 4 deletions .github/workflows/test-claude-command.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 18 additions & 4 deletions .github/workflows/test-codex-command.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 18 additions & 4 deletions .github/workflows/test-safe-outputs-custom-engine.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 19 additions & 4 deletions pkg/workflow/js/missing_tool.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,28 @@ async function main() {
return;
}

// Parse as JSON array
const parsedData = JSON.parse(agentOutput);
// Parse the validated output JSON
let validatedOutput;
try {
validatedOutput = JSON.parse(agentOutput);
} catch (error) {
core.error(
`Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`
);
return;
}

if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
core.info("No valid items found in agent output");
core.setOutput("tools_reported", JSON.stringify(missingTools));
core.setOutput("total_count", missingTools.length.toString());
return;
}

core.info(`Parsed agent output with ${parsedData.length} entries`);
core.info(`Parsed agent output with ${validatedOutput.items.length} entries`);

// Process all parsed entries
for (const entry of parsedData) {
for (const entry of validatedOutput.items) {
if (entry.type === "missing-tool") {
// Validate required fields
if (!entry.tool) {
Expand Down
179 changes: 108 additions & 71 deletions pkg/workflow/js/missing_tool.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,22 @@ describe("missing_tool.cjs", () => {

describe("JSON Array Input Format", () => {
it("should parse JSON array with missing-tool entries", async () => {
const testData = [
{
type: "missing-tool",
tool: "docker",
reason: "Need containerization support",
alternatives: "Use VM or manual setup",
},
{
type: "missing-tool",
tool: "kubectl",
reason: "Kubernetes cluster management required",
},
];
const testData = {
items: [
{
type: "missing-tool",
tool: "docker",
reason: "Need containerization support",
alternatives: "Use VM or manual setup",
},
{
type: "missing-tool",
tool: "kubectl",
reason: "Kubernetes cluster management required",
},
],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);

Expand All @@ -99,22 +102,25 @@ describe("missing_tool.cjs", () => {
});

it("should filter out non-missing-tool entries", async () => {
const testData = [
{
type: "missing-tool",
tool: "docker",
reason: "Need containerization",
},
{
type: "other-type",
data: "should be ignored",
},
{
type: "missing-tool",
tool: "kubectl",
reason: "Need k8s support",
},
];
const testData = {
items: [
{
type: "missing-tool",
tool: "docker",
reason: "Need containerization",
},
{
type: "other-type",
data: "should be ignored",
},
{
type: "missing-tool",
tool: "kubectl",
reason: "Need k8s support",
},
],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);

Expand All @@ -133,60 +139,69 @@ describe("missing_tool.cjs", () => {

describe("Validation", () => {
it("should skip entries missing tool field", async () => {
const testData = [
{
type: "missing-tool",
reason: "No tool specified",
},
{
type: "missing-tool",
tool: "valid-tool",
reason: "This should work",
},
];
const testData = {
items: [
{
type: "missing-tool",
reason: "No tool specified",
},
{
type: "missing-tool",
tool: "valid-tool",
reason: "This should work",
},
],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);

await runScript();

expect(mockCore.setOutput).toHaveBeenCalledWith("total_count", "1");
expect(mockCore.warning).toHaveBeenCalledWith(
`missing-tool entry missing 'tool' field: ${JSON.stringify(testData[0])}`
`missing-tool entry missing 'tool' field: ${JSON.stringify(testData.items[0])}`
);
});

it("should skip entries missing reason field", async () => {
const testData = [
{
type: "missing-tool",
tool: "some-tool",
},
{
type: "missing-tool",
tool: "valid-tool",
reason: "This should work",
},
];
const testData = {
items: [
{
type: "missing-tool",
tool: "some-tool",
},
{
type: "missing-tool",
tool: "valid-tool",
reason: "This should work",
},
],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);

await runScript();

expect(mockCore.setOutput).toHaveBeenCalledWith("total_count", "1");
expect(mockCore.warning).toHaveBeenCalledWith(
`missing-tool entry missing 'reason' field: ${JSON.stringify(testData[0])}`
`missing-tool entry missing 'reason' field: ${JSON.stringify(testData.items[0])}`
);
});
});

describe("Max Reports Limit", () => {
it("should respect max reports limit", async () => {
const testData = [
{ type: "missing-tool", tool: "tool1", reason: "reason1" },
{ type: "missing-tool", tool: "tool2", reason: "reason2" },
{ type: "missing-tool", tool: "tool3", reason: "reason3" },
{ type: "missing-tool", tool: "tool4", reason: "reason4" },
];
const testData = {
items: [
{ type: "missing-tool", tool: "tool1", reason: "reason1" },
{ type: "missing-tool", tool: "tool2", reason: "reason2" },
{ type: "missing-tool", tool: "tool3", reason: "reason3" },
{ type: "missing-tool", tool: "tool4", reason: "reason4" },
],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);
process.env.GITHUB_AW_MISSING_TOOL_MAX = "2";
Expand All @@ -208,11 +223,14 @@ describe("missing_tool.cjs", () => {
});

it("should work without max limit", async () => {
const testData = [
{ type: "missing-tool", tool: "tool1", reason: "reason1" },
{ type: "missing-tool", tool: "tool2", reason: "reason2" },
{ type: "missing-tool", tool: "tool3", reason: "reason3" },
];
const testData = {
items: [
{ type: "missing-tool", tool: "tool1", reason: "reason1" },
{ type: "missing-tool", tool: "tool2", reason: "reason2" },
{ type: "missing-tool", tool: "tool3", reason: "reason3" },
],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);
// No GITHUB_AW_MISSING_TOOL_MAX set
Expand All @@ -233,6 +251,22 @@ describe("missing_tool.cjs", () => {
expect(mockCore.info).toHaveBeenCalledWith("No agent output to process");
});

it("should handle agent output with empty items array", async () => {
const testData = {
items: [],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);

await runScript();

expect(mockCore.setOutput).toHaveBeenCalledWith("total_count", "0");
expect(mockCore.info).toHaveBeenCalledWith(
"Parsed agent output with 0 entries"
);
});

it("should handle missing environment variables", async () => {
// Don't set any environment variables

Expand All @@ -242,13 +276,16 @@ describe("missing_tool.cjs", () => {
});

it("should add timestamp to reported tools", async () => {
const testData = [
{
type: "missing-tool",
tool: "test-tool",
reason: "testing timestamp",
},
];
const testData = {
items: [
{
type: "missing-tool",
tool: "test-tool",
reason: "testing timestamp",
},
],
errors: [],
};

process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(testData);

Expand Down