mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-11-15 03:57:46 +08:00
* feat(viewer): export a session to a self-contained HTML file (#1167) The only renderer for a review session was the live viewer HTTP server, so there was no way to archive a review as a CI artifact. Adds viewer.ExportSession, which drives the same template and the same LoadSession the HTTP handler does, but renders into an io.Writer with both /static/ assets inlined. The assets are passed as template.CSS and template.JS: html/template refuses to interpolate an untyped string into a <style> or <script>, and would silently emit ZgotmplZ for the stylesheet and a JSON string literal for the script. session.html gains three gates on a new Static field, so the served page is unchanged — the field is zero for every HTTP render — while the exported page inlines the assets and de-links the two site-absolute breadcrumb anchors, which are dead over file://. style.css widens nav.breadcrumb a to cover the .crumb span that replaces one of them. Exposed as `ocr session export [session-id]`, on `session` rather than as a `viewer --export` flag because `viewer` is cobra.NoArgs and session_cmd.go already owns session-id resolution and completion. The id is optional and defaults to the newest session: a successful `ocr review` never prints its session id, so requiring one would push every CI archive step through `--format json`. Output goes through the shared --output/-o writer, with "html" added to isMachineReadable so the page is not run through stripAnsiWriter, which would eat the ESC bytes in trace-quoted source. The page is rendered fully into a buffer before the first write, so a failure part-way through Execute cannot leave a truncated file behind. Also adds maximumArgs, mirroring the existing minimumArgs, so the optional positional reports the command's own usage rather than cobra's raw "accepts at most 1 arg(s), received 2". Documented in all five locales of cli-reference.md, including the warning that the artifact embeds reviewed source. Signed-off-by: ChethanUK <chethanuk@outlook.com> * refactor(viewer): share the header brand between served and exported pages Signed-off-by: ChethanUK <chethanuk@outlook.com> * feat: Apply batched suggestions from code review Co-authored-by: Kite <254839944+lizhengfeng101@users.noreply.github.com> --------- Signed-off-by: ChethanUK <chethanuk@outlook.com> Co-authored-by: Kite <254839944+lizhengfeng101@users.noreply.github.com>
138 lines
5.1 KiB
Go
138 lines
5.1 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2026 alibaba/open-code-review Contributors
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestCLIReferenceDocumentsSessionCompare pins that every locale of the CLI
|
|
// reference documents `ocr session compare`. The four files are hand-synced
|
|
// (see PR #920), so the usual failure is a new command landing in `en` only.
|
|
// ponytail: substring checks, not a markdown parse - the whole point is to
|
|
// catch a missing file, and a parser would not catch it any better.
|
|
func TestCLIReferenceDocumentsSessionCompare(t *testing.T) {
|
|
for _, locale := range []string{"en", "zh", "ja", "ru"} {
|
|
t.Run(locale, func(t *testing.T) {
|
|
path := filepath.Join("..", "..", "pages", "src", "content", "docs", locale, "cli-reference.md")
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
for _, want := range []string{
|
|
"`ocr session compare <before> <after>`", // command-summary table row
|
|
"### `ocr session compare`", // reference section
|
|
"`ocr session diff <before> <after>`", // alias
|
|
"not_reviewed", // the JSON bucket that is easy to forget
|
|
} {
|
|
if !strings.Contains(string(body), want) {
|
|
t.Errorf("%s: missing %q", path, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCLIReferenceDocumentsSessionExport is the sibling guard for
|
|
// `ocr session export`. It pins the command surface — the summary row, the
|
|
// reference section, the default no-id form and --output — across every
|
|
// locale, including `ko`, which the compare pin above predates.
|
|
//
|
|
// The note that the exported page embeds reviewed source is currently only in
|
|
// the English page, so it is not pinned here; translating it is left to the
|
|
// locale maintainers.
|
|
func TestCLIReferenceDocumentsSessionExport(t *testing.T) {
|
|
for _, locale := range []string{"en", "zh", "ja", "ru", "ko"} {
|
|
t.Run(locale, func(t *testing.T) {
|
|
path := filepath.Join("..", "..", "pages", "src", "content", "docs", locale, "cli-reference.md")
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
for _, want := range []string{
|
|
"`ocr session export [id]`", // command-summary table row
|
|
"### `ocr session export`", // reference section
|
|
"ocr session export -o review.html", // the no-id form, which is the default
|
|
"`--output <path>`", // the flag that makes it archivable
|
|
} {
|
|
if !strings.Contains(string(body), want) {
|
|
t.Errorf("%s: missing %q", path, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCLIReferenceUsesSubtaskUnit pins that every locale of the CLI reference
|
|
// describes --concurrency/--timeout/--max-tools/--max-tokens/--no-filter in
|
|
// terms of a subtask, not a file or file group. The five files are
|
|
// hand-synced; the usual failure is one locale keeping the old unit.
|
|
func TestCLIReferenceUsesSubtaskUnit(t *testing.T) {
|
|
type localePin struct {
|
|
locale string
|
|
term string
|
|
}
|
|
pins := []localePin{
|
|
{locale: "en", term: "subtask"},
|
|
{locale: "zh", term: "子任务"}, // allow-non-english: locale subtask term from issue 1273
|
|
{locale: "ja", term: "サブタスク"}, // allow-non-english: locale subtask term from issue 1273
|
|
{locale: "ko", term: "서브태스크"}, // allow-non-english: locale subtask term from issue 1273
|
|
{locale: "ru", term: "подзадач"}, // allow-non-english: locale subtask stem from issue 1273
|
|
}
|
|
flagMarkers := []string{
|
|
"| `--no-filter` |",
|
|
"| `--concurrency <n>` |",
|
|
"| `--timeout <minutes>` |",
|
|
"| `--max-tools <n>` |",
|
|
"| `--max-tokens <n>` |",
|
|
}
|
|
leakedConcurrencyUnits := []string{
|
|
"file group", "file groups", "files reviewed", "file scans",
|
|
"文件组", "文件数", // allow-non-english: old zh file/file-group unit
|
|
"ファイルグループ", "ファイルの最大", // allow-non-english: old ja file/file-group unit
|
|
"파일 그룹", // allow-non-english: old ko file-group unit
|
|
"число файлов", "группу файлов", "группы файлов", // allow-non-english: old ru file/file-group unit
|
|
}
|
|
|
|
for _, pin := range pins {
|
|
t.Run(pin.locale, func(t *testing.T) {
|
|
path := filepath.Join("..", "..", "pages", "src", "content", "docs", pin.locale, "cli-reference.md")
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
body := string(raw)
|
|
for _, marker := range flagMarkers {
|
|
row, ok := firstLineContaining(body, marker)
|
|
if !ok {
|
|
t.Errorf("%s: missing flag row %q", path, marker)
|
|
continue
|
|
}
|
|
if !strings.Contains(row, pin.term) {
|
|
t.Errorf("%s: row %q does not use %q", path, row, pin.term)
|
|
}
|
|
if marker == "| `--concurrency <n>` |" {
|
|
for _, leaked := range leakedConcurrencyUnits {
|
|
if strings.Contains(row, leaked) {
|
|
t.Errorf("%s: --concurrency still describes a file/file-group count (%q): %s", path, leaked, row)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func firstLineContaining(body, marker string) (string, bool) {
|
|
for _, line := range strings.Split(body, "\n") {
|
|
if strings.Contains(line, marker) {
|
|
return line, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|