mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-10-21 05:29:48 +00:00
Some checks are pending
CI / test (push) Waiting to run
CI / windows (push) Waiting to run
CI / cross-compile (amd64, darwin) (push) Waiting to run
CI / cross-compile (amd64, windows) (push) Waiting to run
CI / cross-compile (arm64, darwin) (push) Waiting to run
CI / cross-compile (arm64, linux) (push) Waiting to run
CI / cross-compile (arm64, windows) (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Pages / build (push) Waiting to run
Deploy Pages / deploy (push) Blocked by required conditions
* feat(cli): add --output flag to write review/scan results to a file Add `--output <path>` / `-o` to `ocr review` and `ocr scan`, writing the result JSON or text directly to a UTF-8 file instead of stdout. The file is created lazily on the first write so a failed run never truncates an existing target; text-mode files are ANSI-stripped so terminal color codes never pollute the result file. A `[ocr] Results written to <path>` hint is printed to stderr once the file is actually created, and failure output keeps going to stderr so agents always find the failure reason. Closes #851 Signed-off-by: 历代星辰 * test(cli): cover --output flag parsing and file output behavior Add tests for --output/-o flag parsing, the stripAnsiWriter state machine (including escape sequences split across Write calls), lazy file creation (failed runs leave existing targets untouched, never-written targets are not created), and the Results-written stderr hint. Adapt existing emitRunResult / renderComment / outputPreview call sites to the new io.Writer parameter. Signed-off-by: 历代星辰 * fix(test): isolate USERPROFILE so Windows tests never touch the real OCR home os.UserHomeDir() prefers USERPROFILE over HOME on Windows, so t.Setenv("HOME", ...) alone left tests reading and writing the developer's real ~/.opencodereview: config tests overwrote config.json and session/agent tests polluted the sessions store. Add a setTestHome helper (per affected package) that also overrides USERPROFILE, and route every scattered HOME override through it. Signed-off-by: 历代星辰 * fix(cli): propagate output write failures and strip multi-byte ANSI escapes Address the ocr review findings on the --output feature: - lazyFileWriter now records the first write error (Err()) and emits the "Results written" hint only after a successful write; emitRunResult and outputPreview check it after text rendering, so a failed --output write (permission, disk full) exits non-zero like JSON mode already does instead of silently exiting 0 with no file. - stripAnsiWriter keeps multi-byte escapes (ESC + intermediate byte, DCS/PM/APC strings) inside the escape state so trailing bytes are discarded with the sequence instead of leaking into the result file. Signed-off-by: 历代星辰 * fix(cli): re-parse trailing byte after bare-ESC OSC termination and fix Write return semantics Addresses review comments on #852: - stripAnsiWriter ansiOSCEsc: a non-ST byte after ESC is no longer dropped. The OSC ends at a bare ESC terminator and the trailing byte is re-parsed — an ESC starts a new escape sequence, any other byte is forwarded as text. Previously the first byte after a bare-ESC-terminated OSC was silently lost. - stripAnsiWriter Write: report the underlying dst error but return len(p), since the state machine has already consumed the input; returning 0 made a caller retrying on n < len(p) feed the same bytes through twice. - Clarify --output help text: default is stdout and '-' also means stdout. Adds regression tests for both bugs (bare-ESC + trailing text, bare-ESC + new escape, and dst-failure return contract). Signed-off-by: 历代星辰 * fix(cli): reinforce ANSI stripper state machine and validate format flag * docs(cli): document output flag across localized references and READMEs * fix(cli): cap escape intermediate bytes and normalize format in output handlers --------- Signed-off-by: 历代星辰
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2026 alibaba/open-code-review Contributors
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/alibaba/open-code-review/internal/model"
|
|
)
|
|
|
|
func TestBuildBadge(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
comment model.LlmComment
|
|
want string
|
|
}{
|
|
{"both fields", model.LlmComment{Category: "security", Severity: "high"}, "[security · high]"},
|
|
{"category only", model.LlmComment{Category: "bug"}, "[bug]"},
|
|
{"severity only", model.LlmComment{Severity: "low"}, "[low]"},
|
|
{"neither", model.LlmComment{}, ""},
|
|
{"strips control chars", model.LlmComment{Category: "bug\x1b[0m", Severity: "high"}, "[bug[0m · high]"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := buildBadge(tt.comment); got != tt.want {
|
|
t.Errorf("buildBadge() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSeverityColor(t *testing.T) {
|
|
// Each known severity must map to a distinct color; unknown falls back to dim.
|
|
seen := map[string]string{}
|
|
for _, sev := range []string{"critical", "high", "medium", "low"} {
|
|
c := severityColor(sev)
|
|
if c == "" {
|
|
t.Errorf("severityColor(%q) is empty", sev)
|
|
}
|
|
if prev, ok := seen[c]; ok {
|
|
t.Errorf("severityColor(%q) shares color with %q", sev, prev)
|
|
}
|
|
seen[c] = sev
|
|
}
|
|
if got := severityColor("bogus"); got != "\033[2m" {
|
|
t.Errorf("severityColor(unknown) = %q, want dim", got)
|
|
}
|
|
if got := severityColor(""); got != "\033[2m" {
|
|
t.Errorf("severityColor(empty) = %q, want dim", got)
|
|
}
|
|
}
|
|
|
|
// TestRenderComment_BadgeInline verifies the badge is colorized and rendered inline
|
|
// with the first line of the comment content.
|
|
func TestRenderComment_BadgeInline(t *testing.T) {
|
|
setColor(t, true)
|
|
out := captureStdout(t, func() {
|
|
renderComment(model.LlmComment{
|
|
Path: "internal/mcp/client.go",
|
|
StartLine: 27,
|
|
EndLine: 27,
|
|
Content: "Potential environment variable leak.",
|
|
Category: "security",
|
|
Severity: "high",
|
|
}, os.Stdout)
|
|
})
|
|
if !strings.Contains(out, "[security · high]") {
|
|
t.Errorf("expected badge in output, got:\n%s", out)
|
|
}
|
|
// severity high → bright red; the badge must be wrapped in the color + reset.
|
|
if !strings.Contains(out, "\033[91m[security · high]\033[0m") {
|
|
t.Errorf("expected colorized badge, got:\n%q", out)
|
|
}
|
|
if !strings.Contains(out, "Potential environment variable leak.") {
|
|
t.Errorf("expected content in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestSanitizeTerminal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"plain text unchanged", "hello world", "hello world"},
|
|
{"preserves tab", "col1\tcol2", "col1\tcol2"},
|
|
{"preserves newline", "line1\nline2", "line1\nline2"},
|
|
{"strips ESC", "before\x1b[2Jafter", "before[2Jafter"},
|
|
{"strips OSC 52", "\x1b]52;c;dGVzdA==\x07", "]52;c;dGVzdA=="},
|
|
{"strips BEL alone", "beep\x07done", "beepdone"},
|
|
{"strips null byte", "a\x00b", "ab"},
|
|
{"strips DEL", "a\x7fb", "ab"},
|
|
{"strips carriage return", "fake\rreal", "fakereal"},
|
|
{"empty string", "", ""},
|
|
{"only control chars", "\x1b\x07\x00\x7f", ""},
|
|
{"unicode preserved", "代码审查 レビュー 🔍", "代码审查 レビュー 🔍"}, // allow-non-english: fixture asserts non-ASCII output is preserved verbatim
|
|
{"mixed safe and unsafe", "path\x1b[0m/file.go", "path[0m/file.go"},
|
|
{"strips C1 CSI (U+009B)", "before\u009bafter", "beforeafter"},
|
|
{"strips C1 OSC (U+009D)", "before\u009dafter", "beforeafter"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := sanitizeTerminal(tt.in)
|
|
if got != tt.want {
|
|
t.Errorf("sanitizeTerminal(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|