Files
weichen 2da3d76ce1
governance / governance (pull_request) Successful in 25s
ci / vet-and-test (pull_request) Successful in 2m23s
Move the published renderer pin to the migrated renderer main
Why: the document-renderer pin moved to its migrated main for the
endpoint migration; identity regenerated and the published-pin
expectation updated to match.

Evidence: go test ./internal/docgen → ok (local, host-verified)

Impact: generated identity + test constant; part of the coordinated
endpoint migration.

Refs: Agent-System-Dev PR #706
2026-10-22 15:34:06 +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@40e5c671d1ef786ceba62bf784bf3aa53c5da26d"
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()
}