diff --git a/actions/setup/js/parse_threat_detection_results.cjs b/actions/setup/js/parse_threat_detection_results.cjs index 4dfaeb7220c..c05e40822c2 100644 --- a/actions/setup/js/parse_threat_detection_results.cjs +++ b/actions/setup/js/parse_threat_detection_results.cjs @@ -46,9 +46,15 @@ function extractFromStreamJson(line) { // In stream-json mode, the same content appears in both; using only "result" // avoids double-counting. if (obj.type === "result" && typeof obj.result === "string") { - const resultStr = obj.result.trim(); - if (resultStr.startsWith(RESULT_PREFIX)) { - return resultStr; + // The result field contains the model's full response text, which may + // include analysis before the THREAT_DETECTION_RESULT line. + // Split by newlines and find the line that starts with the prefix. + const resultLines = obj.result.split("\n"); + for (const rline of resultLines) { + const rtrimmed = rline.trim(); + if (rtrimmed.startsWith(RESULT_PREFIX)) { + return rtrimmed; + } } } } catch { diff --git a/actions/setup/js/parse_threat_detection_results.test.cjs b/actions/setup/js/parse_threat_detection_results.test.cjs index 78d6b2b5ccb..b6531b320e0 100644 --- a/actions/setup/js/parse_threat_detection_results.test.cjs +++ b/actions/setup/js/parse_threat_detection_results.test.cjs @@ -42,6 +42,31 @@ describe("extractFromStreamJson", () => { expect(result).toContain("THREAT_DETECTION_RESULT:"); }); + it("should extract result when analysis text precedes the verdict line", () => { + // The model may include explanatory text before THREAT_DETECTION_RESULT in the result field + const line = + '{"type":"result","subtype":"success","result":"**Analysis complete.**\\n\\nNo threats found.\\n\\nTHREAT_DETECTION_RESULT:{\\"prompt_injection\\":false,\\"secret_leak\\":false,\\"malicious_patch\\":false,\\"reasons\\":[]}","stop_reason":"end_turn"}'; + const result = extractFromStreamJson(line); + // Ensure we extracted only the verdict line, not the preceding analysis text + expect(result).toMatch(/^THREAT_DETECTION_RESULT:/); + expect(result).not.toContain("**Analysis complete.**"); + }); + + it("should allow parseDetectionLog to parse extracted verdict when analysis text precedes it", () => { + const line = + '{"type":"result","subtype":"success","result":"**Analysis complete.**\\n\\nNo threats found.\\n\\nTHREAT_DETECTION_RESULT:{\\"prompt_injection\\":false,\\"secret_leak\\":false,\\"malicious_patch\\":false,\\"reasons\\":[]}","stop_reason":"end_turn"}'; + const extracted = extractFromStreamJson(line); + expect(extracted).not.toBeNull(); + const { verdict, error } = parseDetectionLog(extracted); + expect(error).toBeUndefined(); + expect(verdict).toEqual({ + prompt_injection: false, + secret_leak: false, + malicious_patch: false, + reasons: [], + }); + }); + it("should return null for type:assistant JSON (not authoritative)", () => { const line = '{"type":"assistant","message":{"content":[{"type":"text","text":"THREAT_DETECTION_RESULT:{\\"prompt_injection\\":false}"}]}}'; const result = extractFromStreamJson(line);