Files
Agent-System-Backend/internal/docgen/renderer_integration_test.go
T
weichenandCursor 1afe164798 Pin published company default renderer and drop candidate wording
Why: agent-document-renderer 31fbe54 publishes company.default@1.0.0 with
review status approved and publicationAllowed true (owner decision
2026-10-16: A4, ~2.5cm margins, SimSun + Times New Roman 12pt body, SimHei
headings, no logo). Backend still hard-coded Candidate=true and every prompt,
skill and tool text called the default a pending-human-visual-review
candidate.

Evidence: go vet ./... exit 0; gofmt -l on changed packages empty;
go test -count=1 ./internal/docgen/... ./internal/prompts/...
./internal/skills/... ./internal/tools/... ok; go test -count=1 ./... exit 0
(42 packages ok); go build ./... and CGO_ENABLED=0 go build ./cmd/agent-system
ok; git diff --cached --check clean. New tests failed before the change.

Impact: bump deps/agent-document-renderer 0d1c512 -> 31fbe54 and regenerate
the renderer identity. The manifest profile receipt now reports the
renderer's own review status and derives Candidate from the snapshot state
(false for the published default). Prompts (base.deliverables v12,
tool.render_document v2, task.report v4, task.doc-minutes v4), docx-author
and house-style skills, the render_document tool description and comparison
exception details describe the default as published (approved by owner
2026-10-16) while still limiting it to ordinary uncontrolled workspace
working/preview Word. Routing is unchanged: project, controlled,
special-template, review and approved outputs still go to RDM, with no
Backend default fallback and no legacy Word builder fallback.

Refs: unified renderer plan Tasks 7/10
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 05:07:19 +00:00

821 lines
30 KiB
Go

package docgen
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"encoding/xml"
"errors"
"io"
"os/exec"
"path/filepath"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
markdownrenderer "agent-document-renderer/markdown"
"agent-document-renderer/model"
"agent-document-renderer/profile"
"agent-document-renderer/profiles"
"agent-document-renderer/render"
)
const representativeMarkdown = `# 研发周会纪要
这是普通工作区交付物的正文。
## 要点
- 统一解析入口
- 保留 Markdown
| 项目 | 状态 |
|---|---|
| 解析器 | 统一 |
![公司默认样图](asset:company.default.sample-image@1.0.0)
:::callout warning
公司默认规则只用于普通工作区交付。
:::
:::page-break
分页后的结论。
`
func workspaceRequest(markdown []byte) Request {
return Request{
DocumentID: "workspace-minutes",
Title: "研发周会纪要",
DocumentType: "general",
Language: "zh-CN",
Markdown: markdown,
Mode: render.ModePreview,
FrozenAt: time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC),
RequestedBy: "alice",
}
}
func TestGenerateProducesDeterministicHashedDeliveryPackage(t *testing.T) {
request := workspaceRequest([]byte(representativeMarkdown))
first, err := Generate(context.Background(), request)
if err != nil {
t.Fatalf("first Generate: %v", err)
}
second, err := Generate(context.Background(), request)
if err != nil {
t.Fatalf("second Generate: %v", err)
}
for _, pair := range []struct {
name string
a Artifact
b Artifact
}{
{"Markdown", first.Markdown, second.Markdown},
{"DOCX", first.DOCX, second.DOCX},
{"manifest", first.Manifest, second.Manifest},
{"archive", first.Archive, second.Archive},
} {
if pair.a.SHA256 == "" || pair.a.SHA256 != pair.b.SHA256 || !bytes.Equal(pair.a.Bytes, pair.b.Bytes) {
t.Errorf("%s is not byte deterministic: first=%s second=%s", pair.name, pair.a.SHA256, pair.b.SHA256)
}
}
var manifest Manifest
if err := json.Unmarshal(first.Manifest.Bytes, &manifest); err != nil {
t.Fatalf("decode manifest: %v", err)
}
bundle, err := profiles.LoadCompanyDefault()
if err != nil {
t.Fatalf("load renderer company default: %v", err)
}
if manifest.SchemaVersion != ManifestSchemaVersion || manifest.RendererIdentity != RendererIdentity ||
manifest.SelectionSource != "organization-default" || manifest.Profile.Candidate ||
manifest.Profile.ReviewStatus != bundle.Review.Status || manifest.Profile.PublicationAllowed != bundle.Review.PublicationAllowed {
t.Fatalf("manifest profile provenance = %#v, renderer review = %#v", manifest, bundle.Review)
}
if manifest.Profile.ReviewStatus != "approved" || !manifest.Profile.PublicationAllowed {
t.Fatalf("published organization default reported review=%q publicationAllowed=%v", manifest.Profile.ReviewStatus, manifest.Profile.PublicationAllowed)
}
if manifest.Profile.ProfileSHA256 == "" || manifest.Profile.AssetManifestSHA256 == "" || manifest.Profile.ReviewSHA256 == "" {
t.Fatalf("manifest omitted source hashes: %#v", manifest.Profile)
}
listed := map[string]string{}
for _, item := range manifest.Artifacts {
listed[item.Name] = item.SHA256
}
if listed[first.Markdown.Name] != first.Markdown.SHA256 || listed[first.DOCX.Name] != first.DOCX.SHA256 {
t.Fatalf("manifest artifact hashes = %#v, want Markdown=%s DOCX=%s", listed, first.Markdown.SHA256, first.DOCX.SHA256)
}
if manifest.Renderer.ArtifactHash != first.DOCX.SHA256 || manifest.Renderer.FrozenAt != request.FrozenAt || manifest.Renderer.RequestedBy != request.RequestedBy {
t.Fatalf("renderer receipt does not match frozen request: %#v", manifest.Renderer)
}
zr, err := zip.NewReader(bytes.NewReader(first.Archive.Bytes), int64(len(first.Archive.Bytes)))
if err != nil {
t.Fatalf("open delivery archive: %v", err)
}
archiveFiles := map[string][]byte{}
for _, file := range zr.File {
stream, err := file.Open()
if err != nil {
t.Fatal(err)
}
raw, err := io.ReadAll(stream)
stream.Close()
if err != nil {
t.Fatal(err)
}
archiveFiles[file.Name] = raw
}
for _, item := range []Artifact{first.Markdown, first.DOCX, first.Manifest} {
if !bytes.Equal(archiveFiles[item.Name], item.Bytes) {
t.Errorf("archive %s bytes do not match delivered artifact", item.Name)
}
}
}
func TestGeneratePersistsCanonicalMarkdownWithStableNodeParity(t *testing.T) {
delivery, err := Generate(context.Background(), workspaceRequest([]byte(representativeMarkdown)))
if err != nil {
t.Fatalf("Generate: %v", err)
}
reparsed, err := markdownrenderer.Parse(delivery.Markdown.Bytes, markdownrenderer.Options{
FirstImport: false,
MaxBytes: 2 << 20,
MaxNodes: 10_000,
MaxDepth: 64,
})
if err != nil {
t.Fatalf("persisted Markdown is not independently parseable: %v", err)
}
if !reflect.DeepEqual(reparsed.Document, delivery.Document) {
t.Fatalf("canonical nodes changed across the persisted Markdown entry\nfirst: %#v\nsecond: %#v", delivery.Document, reparsed.Document)
}
if reparsed.Metadata.DocumentID != "workspace-minutes" || reparsed.Metadata.ProjectID != WorkspaceProjectID ||
reparsed.Metadata.DocumentType != "general" || reparsed.Metadata.Title != "研发周会纪要" ||
reparsed.Metadata.Language != "zh-CN" || reparsed.Metadata.ProfileID != "company.default" ||
reparsed.Metadata.ProfileVersion != "1.0.0" || reparsed.Metadata.Controlled {
t.Fatalf("controlled metadata changed: %#v", reparsed.Metadata)
}
wantKinds := map[model.NodeKind]int{
model.NodeHeading: 2,
model.NodeParagraph: 2,
model.NodeList: 1,
model.NodeTable: 1,
model.NodeImage: 1,
model.NodeCallout: 1,
model.NodePageBreak: 1,
}
gotKinds := map[model.NodeKind]int{}
seenIDs := map[string]bool{}
var visit func(model.Node)
visit = func(node model.Node) {
if node.ID == "" || seenIDs[node.ID] {
t.Fatalf("node ID is missing or unstable: %q", node.ID)
}
seenIDs[node.ID] = true
gotKinds[node.Kind]++
for _, child := range node.Children {
visit(child)
}
}
visit(delivery.Document.Root)
for kind, want := range wantKinds {
if got := gotKinds[kind]; got != want {
t.Errorf("%s nodes = %d, want %d", kind, got, want)
}
}
if !strings.Contains(string(delivery.Markdown.Bytes), "<!-- node-id:") {
t.Fatal("persisted Markdown omitted stable node markers")
}
}
func TestGeneratePreservesCompleteLegacyMarkdownContract(t *testing.T) {
request := workspaceRequest([]byte(sampleMinutes))
delivery, err := Generate(context.Background(), request)
if err != nil {
t.Fatal(err)
}
replayed, err := markdownrenderer.Parse(delivery.Markdown.Bytes, markdownrenderer.Options{
MaxBytes: 2 << 20, MaxNodes: 10_000, MaxDepth: 64,
})
if err != nil || !reflect.DeepEqual(delivery.Document, replayed.Document) {
t.Fatalf("complete legacy fixture did not replay canonically: err=%v", err)
}
wantKinds := []model.NodeKind{
model.NodeHeading, model.NodeHeading, model.NodeParagraph,
model.NodeHeading, model.NodeList, model.NodeQuote,
model.NodeCodeBlock, model.NodeCodeBlock, model.NodeHeading,
model.NodeList, model.NodeThematicBreak, model.NodeTable,
}
gotKinds := make([]model.NodeKind, len(delivery.Document.Root.Children))
for index := range delivery.Document.Root.Children {
gotKinds[index] = delivery.Document.Root.Children[index].Kind
}
if !reflect.DeepEqual(gotKinds, wantKinds) {
t.Fatalf("canonical block kinds = %#v, want %#v", gotKinds, wantKinds)
}
var quote *model.Node
var codes []*model.Node
var hasStrong, hasEmphasis, hasStrike, hasInlineCode, hasLink bool
var walkInline func([]model.Inline)
walkInline = func(inlines []model.Inline) {
for index := range inlines {
switch inlines[index].Kind {
case model.InlineStrong:
hasStrong = true
case model.InlineEmphasis:
hasEmphasis = true
case model.InlineStrike:
hasStrike = true
case model.InlineCode:
hasInlineCode = true
case model.InlineLink:
hasLink = true
}
walkInline(inlines[index].Children)
}
}
var walk func(*model.Node)
walk = func(node *model.Node) {
if node.Kind == model.NodeQuote {
quote = node
}
if node.Kind == model.NodeCodeBlock {
codes = append(codes, node)
}
walkInline(node.Inlines)
for index := range node.Children {
walk(&node.Children[index])
}
}
walk(&delivery.Document.Root)
if quote == nil || len(codes) != 2 || !hasStrong || !hasEmphasis || !hasStrike || !hasInlineCode || !hasLink {
t.Fatalf("semantic canonical document incomplete: quote=%v codes=%d strong=%v emphasis=%v strike=%v inlineCode=%v link=%v", quote != nil, len(codes), hasStrong, hasEmphasis, hasStrike, hasInlineCode, hasLink)
}
if codes[0].Text != "fmt.Println(\"backtick\")\n" || codes[1].Text != "fmt.Println(\"tilde\")\n" {
t.Fatalf("fenced code text = %q / %q", codes[0].Text, codes[1].Text)
}
unordered := delivery.Document.Root.Children[4]
orderedChild := unordered.Children[1].Children[0]
deepBullet := orderedChild.Children[0].Children[0]
if nodePropertyValue(unordered, "ordered") != "false" ||
nodePropertyValue(orderedChild, "ordered") != "true" ||
nodePropertyValue(deepBullet, "ordered") != "false" {
t.Fatalf("nested list semantics = %#v / %#v / %#v", unordered.Properties, orderedChild.Properties, deepBullet.Properties)
}
if nodePropertyValue(unordered.Children[2], "checked") != "true" || nodePropertyValue(unordered.Children[3], "checked") != "false" {
t.Fatalf("task states = %#v / %#v", unordered.Children[2].Properties, unordered.Children[3].Properties)
}
ordered := delivery.Document.Root.Children[9]
if nodePropertyValue(ordered, "ordered") != "true" || nodePropertyValue(ordered, "start") != "3" {
t.Fatalf("ordered-list start = %#v, want 3", ordered.Properties)
}
table := delivery.Document.Root.Children[11]
wantAlignments := []string{"left", "center", "right", ""}
for rowIndex, row := range table.Children {
for cellIndex, cell := range row.Children {
if got := nodePropertyValue(cell, "alignment"); got != wantAlignments[cellIndex] {
t.Fatalf("row %d cell %d alignment = %q, want %q", rowIndex, cellIndex, got, wantAlignments[cellIndex])
}
}
}
documentXML := docxPart(t, delivery.DOCX.Bytes, "word/document.xml")
for _, fragment := range []string{
`<w:b/>`, `<w:i/>`, `<w:strike/>`, `w:val="inlineCode"`, `w:val="link"`,
`w:val="quote"`, `w:val="code"`, `<w:numPr>`, `<w:tbl>`,
`<w:t xml:space="preserve">☑ </w:t>`, `<w:t xml:space="preserve">☐ </w:t>`,
`<w:pBdr><w:bottom w:val="single"`, `<w:jc w:val="left"/>`,
`<w:jc w:val="center"/>`, `<w:jc w:val="right"/>`,
} {
if !strings.Contains(documentXML, fragment) {
t.Errorf("Word output missing %s", fragment)
}
}
numberingXML := docxPart(t, delivery.DOCX.Bytes, "word/numbering.xml")
for _, fragment := range []string{
`<w:ilvl w:val="0"/>`, `<w:ilvl w:val="1"/>`, `<w:ilvl w:val="2"/>`,
`<w:lvlOverride w:ilvl="0"><w:startOverride w:val="3"/></w:lvlOverride>`,
} {
if !strings.Contains(documentXML+numberingXML, fragment) {
t.Errorf("Word numbering missing %s", fragment)
}
}
if bytes.Contains(delivery.DOCX.Bytes, []byte("https://example.com/private-target")) || strings.Contains(documentXML, "TargetMode=\"External\"") {
t.Fatal("safe link target leaked into DOCX package")
}
legacy, err := legacyBuild(request.Title, sampleMinutes)
if err != nil {
t.Fatal(err)
}
receipt := compareLegacyAndShared(request.Title, []byte(sampleMinutes), delivery.Document, legacy, delivery.DOCX.Bytes)
for _, name := range []string{"list-structure", "task-state", "thematic-break", "table-alignment"} {
if dimension := comparisonDimension(t, receipt, name); dimension.Status != "match" {
t.Errorf("complete fixture comparison dimension %s = %#v, want match", name, dimension)
}
}
}
func TestMaterializedMarkdownMustFitAndReplayAtOneBudget(t *testing.T) {
request := workspaceRequest([]byte("# T\n\nsmall body\n"))
request.Title = "T"
request.Limits.MaxMarkdownBytes = 256
if delivery, err := Generate(context.Background(), request); !errors.Is(err, ErrResourceLimit) {
t.Fatalf("materialized overflow returned delivery=%d bytes err=%v, want ErrResourceLimit", len(delivery.Markdown.Bytes), err)
}
request.Limits.MaxMarkdownBytes = 0
first, err := Generate(context.Background(), request)
if err != nil {
t.Fatal(err)
}
request.Markdown = first.Markdown.Bytes
request.Limits.MaxMarkdownBytes = len(first.Markdown.Bytes)
second, err := Generate(context.Background(), request)
if err != nil || !bytes.Equal(first.Markdown.Bytes, second.Markdown.Bytes) {
t.Fatalf("exact-budget replay failed: err=%v first=%d second=%d", err, len(first.Markdown.Bytes), len(second.Markdown.Bytes))
}
}
func TestPersistedControlledMetadataRoutesBeforeDefaultProfileLoad(t *testing.T) {
source := []byte("---\ndocumentId: controlled\nprojectId: workspace\ndocumentType: general\ntitle: Controlled\nlanguage: zh-CN\nprofileId: company.default\nprofileVersion: 1.0.0\ncontrolled: true\n---\n\n<!-- node-id:heading -->\n# Controlled\n")
request := workspaceRequest(source)
request.DocumentID = "controlled"
request.Title = "Controlled"
loads := 0
deps := defaultGeneratorDependencies()
baseLoad := deps.loadDefault
deps.loadDefault = func(ctx context.Context) (profiles.Bundle, error) {
loads++
return baseLoad(ctx)
}
_, err := generate(context.Background(), request, deps)
if !errors.Is(err, ErrRDMRequired) || loads != 0 {
t.Fatalf("persisted controlled route err=%v profileLoads=%d, want ErrRDMRequired before load", err, loads)
}
}
func TestRendererIdentityMatchesPublishedPin(t *testing.T) {
const want = "agent-document-renderer@31fbe54fae8bf11be664919ce173da0d68ebfdc9"
if RendererIdentity != want {
t.Fatalf("RendererIdentity = %q, want %q", RendererIdentity, want)
}
}
func TestRendererIdentityIsGeneratedFromCheckedOutComponent(t *testing.T) {
repository, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
command := exec.Command("go", "run", "./internal/docgen/internal/genrendereridentity", "-check")
command.Dir = repository
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("renderer identity generation check: %v\n%s", err, output)
}
}
func TestGenerateReusesPersistedMarkdownWithoutChangingNodeIdentity(t *testing.T) {
request := workspaceRequest([]byte(representativeMarkdown))
first, err := Generate(context.Background(), request)
if err != nil {
t.Fatal(err)
}
request.Markdown = first.Markdown.Bytes
second, err := Generate(context.Background(), request)
if err != nil {
t.Fatalf("Generate persisted Markdown: %v", err)
}
if !reflect.DeepEqual(first.Document, second.Document) {
t.Fatalf("canonical node identities changed\nfirst: %#v\nsecond: %#v", first.Document, second.Document)
}
if !bytes.Equal(first.Markdown.Bytes, second.Markdown.Bytes) || !bytes.Equal(first.DOCX.Bytes, second.DOCX.Bytes) {
t.Fatal("identical persisted Markdown and frozen inputs changed delivery bytes")
}
}
func TestGenerateRejectsUnknownPersistedMetadata(t *testing.T) {
request := workspaceRequest([]byte(representativeMarkdown))
first, err := Generate(context.Background(), request)
if err != nil {
t.Fatal(err)
}
request.Markdown = bytes.Replace(first.Markdown.Bytes, []byte("controlled: false\n"), []byte("controlled: false\nscript: build.sh\n"), 1)
_, err = Generate(context.Background(), request)
var diagnostic *DiagnosticError
if !errors.As(err, &diagnostic) || diagnostic.Stage != "parse" || diagnostic.Code != "unknown_metadata" {
t.Fatalf("unknown metadata error = %#v (%v)", diagnostic, err)
}
}
func TestGenerateRejectsControlledAndProjectRoutesBeforeDefaultFallback(t *testing.T) {
for _, mutate := range []func(*Request){
func(request *Request) { request.Controlled = true },
func(request *Request) { request.ProjectDocument = true },
} {
request := workspaceRequest([]byte(representativeMarkdown))
mutate(&request)
if _, err := Generate(context.Background(), request); !errors.Is(err, ErrRDMRequired) {
t.Fatalf("Generate route error = %v, want ErrRDMRequired", err)
}
}
}
func TestGenerateRoutesReviewAndApprovalModesToRDM(t *testing.T) {
for _, mode := range []render.Mode{render.ModeReview, render.ModeApproved} {
request := workspaceRequest([]byte(representativeMarkdown))
request.Mode = mode
if _, err := Generate(context.Background(), request); !errors.Is(err, ErrRDMRequired) {
t.Errorf("mode %s error = %v, want ErrRDMRequired", mode, err)
}
}
}
func TestGenerateRejectsProfileAndAssetMismatch(t *testing.T) {
request := workspaceRequest([]byte(representativeMarkdown))
request.ProfileID = "company.other"
request.ProfileVersion = "9.9.9"
if _, err := Generate(context.Background(), request); !errors.Is(err, ErrProfileMismatch) {
t.Fatalf("profile mismatch error = %v, want ErrProfileMismatch", err)
}
request = workspaceRequest([]byte(strings.ReplaceAll(representativeMarkdown,
"asset:company.default.sample-image@1.0.0", "asset:undeclared.image@1.0.0")))
if _, err := Generate(context.Background(), request); err == nil || !strings.Contains(err.Error(), "undeclared asset") {
t.Fatalf("asset mismatch error = %v, want an honest undeclared-asset diagnostic", err)
}
}
func TestGenerateEnforcesLimitsAndCancellation(t *testing.T) {
request := workspaceRequest([]byte(representativeMarkdown))
request.Limits.MaxMarkdownBytes = 32
if _, err := Generate(context.Background(), request); !errors.Is(err, ErrResourceLimit) {
t.Fatalf("limit error = %v, want ErrResourceLimit", err)
}
canceled, cancel := context.WithCancel(context.Background())
cancel()
if _, err := Generate(canceled, workspaceRequest([]byte(representativeMarkdown))); !errors.Is(err, context.Canceled) {
t.Fatalf("cancellation error = %v, want context.Canceled", err)
}
}
func TestGenerateReturnsTypedDiagnosticsForUnsafeMarkdownAndAssets(t *testing.T) {
tests := []struct {
name string
markdown string
stage string
code string
}{
{"script", "# Report\n\n<script>alert(1)</script>\n", "parse", "unsafe_markdown"},
{"external asset", "# Report\n\n![remote](https://example.com/a.png)\n", "parse", "unsafe_markdown"},
{"unknown directive", "# Report\n\n:::execute script=build.sh\n", "parse", "unknown_directive"},
{"undeclared asset", "# Report\n\n![missing](asset:missing@1.0.0)\n", "render", "asset_mismatch"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
delivery, err := Generate(context.Background(), workspaceRequest([]byte(test.markdown)))
if err == nil {
t.Fatal("Generate succeeded for unsafe input")
}
var diagnostic *DiagnosticError
if !errors.As(err, &diagnostic) {
t.Fatalf("error %T %v is not a DiagnosticError", err, err)
}
if diagnostic.Stage != test.stage || diagnostic.Code != test.code || strings.TrimSpace(diagnostic.Error()) == "" {
t.Fatalf("diagnostic = %#v, want stage=%s code=%s", diagnostic, test.stage, test.code)
}
if len(delivery.Markdown.Bytes)+len(delivery.DOCX.Bytes)+len(delivery.Manifest.Bytes)+len(delivery.Archive.Bytes) != 0 {
t.Fatal("failed render returned partial delivery bytes")
}
})
}
}
type failingRenderer struct{ err error }
func (renderer failingRenderer) Validate(context.Context, render.Request) ([]render.Diagnostic, error) {
return []render.Diagnostic{{Code: "forced_failure", Severity: render.SeverityError, Message: renderer.err.Error()}}, renderer.err
}
func (renderer failingRenderer) Render(context.Context, render.Request) (render.Result, error) {
return render.Result{Diagnostics: []render.Diagnostic{{Code: "forced_failure", Severity: render.SeverityError, Message: renderer.err.Error()}}}, renderer.err
}
type cleanupRenderer struct {
active *atomic.Int32
started chan struct{}
canceled chan struct{}
release chan struct{}
}
func (renderer cleanupRenderer) Validate(context.Context, render.Request) ([]render.Diagnostic, error) {
return nil, nil
}
func (renderer cleanupRenderer) Render(ctx context.Context, _ render.Request) (render.Result, error) {
renderer.active.Add(1)
defer renderer.active.Add(-1)
close(renderer.started)
<-ctx.Done()
close(renderer.canceled)
<-renderer.release
return render.Result{}, ctx.Err()
}
func TestCancellationWaitsForRendererUnwindAndRecoversCapacity(t *testing.T) {
var active atomic.Int32
started := make(chan struct{})
canceled := make(chan struct{})
release := make(chan struct{})
deps := defaultGeneratorDependencies()
deps.newRenderer = func(*profile.AssetReader) render.Renderer {
return cleanupRenderer{active: &active, started: started, canceled: canceled, release: release}
}
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, err := generate(ctx, workspaceRequest([]byte(representativeMarkdown)), deps)
result <- err
}()
<-started
cancel()
<-canceled
select {
case err := <-result:
t.Fatalf("Generate returned before renderer cleanup: %v", err)
default:
}
close(release)
select {
case err := <-result:
if !errors.Is(err, context.Canceled) || active.Load() != 0 {
t.Fatalf("cancellation err=%v active=%d, want canceled and zero capacity", err, active.Load())
}
case <-time.After(time.Second):
t.Fatal("Generate did not return after renderer cleanup")
}
}
func TestRendererFailureNeverFallsBackToLegacy(t *testing.T) {
forced := errors.New("forced renderer failure")
legacyCalls := 0
deps := defaultGeneratorDependencies()
deps.newRenderer = func(*profile.AssetReader) render.Renderer { return failingRenderer{err: forced} }
deps.legacyBuild = func(string, string) ([]byte, error) {
legacyCalls++
return []byte("legacy"), nil
}
request := workspaceRequest([]byte(representativeMarkdown))
if _, err := generateWithOptions(context.Background(), request, RenderOptions{MigrationComparison: true}, deps); !errors.Is(err, forced) {
t.Fatalf("generate error = %v, want forced renderer failure", err)
}
if legacyCalls != 0 {
t.Fatalf("legacy renderer called %d times after shared renderer failure", legacyCalls)
}
}
func TestLegacyComparisonIsExplicitAndDoesNotReplaceSharedOutput(t *testing.T) {
legacyCalls := 0
deps := defaultGeneratorDependencies()
deps.legacyBuild = func(string, string) ([]byte, error) {
legacyCalls++
return []byte("legacy-comparison-only"), nil
}
request := workspaceRequest([]byte(representativeMarkdown))
withoutFlag, err := generate(context.Background(), request, deps)
if err != nil {
t.Fatal(err)
}
if legacyCalls != 0 || withoutFlag.Comparison.Enabled {
t.Fatalf("disabled comparison called legacy renderer: calls=%d receipt=%#v", legacyCalls, withoutFlag.Comparison)
}
withFlag, err := generateWithOptions(context.Background(), request, RenderOptions{MigrationComparison: true}, deps)
if err != nil {
t.Fatal(err)
}
if legacyCalls != 1 || !withFlag.Comparison.Enabled || withFlag.Comparison.LegacySHA256 == "" || withFlag.Comparison.Assessment != "unassessed" || len(withFlag.Comparison.Unassessed) == 0 {
t.Fatalf("enabled comparison receipt=%#v calls=%d", withFlag.Comparison, legacyCalls)
}
if !bytes.Equal(withoutFlag.DOCX.Bytes, withFlag.DOCX.Bytes) {
t.Fatal("legacy comparison replaced the shared-renderer production output")
}
}
func TestRepresentativeLegacyComparisonRecordsOnlyIntentionalDifferences(t *testing.T) {
const common = "# 研发周会纪要\n\n正文有 **粗体**、*斜体*、~~删除~~、`代码` 和 [链接](https://example.com/private)。\n\n> 引用内容。\n\n```go\nfmt.Println(1)\n```\n\n## 要点\n\n- [x] 已完成\n- [ ] 待办\n\n1. 第一项\n2. 第二项\n\n---\n\n| 左 | 中 | 右 |\n| :--- | :---: | ---: |\n| L | C | R |\n"
request := workspaceRequest([]byte(common))
delivery, err := generateWithOptions(context.Background(), request, RenderOptions{MigrationComparison: true}, defaultGeneratorDependencies())
if err != nil {
t.Fatal(err)
}
if delivery.Comparison.Assessment != "assessed" || len(delivery.Comparison.AccidentalDifferences) != 0 || len(delivery.Comparison.Unassessed) != 0 {
t.Fatalf("accidental differences remain: %#v", delivery.Comparison.AccidentalDifferences)
}
exceptions := map[string]bool{}
for _, dimension := range delivery.Comparison.Dimensions {
if dimension.Status == "intentional" {
exceptions[dimension.ExceptionID] = true
}
}
for _, id := range []string{"RDR-T10-CMP-001", "RDR-T10-CMP-002", "RDR-T10-CMP-003", "RDR-T10-CMP-004"} {
if !exceptions[id] {
t.Errorf("comparison omitted reviewed exception %s: %#v", id, delivery.Comparison.Dimensions)
}
}
for _, name := range []string{
"list-structure", "task-state", "thematic-break", "table-alignment",
"word-numbering", "word-task-state", "word-thematic-rule", "word-table-alignment",
} {
dimension := comparisonDimension(t, delivery.Comparison, name)
if dimension.Status != "match" {
t.Errorf("comparison dimension %s = %#v, want match", name, dimension)
}
}
for _, dimension := range delivery.Comparison.Dimensions {
if dimension.ExceptionID == comparisonExceptionCorporate && !strings.Contains(dimension.Detail, "published organization default") {
t.Errorf("corporate layout exception %s detail = %q, want published organization default", dimension.Name, dimension.Detail)
}
}
}
func TestComparisonDetectsCanonicalAndDOCXTableAlignmentLoss(t *testing.T) {
const aligned = "# Alignment\n\n| Left | Center | Right | Default |\n| :--- | :---: | ---: | --- |\n| L | C | R | D |\n"
request := workspaceRequest([]byte(aligned))
request.Title = "Alignment"
delivery, err := Generate(context.Background(), request)
if err != nil {
t.Fatal(err)
}
legacy, err := legacyBuild(request.Title, aligned)
if err != nil {
t.Fatal(err)
}
canonicalWithoutAlignment := delivery.Document.Clone()
for rowIndex := range canonicalWithoutAlignment.Root.Children[1].Children {
for cellIndex := range canonicalWithoutAlignment.Root.Children[1].Children[rowIndex].Children {
canonicalWithoutAlignment.Root.Children[1].Children[rowIndex].Children[cellIndex].Properties = nil
}
}
canonicalLoss := compareLegacyAndShared(request.Title, []byte(aligned), canonicalWithoutAlignment, legacy, delivery.DOCX.Bytes)
if dimension := comparisonDimension(t, canonicalLoss, "table-alignment"); dimension.Status != "accidental" {
t.Fatalf("canonical table-alignment loss = %#v, want accidental", dimension)
}
docxWithoutAlignment := rewriteDOCXPart(t, delivery.DOCX.Bytes, "word/document.xml", func(content []byte) []byte {
for _, value := range []string{"left", "center", "right"} {
content = bytes.ReplaceAll(content, []byte(`<w:jc w:val="`+value+`"/>`), nil)
}
return content
})
docxLoss := compareLegacyAndShared(request.Title, []byte(aligned), delivery.Document, legacy, docxWithoutAlignment)
if docxLoss.Assessment != "differences" {
t.Fatalf("DOCX alignment loss assessment = %q, want differences", docxLoss.Assessment)
}
if dimension := comparisonDimension(t, docxLoss, "word-table-alignment"); dimension.Status != "accidental" {
t.Fatalf("DOCX table-alignment loss = %#v, want accidental", dimension)
}
}
func TestLegacyComparisonReportsAccidentalAndUnassessedDimensions(t *testing.T) {
request := workspaceRequest([]byte("# Expected\n\nExpected body.\n"))
deps := defaultGeneratorDependencies()
deps.legacyBuild = func(title, _ string) ([]byte, error) { return legacyBuild(title, "# Wrong\n\nLost body.\n") }
accidental, err := generateWithOptions(context.Background(), request, RenderOptions{MigrationComparison: true}, deps)
if err != nil {
t.Fatal(err)
}
if accidental.Comparison.Assessment != "differences" || len(accidental.Comparison.AccidentalDifferences) == 0 {
t.Fatalf("content loss was not accidental: %#v", accidental.Comparison)
}
deps.legacyBuild = func(string, string) ([]byte, error) { return []byte("not a DOCX"), nil }
unassessed, err := generateWithOptions(context.Background(), request, RenderOptions{MigrationComparison: true}, deps)
if err != nil {
t.Fatal(err)
}
if unassessed.Comparison.Assessment != "unassessed" || len(unassessed.Comparison.Unassessed) == 0 {
t.Fatalf("invalid legacy sample was reported as assessed: %#v", unassessed.Comparison)
}
}
func docxVisibleText(t *testing.T, raw []byte) string {
t.Helper()
zr, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
if err != nil {
t.Fatal(err)
}
for _, file := range zr.File {
if file.Name != "word/document.xml" {
continue
}
stream, err := file.Open()
if err != nil {
t.Fatal(err)
}
decoder := xml.NewDecoder(stream)
var text strings.Builder
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
stream.Close()
t.Fatal(err)
}
if chars, ok := token.(xml.CharData); ok {
text.Write(chars)
text.WriteByte('\n')
}
}
stream.Close()
return text.String()
}
t.Fatal("word/document.xml missing")
return ""
}
func docxPart(t *testing.T, raw []byte, name string) string {
t.Helper()
zr, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
if err != nil {
t.Fatal(err)
}
for _, file := range zr.File {
if file.Name != name {
continue
}
stream, err := file.Open()
if err != nil {
t.Fatal(err)
}
content, err := io.ReadAll(stream)
stream.Close()
if err != nil {
t.Fatal(err)
}
return string(content)
}
t.Fatalf("DOCX part %s missing", name)
return ""
}
func comparisonDimension(t *testing.T, receipt ComparisonReceipt, name string) ComparisonDimension {
t.Helper()
for _, dimension := range receipt.Dimensions {
if dimension.Name == name {
return dimension
}
}
t.Fatalf("comparison dimension %s missing: %#v", name, receipt.Dimensions)
return ComparisonDimension{}
}
func rewriteDOCXPart(t *testing.T, raw []byte, name string, rewrite func([]byte) []byte) []byte {
t.Helper()
reader, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
writer := zip.NewWriter(&output)
for _, file := range reader.File {
stream, err := file.Open()
if err != nil {
t.Fatal(err)
}
content, err := io.ReadAll(stream)
stream.Close()
if err != nil {
t.Fatal(err)
}
if file.Name == name {
content = rewrite(content)
}
target, err := writer.Create(file.Name)
if err != nil {
t.Fatal(err)
}
if _, err := target.Write(content); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
return output.Bytes()
}