diff --git a/deps/agent-document-renderer b/deps/agent-document-renderer index 1095077..3f25999 160000 --- a/deps/agent-document-renderer +++ b/deps/agent-document-renderer @@ -1 +1 @@ -Subproject commit 10950774736b64b5d8e544ce03d45ae5a6ceca7c +Subproject commit 3f259990b5d43ec0d318f39d496816140ab559b5 diff --git a/internal/docgen/comparison.go b/internal/docgen/comparison.go index 3996cdb..512b0b6 100644 --- a/internal/docgen/comparison.go +++ b/internal/docgen/comparison.go @@ -62,6 +62,7 @@ type docxNumbering struct { type docxComparison struct { Paragraphs []string `json:"paragraphs"` + TOC []string `json:"toc"` PageSize map[string]string `json:"pageSize"` Margins map[string]string `json:"margins"` Headers int `json:"headers"` @@ -95,7 +96,7 @@ func compareLegacyAndShared(title string, source []byte, canonical model.Canonic if legacyErr != nil || sharedErr != nil { detail := fmt.Sprintf("legacy=%v shared=%v", legacyErr, sharedErr) for _, name := range []string{ - "visible-text", "page-layout", "header-footer", "node-bookmarks", "safe-links", "security-objects", "word-semantics", + "visible-text", "table-of-contents", "page-layout", "header-footer", "node-bookmarks", "safe-links", "security-objects", "word-semantics", "word-numbering", "word-task-state", "word-thematic-rule", "word-table-alignment", } { receipt.Dimensions = append(receipt.Dimensions, ComparisonDimension{Name: name, Status: "unassessed", Detail: detail}) @@ -115,6 +116,12 @@ func compareLegacyAndShared(title string, source []byte, canonical model.Canonic } else { receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("visible-text", legacyParagraphs, sharedDOCX.Paragraphs, "")) } + if len(legacyDOCX.TOC) == 0 && len(sharedDOCX.TOC) > 0 { + receipt.Dimensions = append(receipt.Dimensions, intentionalDimension("table-of-contents", legacyDOCX.TOC, sharedDOCX.TOC, + comparisonExceptionCorporate, "published organization default declares a table of contents")) + } else { + receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("table-of-contents", legacyDOCX.TOC, sharedDOCX.TOC, "")) + } if equalJSON(legacyDOCX.PageSize, sharedDOCX.PageSize) { receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("page-size", legacyDOCX.PageSize, sharedDOCX.PageSize, "")) @@ -501,6 +508,11 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { inParagraph, inText, inCell, inParagraphBorder := false, false, false, false paragraphLevel, paragraphNumID, cellAlignment := 0, "", "" paragraphRule := false + // A TOC is its field's paragraphs plus an immediately preceding caption + // paragraph excluded from the outline (outlineLvl 9). + inInstruction, paragraphTOC, paragraphCaption := false, false, false + fieldDepth, tocFieldDepth := 0, 0 + var pendingCaption []string var table [][]string var row []string for { @@ -518,10 +530,27 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { inParagraph = true paragraph.Reset() paragraphLevel, paragraphNumID, paragraphRule = 0, "", false + paragraphTOC, paragraphCaption = tocFieldDepth > 0, false case "t": if inParagraph { inText = true } + case "outlineLvl": + if inParagraph && xmlAttributeValue(value.Attr, "val") == "9" { + paragraphCaption = true + } + case "fldChar": + switch xmlAttributeValue(value.Attr, "fldCharType") { + case "begin": + fieldDepth++ + case "end": + if fieldDepth == tocFieldDepth { + tocFieldDepth = 0 + } + fieldDepth = max(fieldDepth-1, 0) + } + case "instrText": + inInstruction = true case "numId": if inParagraph { paragraphNumID = xmlAttributeValue(value.Attr, "val") @@ -559,15 +588,35 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { if inText { paragraph.Write(value) } + if inInstruction && tocFieldDepth == 0 && fieldDepth > 0 { + if fields := strings.Fields(string(value)); len(fields) > 0 && strings.EqualFold(fields[0], "TOC") { + tocFieldDepth, paragraphTOC = fieldDepth, inParagraph + } + } case xml.EndElement: switch value.Name.Local { case "t": inText = false + case "instrText": + inInstruction = false case "pBdr": inParagraphBorder = false case "p": text := normalizeComparisonText(paragraph.String()) - if text != "" { + if paragraphTOC { + result.TOC = append(result.TOC, pendingCaption...) + pendingCaption = nil + if text != "" { + result.TOC = append(result.TOC, text) + } + inParagraph = false + break + } + result.Paragraphs = append(result.Paragraphs, pendingCaption...) + pendingCaption = nil + if text != "" && paragraphCaption { + pendingCaption = []string{text} + } else if text != "" { result.Paragraphs = append(result.Paragraphs, text) } if paragraphNumID != "" { @@ -596,6 +645,7 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { } } } + result.Paragraphs = append(result.Paragraphs, pendingCaption...) if len(numbered) > 0 { numbering, found := parts["word/numbering.xml"] if !found { diff --git a/internal/docgen/renderer.go b/internal/docgen/renderer.go index 5ba8fb4..f60a435 100644 --- a/internal/docgen/renderer.go +++ b/internal/docgen/renderer.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strings" "time" @@ -26,7 +27,6 @@ const ( ManifestSchemaVersion = "backend-document-delivery/v1" AdapterIdentity = RendererIdentity + "/word-adapter-v1" defaultProfileID = "company.default" - defaultProfileVersion = "1.0.0" defaultRenderCapacity = 4 ) @@ -194,18 +194,15 @@ func Prepare(ctx context.Context, request Request) (PreparedDocument, error) { if err := validateWorkspaceRoute(request, parsed.Metadata); err != nil { return PreparedDocument{}, err } - if parsed.Metadata.ProfileID != defaultProfileID || parsed.Metadata.ProfileVersion != defaultProfileVersion { - return PreparedDocument{}, fmt.Errorf("%w: Markdown binds %s@%s, expected %s@%s", ErrProfileMismatch, - parsed.Metadata.ProfileID, parsed.Metadata.ProfileVersion, defaultProfileID, defaultProfileVersion) + if _, err := profileVersionFor(request, &parsed.Metadata); err != nil { + return PreparedDocument{}, err } } else { - if request.ProfileID != "" || request.ProfileVersion != "" { - if request.ProfileID != defaultProfileID || request.ProfileVersion != defaultProfileVersion { - return PreparedDocument{}, fmt.Errorf("%w: requested %s@%s, expected %s@%s", ErrProfileMismatch, - request.ProfileID, request.ProfileVersion, defaultProfileID, defaultProfileVersion) - } + profileVersion, err := profileVersionFor(request, nil) + if err != nil { + return PreparedDocument{}, err } - source, err := workspaceMarkdown(request, defaultProfileID, defaultProfileVersion) + source, err := workspaceMarkdown(request, defaultProfileID, profileVersion) if err != nil { return PreparedDocument{}, err } @@ -231,14 +228,14 @@ func Prepare(ctx context.Context, request Request) (PreparedDocument, error) { } type generatorDependencies struct { - loadDefault func(context.Context) (profiles.Bundle, error) + loadProfile func(context.Context, string) (profiles.Bundle, error) newRenderer func(*profile.AssetReader) render.Renderer legacyBuild func(string, string) ([]byte, error) } func defaultGeneratorDependencies() generatorDependencies { return generatorDependencies{ - loadDefault: profiles.LoadCompanyDefaultContext, + loadProfile: profiles.LoadCompanyDefaultVersionContext, newRenderer: func(assets *profile.AssetReader) render.Renderer { return word.NewAdapter(assets) }, legacyBuild: legacyBuild, } @@ -274,6 +271,7 @@ func generateWithOptions(ctx context.Context, request Request, options RenderOpt return Delivery{}, fmt.Errorf("%w: Markdown is %d bytes, limit is %d", ErrResourceLimit, len(request.Markdown), limits.MaxMarkdownBytes) } var parsed markdownrenderer.Result + var boundMetadata *markdownrenderer.Metadata materialized := bytes.HasPrefix(bytes.TrimSpace(request.Markdown), []byte("---")) if materialized { parsed, err = parseMarkdownContext(ctx, request.Markdown, false, limits) @@ -283,17 +281,20 @@ func generateWithOptions(ctx context.Context, request Request, options RenderOpt if err := validateWorkspaceRoute(request, parsed.Metadata); err != nil { return Delivery{}, err } + boundMetadata = &parsed.Metadata } - bundle, err := dependencies.loadDefault(ctx) + boundVersion, err := profileVersionFor(request, boundMetadata) if err != nil { - return Delivery{}, fmt.Errorf("docgen: load company default: %w", err) + return Delivery{}, err + } + bundle, err := dependencies.loadProfile(ctx, boundVersion) + if err != nil { + return Delivery{}, fmt.Errorf("docgen: load company default %s: %w", boundVersion, err) } profileID, profileVersion := bundle.Snapshot.ProfileIdentity() - if request.ProfileID != "" || request.ProfileVersion != "" { - if request.ProfileID != profileID || request.ProfileVersion != profileVersion { - return Delivery{}, fmt.Errorf("%w: requested %s@%s, available %s@%s", ErrProfileMismatch, - request.ProfileID, request.ProfileVersion, profileID, profileVersion) - } + if profileID != defaultProfileID || profileVersion != boundVersion { + return Delivery{}, fmt.Errorf("%w: loaded %s@%s, bound %s@%s", ErrProfileMismatch, + profileID, profileVersion, defaultProfileID, boundVersion) } if !materialized { source, sourceErr := workspaceMarkdown(request, profileID, profileVersion) @@ -481,6 +482,37 @@ func validateWorkspaceRoute(request Request, metadata markdownrenderer.Metadata) return nil } +// profileVersionFor returns the company-default version a document renders +// with. Published versions are immutable, so a version the persisted Markdown +// or the request already binds is kept; only a new, unbound document takes the +// latest published version. metadata is nil until Markdown is materialized. +func profileVersionFor(request Request, metadata *markdownrenderer.Metadata) (string, error) { + requested := request.ProfileID != "" || request.ProfileVersion != "" + if requested && !publishedProfileVersion(request.ProfileID, request.ProfileVersion) { + return "", fmt.Errorf("%w: requested %s@%s is not a published %s version", ErrProfileMismatch, + request.ProfileID, request.ProfileVersion, defaultProfileID) + } + if metadata == nil { + if requested { + return request.ProfileVersion, nil + } + return profiles.LatestCompanyDefaultVersion, nil + } + if !publishedProfileVersion(metadata.ProfileID, metadata.ProfileVersion) { + return "", fmt.Errorf("%w: Markdown binds %s@%s, which is not a published %s version", ErrProfileMismatch, + metadata.ProfileID, metadata.ProfileVersion, defaultProfileID) + } + if requested && request.ProfileVersion != metadata.ProfileVersion { + return "", fmt.Errorf("%w: requested %s@%s, Markdown binds %s@%s", ErrProfileMismatch, + request.ProfileID, request.ProfileVersion, metadata.ProfileID, metadata.ProfileVersion) + } + return metadata.ProfileVersion, nil +} + +func publishedProfileVersion(profileID, profileVersion string) bool { + return profileID == defaultProfileID && slices.Contains(profiles.CompanyDefaultVersions(), profileVersion) +} + func validateWorkspaceMetadata(request Request, metadata markdownrenderer.Metadata, profileID, profileVersion string) error { if err := validateWorkspaceRoute(request, metadata); err != nil { return err diff --git a/internal/docgen/renderer_identity_generated.go b/internal/docgen/renderer_identity_generated.go index 7dd2f37..14aad20 100644 --- a/internal/docgen/renderer_identity_generated.go +++ b/internal/docgen/renderer_identity_generated.go @@ -2,6 +2,6 @@ package docgen -const rendererRevision = "10950774736b64b5d8e544ce03d45ae5a6ceca7c" +const rendererRevision = "3f259990b5d43ec0d318f39d496816140ab559b5" const RendererIdentity = "agent-document-renderer@" + rendererRevision diff --git a/internal/docgen/renderer_integration_test.go b/internal/docgen/renderer_integration_test.go index 55db1fe..ca919b4 100644 --- a/internal/docgen/renderer_integration_test.go +++ b/internal/docgen/renderer_integration_test.go @@ -160,7 +160,7 @@ func TestGeneratePersistsCanonicalMarkdownWithStableNodeParity(t *testing.T) { 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 { + reparsed.Metadata.ProfileVersion != profiles.LatestCompanyDefaultVersion || reparsed.Metadata.Controlled { t.Fatalf("controlled metadata changed: %#v", reparsed.Metadata) } @@ -355,10 +355,10 @@ func TestPersistedControlledMetadataRoutesBeforeDefaultProfileLoad(t *testing.T) request.Title = "Controlled" loads := 0 deps := defaultGeneratorDependencies() - baseLoad := deps.loadDefault - deps.loadDefault = func(ctx context.Context) (profiles.Bundle, error) { + baseLoad := deps.loadProfile + deps.loadProfile = func(ctx context.Context, version string) (profiles.Bundle, error) { loads++ - return baseLoad(ctx) + return baseLoad(ctx, version) } _, err := generate(context.Background(), request, deps) if !errors.Is(err, ErrRDMRequired) || loads != 0 { @@ -367,7 +367,7 @@ func TestPersistedControlledMetadataRoutesBeforeDefaultProfileLoad(t *testing.T) } func TestRendererIdentityMatchesPublishedPin(t *testing.T) { - const want = "agent-document-renderer@10950774736b64b5d8e544ce03d45ae5a6ceca7c" + const want = "agent-document-renderer@3f259990b5d43ec0d318f39d496816140ab559b5" if RendererIdentity != want { t.Fatalf("RendererIdentity = %q, want %q", RendererIdentity, want) } @@ -456,6 +456,157 @@ func TestGenerateRejectsProfileAndAssetMismatch(t *testing.T) { } } +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 @@ -713,6 +864,45 @@ func TestRepresentativeLegacyComparisonRecordsOnlyIntentionalDifferences(t *test 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) {