Files
Agent-System-Backend/internal/docgen/renderer_integration_test.go
T
weichenandCursor c34279ad55
governance / governance (pull_request) Successful in 16s
ci / vet-and-test (pull_request) Successful in 1m2s
Bump the renderer to 3f25999 and bind documents to published profile versions
Why: Agent-Document-Renderer main publishes company.default@1.1.0 with a
declarative TOC (spec 04, acceptance 10 §4 §7) and keeps 1.0.0 loadable by
version. The fixed defaultProfileVersion "1.0.0" no longer matched
LoadCompanyDefaultContext (1.1.0), so every delivery failed with a profile
mismatch. New documents must take the latest published version, while
documents that already bind a published version keep rendering with exactly
that version, because published rules are immutable and old versions remain
retrievable (spec 08).

Evidence: go vet ./... ok; go test -count=1 ./... ok (all packages);
go run ./internal/docgen/internal/genrendereridentity -check ok;
scripts/agent-exec validate --repo . ok; scripts/dm governance check --repo .
ok; gofmt -l clean; git diff --cached --check clean. New tests cover the
default binding (1.1.0, TOC present), pinned 1.0.0 (no TOC) and 1.1.0 for
both a request pin and a persisted-Markdown replay, and refusal of unknown
versions and other profile IDs before any profile load.

Impact: docgen resolves the profile version from the persisted Markdown
binding or the request, defaults new documents to
profiles.LatestCompanyDefaultVersion, and loads it through
LoadCompanyDefaultVersionContext. The legacy migration comparison records the
declared TOC as an intentional corporate-layout difference
(RDR-T10-CMP-002) and keeps it out of the body visible-text dimension.
RendererIdentity is regenerated for 3f259990b5d4.

Refs: unified renderer plan Task 13; renderer PR #7
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 06:24:36 +00:00

1076 lines
42 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 != profiles.LatestCompanyDefaultVersion || 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.loadProfile
deps.loadProfile = func(ctx context.Context, version string) (profiles.Bundle, error) {
loads++
return baseLoad(ctx, version)
}
_, 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@3f259990b5d43ec0d318f39d496816140ab559b5"
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)
}
}
const tocFieldInstruction = `TOC \o "1-`
func TestGenerateBindsNewDocumentsToLatestPublishedProfile(t *testing.T) {
request := workspaceRequest([]byte(representativeMarkdown))
prepared, err := Prepare(context.Background(), request)
if err != nil {
t.Fatalf("Prepare: %v", err)
}
delivery, err := Generate(context.Background(), request)
if err != nil {
t.Fatalf("Generate: %v", err)
}
latest, err := profiles.LoadCompanyDefault()
if err != nil {
t.Fatal(err)
}
if _, version := latest.Snapshot.ProfileIdentity(); version != profiles.LatestCompanyDefaultVersion {
t.Fatalf("renderer latest company default = %s, want %s", version, profiles.LatestCompanyDefaultVersion)
}
for name, markdown := range map[string][]byte{"Prepare": prepared.Markdown.Bytes, "Generate": delivery.Markdown.Bytes} {
metadata := persistedMetadata(t, markdown)
if metadata.ProfileID != "company.default" || metadata.ProfileVersion != profiles.LatestCompanyDefaultVersion {
t.Fatalf("%s bound new document to %s@%s, want company.default@%s", name, metadata.ProfileID, metadata.ProfileVersion, profiles.LatestCompanyDefaultVersion)
}
}
if delivery.Profile.Version != profiles.LatestCompanyDefaultVersion || delivery.Profile.SnapshotHash != latest.Snapshot.SnapshotHash() {
t.Fatalf("delivery profile receipt = %#v, want latest snapshot %s", delivery.Profile, latest.Snapshot.SnapshotHash())
}
documentXML := docxPart(t, delivery.DOCX.Bytes, "word/document.xml")
if !strings.Contains(documentXML, tocFieldInstruction) || !strings.Contains(docxVisibleText(t, delivery.DOCX.Bytes), "目录") {
t.Fatal("new document rendered with the latest company default omitted its declared TOC")
}
}
func TestGenerateKeepsPinnedPublishedProfileVersion(t *testing.T) {
for _, test := range []struct {
version string
toc bool
}{
{version: "1.0.0", toc: false},
{version: "1.1.0", toc: true},
} {
t.Run(test.version, func(t *testing.T) {
pinned, err := profiles.LoadCompanyDefaultVersion(test.version)
if err != nil {
t.Fatal(err)
}
request := workspaceRequest([]byte(representativeMarkdown))
request.ProfileID, request.ProfileVersion = "company.default", test.version
prepared, err := Prepare(context.Background(), request)
if err != nil {
t.Fatalf("Prepare pinned %s: %v", test.version, err)
}
if metadata := persistedMetadata(t, prepared.Markdown.Bytes); metadata.ProfileVersion != test.version {
t.Fatalf("Prepare bound pinned request to %s, want %s", metadata.ProfileVersion, test.version)
}
first, err := Generate(context.Background(), request)
if err != nil {
t.Fatalf("Generate pinned %s: %v", test.version, err)
}
replay := workspaceRequest(first.Markdown.Bytes)
second, err := Generate(context.Background(), replay)
if err != nil {
t.Fatalf("Generate persisted %s Markdown: %v", test.version, err)
}
for name, delivery := range map[string]Delivery{"pinned request": first, "persisted replay": second} {
if delivery.Profile.ProfileID != "company.default" || delivery.Profile.Version != test.version ||
delivery.Profile.SnapshotHash != pinned.Snapshot.SnapshotHash() || delivery.Profile.ProfileSHA256 != pinned.Review.ProfileSHA256 {
t.Fatalf("%s profile receipt = %#v, want company.default@%s snapshot %s", name, delivery.Profile, test.version, pinned.Snapshot.SnapshotHash())
}
if metadata := persistedMetadata(t, delivery.Markdown.Bytes); metadata.ProfileVersion != test.version {
t.Fatalf("%s Markdown binds %s, want %s", name, metadata.ProfileVersion, test.version)
}
if got := strings.Contains(docxPart(t, delivery.DOCX.Bytes, "word/document.xml"), tocFieldInstruction); got != test.toc {
t.Fatalf("%s TOC present = %v, want %v for company.default@%s", name, got, test.toc, test.version)
}
}
if !bytes.Equal(first.DOCX.Bytes, second.DOCX.Bytes) {
t.Fatal("replaying persisted Markdown changed the pinned delivery bytes")
}
})
}
}
func TestProfileBindingRefusesUnpublishedVersionsBeforeLoad(t *testing.T) {
persisted, err := Generate(context.Background(), workspaceRequest([]byte(representativeMarkdown)))
if err != nil {
t.Fatal(err)
}
latestBinding := []byte("profileVersion: \"" + profiles.LatestCompanyDefaultVersion + "\"\n")
if !bytes.Contains(persisted.Markdown.Bytes, latestBinding) {
t.Fatalf("persisted Markdown does not carry %q", latestBinding)
}
pinnedOld := workspaceRequest(bytes.Replace(persisted.Markdown.Bytes, latestBinding, []byte("profileVersion: \"1.0.0\"\n"), 1))
pinnedOld.ProfileID, pinnedOld.ProfileVersion = "company.default", profiles.LatestCompanyDefaultVersion
tests := map[string]Request{}
for name, identity := range map[string][2]string{
"unknown version": {"company.default", "9.9.9"},
"other profile": {"company.other", "1.0.0"},
"missing version": {"company.default", ""},
"missing profile id": {"", "1.0.0"},
} {
request := workspaceRequest([]byte(representativeMarkdown))
request.ProfileID, request.ProfileVersion = identity[0], identity[1]
tests["requested "+name] = request
}
tests["persisted unknown version"] = workspaceRequest(bytes.Replace(persisted.Markdown.Bytes, latestBinding, []byte("profileVersion: \"9.9.9\"\n"), 1))
tests["persisted other profile"] = workspaceRequest(bytes.Replace(persisted.Markdown.Bytes,
[]byte("profileId: \"company.default\"\n"), []byte("profileId: \"company.other\"\n"), 1))
tests["request disagrees with persisted binding"] = pinnedOld
for name, request := range tests {
t.Run(name, func(t *testing.T) {
if _, err := Prepare(context.Background(), request); !errors.Is(err, ErrProfileMismatch) {
t.Fatalf("Prepare error = %v, want ErrProfileMismatch", err)
}
loads := 0
deps := defaultGeneratorDependencies()
baseLoad := deps.loadProfile
deps.loadProfile = func(ctx context.Context, version string) (profiles.Bundle, error) {
loads++
return baseLoad(ctx, version)
}
if _, err := generate(context.Background(), request, deps); !errors.Is(err, ErrProfileMismatch) || loads != 0 {
t.Fatalf("Generate error = %v profileLoads=%d, want ErrProfileMismatch before load", err, loads)
}
})
}
}
func TestGenerateRefusesLoadedProfileWithUnexpectedIdentity(t *testing.T) {
deps := defaultGeneratorDependencies()
deps.loadProfile = func(ctx context.Context, _ string) (profiles.Bundle, error) {
return profiles.LoadCompanyDefaultVersionContext(ctx, "1.0.0")
}
if _, err := generate(context.Background(), workspaceRequest([]byte(representativeMarkdown)), deps); !errors.Is(err, ErrProfileMismatch) {
t.Fatalf("Generate with a loader returning the wrong version error = %v, want ErrProfileMismatch", err)
}
}
func persistedMetadata(t *testing.T, markdown []byte) markdownrenderer.Metadata {
t.Helper()
parsed, err := markdownrenderer.Parse(markdown, markdownrenderer.Options{MaxBytes: 2 << 20, MaxNodes: 10_000, MaxDepth: 64})
if err != nil {
t.Fatalf("persisted Markdown is not parseable: %v", err)
}
return parsed.Metadata
}
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")
}
})
}
}
// TestRendererNodeDiagnosticsPassThroughUnchanged pins the wire contract for
// node-located content diagnostics: the renderer's codes, severities, NodeID
// and RulePath reach the manifest and the typed failure without rewriting.
func TestRendererNodeDiagnosticsPassThroughUnchanged(t *testing.T) {
warnings := []render.Diagnostic{
{Code: "required_content_missing", Severity: render.SeverityWarning, NodeID: "node-7", RulePath: "mappings[2]", Message: "required node node-7 is empty", Suggestion: "add content"},
{Code: "intentionally_empty", Severity: render.SeverityInfo, NodeID: "node-8", RulePath: "mappings[2]", Message: "node node-8 is intentionally empty"},
{Code: "mapping_fallback", Severity: render.SeverityWarning, NodeID: "node-9", Message: "style not defined", Suggestion: "define the style"},
}
deps := defaultGeneratorDependencies()
deps.newRenderer = func(*profile.AssetReader) render.Renderer {
return diagnosticsRenderer{diagnostics: warnings}
}
delivery, err := generate(context.Background(), workspaceRequest([]byte(representativeMarkdown)), deps)
if err != nil {
t.Fatalf("generate with warnings: %v", err)
}
var manifest Manifest
if err := json.Unmarshal(delivery.Manifest.Bytes, &manifest); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(manifest.Diagnostics, warnings) {
t.Fatalf("manifest diagnostics = %#v, want renderer diagnostics %#v", manifest.Diagnostics, warnings)
}
for _, fragment := range []string{`"nodeId": "node-7"`, `"rulePath": "mappings[2]"`, `"code": "required_content_missing"`} {
if !strings.Contains(string(delivery.Manifest.Bytes), fragment) {
t.Errorf("manifest JSON omits %s", fragment)
}
}
refusal := []render.Diagnostic{{Code: "mapping_unresolved", Severity: render.SeverityError, NodeID: "node-3", Message: "node node-3 cannot be mapped"}}
deps.newRenderer = func(*profile.AssetReader) render.Renderer {
return diagnosticsRenderer{diagnostics: refusal, err: errors.New("word: mapping unresolved for node node-3")}
}
_, err = generate(context.Background(), workspaceRequest([]byte(representativeMarkdown)), deps)
var diagnostic *DiagnosticError
if !errors.As(err, &diagnostic) {
t.Fatalf("refusal error %T %v is not a DiagnosticError", err, err)
}
if !reflect.DeepEqual(diagnostic.Diagnostics, refusal) {
t.Fatalf("refusal diagnostics = %#v, want %#v", diagnostic.Diagnostics, refusal)
}
}
// diagnosticsRenderer returns fixed renderer diagnostics without a template, so
// the Backend's carriage of the renderer's diagnostics is tested on its own.
type diagnosticsRenderer struct {
diagnostics []render.Diagnostic
err error
}
func (renderer diagnosticsRenderer) Validate(context.Context, render.Request) ([]render.Diagnostic, error) {
return renderer.diagnostics, renderer.err
}
func (renderer diagnosticsRenderer) Render(context.Context, render.Request) (render.Result, error) {
if renderer.err != nil {
return render.Result{Diagnostics: renderer.diagnostics}, renderer.err
}
return render.Result{
Artifact: []byte("docx"), MIME: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
Diagnostics: renderer.diagnostics,
}, nil
}
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)
}
}
if dimension := comparisonDimension(t, delivery.Comparison, "table-of-contents"); dimension.Status != "intentional" || dimension.ExceptionID != comparisonExceptionCorporate {
t.Errorf("declared TOC comparison = %#v, want intentional %s", dimension, comparisonExceptionCorporate)
}
if dimension := comparisonDimension(t, delivery.Comparison, "visible-text"); dimension.Status == "accidental" {
t.Errorf("declared TOC leaked into the body visible-text comparison: %#v", dimension)
}
request.ProfileID, request.ProfileVersion = "company.default", "1.0.0"
pinned, err := generateWithOptions(context.Background(), request, RenderOptions{MigrationComparison: true}, defaultGeneratorDependencies())
if err != nil {
t.Fatal(err)
}
if pinned.Comparison.Assessment != "assessed" || len(pinned.Comparison.AccidentalDifferences) != 0 {
t.Fatalf("pinned 1.0.0 comparison has accidental differences: %#v", pinned.Comparison.AccidentalDifferences)
}
if dimension := comparisonDimension(t, pinned.Comparison, "table-of-contents"); dimension.Status != "match" {
t.Errorf("pinned 1.0.0 TOC comparison = %#v, want match without a TOC", dimension)
}
}
func TestComparisonKeepsBodyTextLossAccidentalBesideDeclaredTOC(t *testing.T) {
const source = "# Report\n\n## Scope\n\nScope body.\n\n## Result\n\nResult body.\n"
request := workspaceRequest([]byte(source))
request.Title = "Report"
delivery, err := Generate(context.Background(), request)
if err != nil {
t.Fatal(err)
}
legacy, err := legacyBuild(request.Title, "# Report\n\n## Scope\n\nScope body.\n\n## Result\n\nDifferent body.\n")
if err != nil {
t.Fatal(err)
}
receipt := compareLegacyAndShared(request.Title, []byte(source), delivery.Document, legacy, delivery.DOCX.Bytes)
if dimension := comparisonDimension(t, receipt, "table-of-contents"); dimension.Status != "intentional" {
t.Fatalf("declared TOC comparison = %#v, want intentional", dimension)
}
if dimension := comparisonDimension(t, receipt, "visible-text"); dimension.Status != "accidental" {
t.Fatalf("body text loss beside a TOC = %#v, want accidental", dimension)
}
}
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()
}