@warpgogol/werkstatt-shared 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -16
- package/README.uk.md +24 -14
- package/package.json +7 -1
- package/src/checks/result-helpers.ts +21 -4
- package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
- package/src/integration/vitest.config.ts +7 -0
- package/src/ontology/manifest.ts +19 -0
- package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
- package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
- package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
- package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
- package/src/ontology/schemas/system/tests/release.test.ts +22 -0
- package/src/ontology/schemas/system/tests/text.test.ts +18 -0
- package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
- package/src/ontology/schemas/tests/biome.test.ts +19 -0
- package/src/ontology/schemas/tests/capability.test.ts +22 -0
- package/src/ontology/schemas/tests/constellation.test.ts +22 -0
- package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
- package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
- package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
- package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
- package/src/ontology/schemas/tests/site-family.test.ts +18 -0
- package/src/ontology/schemas/tests/system.test.ts +20 -0
- package/src/share/agent/tests/api-catalog.test.ts +8 -0
- package/src/share/agent/tests/ard-catalog.test.ts +8 -0
- package/src/share/agent/tests/capability.test.ts +14 -0
- package/src/share/agent/tests/fleet-catalog.test.ts +15 -0
- package/src/share/agent/tests/knowledge.test.ts +20 -0
- package/src/share/agent/tests/manifest.test.ts +9 -0
- package/src/share/agent/tests/mcp-card.test.ts +8 -0
- package/src/share/agent/tests/omit-empty.test.ts +33 -0
- package/src/share/agent/tests/openapi.test.ts +8 -0
- package/src/share/agent/tests/proof.test.ts +15 -0
- package/src/share/knowledge/tests/claim.test.ts +11 -0
- package/src/share/knowledge/tests/derivation.test.ts +30 -0
- package/src/share/knowledge/tests/freshness.test.ts +18 -0
- package/src/share/knowledge/tests/ledger.test.ts +35 -0
- package/src/share/knowledge/tests/plan.test.ts +19 -0
- package/src/share/knowledge/tests/source.test.ts +19 -0
- package/src/share/remediation/remediation-catalog.ts +196 -0
- package/src/share/schemas/tests/claim-records.test.ts +52 -0
- package/src/share/schemas/tests/claims.test.ts +23 -0
- package/src/share/schemas/tests/effects.test.ts +116 -0
- package/src/share/schemas/tests/features.test.ts +61 -0
- package/src/share/schemas/tests/horizontal-align.test.ts +16 -0
- package/src/share/schemas/tests/icon-color.test.ts +16 -0
- package/src/share/schemas/tests/live-photo.test.ts +40 -0
- package/src/share/schemas/tests/material-credit.test.ts +51 -0
- package/src/share/schemas/tests/media.test.ts +26 -0
- package/src/share/schemas/tests/navigation.test.ts +58 -0
- package/src/share/schemas/tests/page-base.test.ts +19 -0
- package/src/share/schemas/tests/participant.test.ts +26 -0
- package/src/share/schemas/tests/person.test.ts +12 -0
- package/src/share/schemas/tests/print.test.ts +30 -0
- package/src/share/schemas/tests/section-background.test.ts +31 -0
- package/src/share/schemas/tests/section-body.test.ts +16 -0
- package/src/share/schemas/tests/section-cards.test.ts +17 -0
- package/src/share/schemas/tests/section-cta.test.ts +41 -0
- package/src/share/schemas/tests/section-header.test.ts +31 -0
- package/src/share/schemas/tests/section-image.test.ts +24 -0
- package/src/share/schemas/tests/section-motion.test.ts +29 -0
- package/src/share/schemas/tests/section-shell.test.ts +42 -0
- package/src/share/schemas/tests/section-stats.test.ts +17 -0
- package/src/share/schemas/tests/site-background.test.ts +33 -0
- package/src/share/schemas/tests/standard-list-item.test.ts +16 -0
- package/src/share/scripts/tests/external-link-qr.test.ts +8 -0
- package/src/share/scripts/tests/external-links.test.ts +25 -0
- package/src/share/scripts/tests/gsap-counter.test.ts +8 -0
- package/src/share/scripts/tests/gsap-mountain-journey.test.ts +8 -0
- package/src/share/scripts/tests/gsap-parallax.test.ts +8 -0
- package/src/share/scripts/tests/gsap-reveal.test.ts +8 -0
- package/src/share/scripts/tests/gsap-stagger.test.ts +8 -0
- package/src/share/scripts/tests/inline-number-animation.test.ts +8 -0
- package/src/share/scripts/tests/lenis.test.ts +8 -0
- package/src/share/scripts/tests/live-photos.test.ts +8 -0
- package/src/share/scripts/tests/lordicon.test.ts +8 -0
- package/src/share/scripts/tests/orchestrator.test.ts +8 -0
- package/src/share/scripts/tests/scheduler.test.ts +23 -0
- package/src/share/semantic/jsonld/tests/article.test.ts +89 -0
- package/src/share/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
- package/src/share/semantic/jsonld/tests/collection-list.test.ts +39 -0
- package/src/share/semantic/jsonld/tests/faq.test.ts +60 -0
- package/src/share/semantic/jsonld/tests/initiative.test.ts +56 -0
- package/src/share/semantic/jsonld/tests/organization.test.ts +123 -0
- package/src/share/semantic/jsonld/tests/person.test.ts +89 -0
- package/src/share/semantic/jsonld/tests/service.test.ts +65 -0
- package/src/share/semantic/jsonld/tests/shared.test.ts +41 -0
- package/src/share/semantic/jsonld/tests/video.test.ts +84 -0
- package/src/share/semantic/jsonld/tests/webpage.test.ts +93 -0
- package/src/share/semantic/jsonld/tests/website.test.ts +27 -0
- package/src/share/semantic/jsonld/types.ts +1 -0
- package/src/share/semantic/models.ts +1 -0
- package/src/share/semantic/tests/ai.test.ts +55 -0
- package/src/share/semantic/tests/block-extraction.test.ts +34 -0
- package/src/share/semantic/tests/breadcrumbs.test.ts +127 -0
- package/src/share/semantic/tests/build-page.test.ts +122 -0
- package/src/share/semantic/tests/business-projection.test.ts +188 -0
- package/src/share/semantic/tests/derived-prices-loader.test.ts +46 -0
- package/src/share/semantic/tests/extract.test.ts +126 -0
- package/src/share/semantic/tests/feed.test.ts +113 -0
- package/src/share/semantic/tests/helpers.ts +32 -0
- package/src/share/semantic/tests/ids.test.ts +118 -0
- package/src/share/semantic/tests/image-sitemap.test.ts +109 -0
- package/src/share/semantic/tests/jsonld.test.ts +61 -0
- package/src/share/semantic/tests/llms-policy.test.ts +60 -0
- package/src/share/semantic/tests/llms.test.ts +118 -0
- package/src/share/semantic/tests/markdown-hygiene.test.ts +72 -0
- package/src/share/semantic/tests/markdown-page.test.ts +97 -0
- package/src/share/semantic/tests/markdown-twin-provenance.test.ts +152 -0
- package/src/share/semantic/tests/organization-profile.test.ts +107 -0
- package/src/share/semantic/tests/output-projection.test.ts +77 -0
- package/src/share/semantic/tests/page-markdown.test.ts +98 -0
- package/src/share/semantic/tests/page-utils.test.ts +88 -0
- package/src/share/semantic/tests/price-marker-resolver.test.ts +88 -0
- package/src/share/semantic/tests/robots.test.ts +70 -0
- package/src/share/semantic/tests/update-stamp.test.ts +102 -0
- package/src/share/utility-registry.yaml +14 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildMarkdownPageSemantic } from "../page-builders/markdown-page.ts";
|
|
3
|
+
|
|
4
|
+
const baseInput = {
|
|
5
|
+
type: "content" as const,
|
|
6
|
+
lang: "de",
|
|
7
|
+
url: "https://example.com/de/page/",
|
|
8
|
+
title: "Test Page",
|
|
9
|
+
description: "A test page",
|
|
10
|
+
heading: "Test Heading",
|
|
11
|
+
blocks: [],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("buildMarkdownPageSemantic", () => {
|
|
15
|
+
it("builds a SemanticPageModel with canonical URL", () => {
|
|
16
|
+
const result = buildMarkdownPageSemantic(baseInput);
|
|
17
|
+
expect(result.type).toBe("content");
|
|
18
|
+
expect(result.lang).toBe("de");
|
|
19
|
+
expect(result.url).toBe("https://example.com/de/page/");
|
|
20
|
+
expect(result.title).toBe("Test Page");
|
|
21
|
+
expect(result.description).toBe("A test page");
|
|
22
|
+
expect(result.heading).toBe("Test Heading");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("builds flat breadcrumb fallback when no breadcrumbs provided", () => {
|
|
26
|
+
const result = buildMarkdownPageSemantic({
|
|
27
|
+
...baseInput,
|
|
28
|
+
breadcrumbsContent: { homeLabel: "Start" },
|
|
29
|
+
});
|
|
30
|
+
expect(result.breadcrumbs).toHaveLength(2);
|
|
31
|
+
expect(result.breadcrumbs[0]).toEqual({ name: "Start", url: "https://example.com/de/" });
|
|
32
|
+
expect(result.breadcrumbs[1]).toEqual({ name: "Test Heading", url: "https://example.com/de/page/" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("uses provided breadcrumbs when available", () => {
|
|
36
|
+
const result = buildMarkdownPageSemantic({
|
|
37
|
+
...baseInput,
|
|
38
|
+
breadcrumbs: [
|
|
39
|
+
{ name: "Home", url: "https://example.com/de/" },
|
|
40
|
+
{ name: "Section", url: "https://example.com/de/section/" },
|
|
41
|
+
{ name: "Page", url: "https://example.com/de/section/page/" },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
expect(result.breadcrumbs).toHaveLength(3);
|
|
45
|
+
expect(result.breadcrumbs[1]).toEqual({ name: "Section", url: "https://example.com/de/section/" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("passes through blocks", () => {
|
|
49
|
+
const blocks = [{ id: "b1", blockType: "prose", heading: "Block 1" }];
|
|
50
|
+
const result = buildMarkdownPageSemantic({ ...baseInput, blocks });
|
|
51
|
+
expect(result.blocks).toBe(blocks);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("passes through people and initiatives", () => {
|
|
55
|
+
const result = buildMarkdownPageSemantic({
|
|
56
|
+
...baseInput,
|
|
57
|
+
people: [{ name: "Jane", role: "CEO" }],
|
|
58
|
+
initiatives: [{ id: "i1", name: "Project", summary: "A project" }],
|
|
59
|
+
});
|
|
60
|
+
expect(result.people).toEqual([{ name: "Jane", role: "CEO" }]);
|
|
61
|
+
expect(result.initiatives).toEqual([{ id: "i1", name: "Project", summary: "A project" }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("passes through faqEntries when provided", () => {
|
|
65
|
+
const result = buildMarkdownPageSemantic({
|
|
66
|
+
...baseInput,
|
|
67
|
+
faqEntries: [{ id: "q1", question: "Q?", answer: "A." }],
|
|
68
|
+
});
|
|
69
|
+
expect(result.faqEntries).toEqual([{ id: "q1", question: "Q?", answer: "A." }]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sets faqEntries to undefined when empty", () => {
|
|
73
|
+
const result = buildMarkdownPageSemantic({
|
|
74
|
+
...baseInput,
|
|
75
|
+
faqEntries: [],
|
|
76
|
+
});
|
|
77
|
+
expect(result.faqEntries).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("includes audience when provided", () => {
|
|
81
|
+
const result = buildMarkdownPageSemantic({
|
|
82
|
+
...baseInput,
|
|
83
|
+
audience: "developer",
|
|
84
|
+
});
|
|
85
|
+
expect(result.audience).toBe("developer");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("omits audience when not provided", () => {
|
|
89
|
+
const result = buildMarkdownPageSemantic(baseInput);
|
|
90
|
+
expect(result.audience).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("uses default Home label when no breadcrumbsContent", () => {
|
|
94
|
+
const result = buildMarkdownPageSemantic(baseInput);
|
|
95
|
+
expect(result.breadcrumbs[0].name).toBe("Home");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
AUDIENCE_BY_PAGE_TYPE,
|
|
4
|
+
buildMarkdownTwin,
|
|
5
|
+
buildMarkdownTwinFrontmatter,
|
|
6
|
+
computeContentHash,
|
|
7
|
+
parseMarkdownTwinFrontmatter,
|
|
8
|
+
verifyMarkdownTwinHash,
|
|
9
|
+
type MarkdownTwinProvenance,
|
|
10
|
+
} from "../markdown-twin-provenance.ts";
|
|
11
|
+
|
|
12
|
+
const baseProvenance: MarkdownTwinProvenance = {
|
|
13
|
+
canonical: "https://example.com/de/page",
|
|
14
|
+
language: "de",
|
|
15
|
+
lastModified: "2024-01-15",
|
|
16
|
+
license: "CC-BY-4.0",
|
|
17
|
+
generator: "test",
|
|
18
|
+
sourceKind: "authored",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("computeContentHash", () => {
|
|
22
|
+
it("produces sha256: prefix", () => {
|
|
23
|
+
expect(computeContentHash("body text").startsWith("sha256:")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("is deterministic for same input", () => {
|
|
27
|
+
expect(computeContentHash("body")).toBe(computeContentHash("body"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("normalizes trailing whitespace", () => {
|
|
31
|
+
expect(computeContentHash("body\n\n")).toBe(computeContentHash("body"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("normalizes CRLF to LF", () => {
|
|
35
|
+
expect(computeContentHash("a\r\nb")).toBe(computeContentHash("a\nb"));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("buildMarkdownTwinFrontmatter", () => {
|
|
40
|
+
it("includes all core provenance fields", () => {
|
|
41
|
+
const fm = buildMarkdownTwinFrontmatter(baseProvenance, "sha256:abc");
|
|
42
|
+
expect(fm).toContain('canonical: "https://example.com/de/page"');
|
|
43
|
+
expect(fm).toContain('language: "de"');
|
|
44
|
+
expect(fm).toContain('lastModified: "2024-01-15"');
|
|
45
|
+
expect(fm).toContain('contentHash: "sha256:abc"');
|
|
46
|
+
expect(fm).toContain('license: "CC-BY-4.0"');
|
|
47
|
+
expect(fm).toContain('generator: "test"');
|
|
48
|
+
expect(fm).toContain('sourceKind: "authored"');
|
|
49
|
+
expect(fm).toContain('schema: "gogol.markdown-twin@2"');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("emits null for lastModified when null", () => {
|
|
53
|
+
const fm = buildMarkdownTwinFrontmatter(
|
|
54
|
+
{ ...baseProvenance, lastModified: null },
|
|
55
|
+
"sha256:abc",
|
|
56
|
+
);
|
|
57
|
+
expect(fm).toContain("lastModified: null");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("includes pageId when provided", () => {
|
|
61
|
+
const fm = buildMarkdownTwinFrontmatter(
|
|
62
|
+
{ ...baseProvenance, pageId: "about" },
|
|
63
|
+
"sha256:abc",
|
|
64
|
+
);
|
|
65
|
+
expect(fm).toContain('pageId: "about"');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("includes sourceInputs when provided", () => {
|
|
69
|
+
const fm = buildMarkdownTwinFrontmatter(
|
|
70
|
+
{ ...baseProvenance, sourceInputs: ["file1.md", "file2.md"] },
|
|
71
|
+
"sha256:abc",
|
|
72
|
+
);
|
|
73
|
+
expect(fm).toContain('sourceInputs:');
|
|
74
|
+
expect(fm).toContain('"file1.md"');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("includes semantic meta when provided", () => {
|
|
78
|
+
const fm = buildMarkdownTwinFrontmatter(
|
|
79
|
+
{
|
|
80
|
+
...baseProvenance,
|
|
81
|
+
semantic: {
|
|
82
|
+
id: "page-1",
|
|
83
|
+
route: "/de/page",
|
|
84
|
+
title: "Page",
|
|
85
|
+
type: "content",
|
|
86
|
+
domain: "content",
|
|
87
|
+
audience: "general",
|
|
88
|
+
lang: "de",
|
|
89
|
+
metaDescription: "A page",
|
|
90
|
+
priority: 0.7,
|
|
91
|
+
tags: ["tag1"],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
"sha256:abc",
|
|
95
|
+
);
|
|
96
|
+
expect(fm).toContain('id: "page-1"');
|
|
97
|
+
expect(fm).toContain('type: "content"');
|
|
98
|
+
expect(fm).toContain("priority: 0.7");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("buildMarkdownTwin", () => {
|
|
103
|
+
it("produces frontmatter + body", () => {
|
|
104
|
+
const twin = buildMarkdownTwin("# Hello\n\nWorld", baseProvenance);
|
|
105
|
+
expect(twin.startsWith("---\n")).toBe(true);
|
|
106
|
+
expect(twin).toContain("# Hello");
|
|
107
|
+
expect(twin).toContain("World");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("parseMarkdownTwinFrontmatter", () => {
|
|
112
|
+
it("round-trips buildMarkdownTwin output", () => {
|
|
113
|
+
const body = "# Hello\n\nWorld";
|
|
114
|
+
const twin = buildMarkdownTwin(body, baseProvenance);
|
|
115
|
+
const parsed = parseMarkdownTwinFrontmatter(twin);
|
|
116
|
+
expect(parsed).not.toBeNull();
|
|
117
|
+
expect(parsed!.frontmatter.canonical).toBe("https://example.com/de/page");
|
|
118
|
+
expect(parsed!.frontmatter.language).toBe("de");
|
|
119
|
+
expect(parsed!.body).toBe("# Hello\n\nWorld\n");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns null for no frontmatter", () => {
|
|
123
|
+
expect(parseMarkdownTwinFrontmatter("just text")).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("verifyMarkdownTwinHash", () => {
|
|
128
|
+
it("verifies correct hash", () => {
|
|
129
|
+
const twin = buildMarkdownTwin("# Hello", baseProvenance);
|
|
130
|
+
expect(verifyMarkdownTwinHash(twin)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("fails for tampered body", () => {
|
|
134
|
+
const twin = buildMarkdownTwin("# Hello", baseProvenance);
|
|
135
|
+
const tampered = twin.replace("# Hello", "# Tampered");
|
|
136
|
+
expect(verifyMarkdownTwinHash(tampered)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("AUDIENCE_BY_PAGE_TYPE", () => {
|
|
141
|
+
it("maps home to general", () => {
|
|
142
|
+
expect(AUDIENCE_BY_PAGE_TYPE.home).toBe("general");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("maps projects to developer", () => {
|
|
146
|
+
expect(AUDIENCE_BY_PAGE_TYPE.projects).toBe("developer");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("maps legal to business_owner", () => {
|
|
150
|
+
expect(AUDIENCE_BY_PAGE_TYPE.legal).toBe("business_owner");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildOrganizationProfile } from "../organization-profile.ts";
|
|
3
|
+
|
|
4
|
+
const baseInput = {
|
|
5
|
+
lang: "de",
|
|
6
|
+
siteUrl: "https://example.com",
|
|
7
|
+
brandName: "Warpgogol",
|
|
8
|
+
description: "Digital infrastructure",
|
|
9
|
+
founders: [],
|
|
10
|
+
boardMembers: [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("buildOrganizationProfile", () => {
|
|
14
|
+
it("builds organization with canonical root URL", () => {
|
|
15
|
+
const result = buildOrganizationProfile(baseInput);
|
|
16
|
+
expect(result.organization.name).toBe("Warpgogol");
|
|
17
|
+
expect(result.organization.url).toBe("https://example.com/");
|
|
18
|
+
expect(result.organization.description).toBe("Digital infrastructure");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("emits image as og-image.png absolute URL", () => {
|
|
22
|
+
const result = buildOrganizationProfile(baseInput);
|
|
23
|
+
expect(result.organization.image).toBe("https://example.com/og-image.png");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("deduplicates people by name", () => {
|
|
27
|
+
const result = buildOrganizationProfile({
|
|
28
|
+
...baseInput,
|
|
29
|
+
founders: [{ name: "Jane" }, { name: "John" }],
|
|
30
|
+
boardMembers: [{ name: "Jane" }, { name: "Bob" }],
|
|
31
|
+
});
|
|
32
|
+
expect(result.people.map((p) => p.name).sort()).toEqual(["Bob", "Jane", "John"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("prefers team over founders+board when available", () => {
|
|
36
|
+
const result = buildOrganizationProfile({
|
|
37
|
+
...baseInput,
|
|
38
|
+
founders: [{ name: "Founder" }],
|
|
39
|
+
boardMembers: [{ name: "Board" }],
|
|
40
|
+
team: [{ name: "Team Member" }],
|
|
41
|
+
});
|
|
42
|
+
expect(result.people).toHaveLength(1);
|
|
43
|
+
expect(result.people[0].name).toBe("Team Member");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("builds address from primitives", () => {
|
|
47
|
+
const result = buildOrganizationProfile({
|
|
48
|
+
...baseInput,
|
|
49
|
+
address: { street: "Test St", streetNumber: "1", zip: "12345", city: "Berlin", country: "DE" },
|
|
50
|
+
});
|
|
51
|
+
expect(result.organization.address).toEqual({
|
|
52
|
+
streetAddress: "Test St 1",
|
|
53
|
+
postalCode: "12345",
|
|
54
|
+
addressLocality: "Berlin",
|
|
55
|
+
addressCountry: "DE",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("emits contactPoints when email or contactType provided", () => {
|
|
60
|
+
const result = buildOrganizationProfile({
|
|
61
|
+
...baseInput,
|
|
62
|
+
email: "info@example.com",
|
|
63
|
+
contactType: "support",
|
|
64
|
+
});
|
|
65
|
+
expect(result.organization.contactPoints).toEqual([
|
|
66
|
+
{ contactType: "support", email: "info@example.com" },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("emits donationAccount from bank primitives", () => {
|
|
71
|
+
const result = buildOrganizationProfile({
|
|
72
|
+
...baseInput,
|
|
73
|
+
bank: { accountHolder: "Org", iban: "DE00", bic: "BIC", bankName: "Bank", konto: "1", blz: "2" },
|
|
74
|
+
});
|
|
75
|
+
expect(result.organization.donationAccount).toEqual({
|
|
76
|
+
accountHolder: "Org",
|
|
77
|
+
iban: "DE00",
|
|
78
|
+
bic: "BIC",
|
|
79
|
+
bankName: "Bank",
|
|
80
|
+
accountNumber: "1",
|
|
81
|
+
bankCode: "2",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("emits sameAs and logo when provided", () => {
|
|
86
|
+
const result = buildOrganizationProfile({
|
|
87
|
+
...baseInput,
|
|
88
|
+
sameAs: ["https://twitter.com/example"],
|
|
89
|
+
logoUrl: "https://example.com/logo.png",
|
|
90
|
+
});
|
|
91
|
+
expect(result.organization.sameAs).toEqual(["https://twitter.com/example"]);
|
|
92
|
+
expect(result.organization.logo).toBe("https://example.com/logo.png");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("emits schemaType override", () => {
|
|
96
|
+
const result = buildOrganizationProfile({
|
|
97
|
+
...baseInput,
|
|
98
|
+
schemaType: ["Organization", "ProfessionalService"],
|
|
99
|
+
});
|
|
100
|
+
expect(result.organization.schemaType).toEqual(["Organization", "ProfessionalService"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns empty initiatives array", () => {
|
|
104
|
+
const result = buildOrganizationProfile(baseInput);
|
|
105
|
+
expect(result.initiatives).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolvePageOutput } from "../output-projection.ts";
|
|
3
|
+
|
|
4
|
+
describe("resolvePageOutput", () => {
|
|
5
|
+
it("returns defaults for undefined input", () => {
|
|
6
|
+
const result = resolvePageOutput(undefined);
|
|
7
|
+
expect(result.sitemap.include).toBe(true);
|
|
8
|
+
expect(result.sitemap.category).toBe("content");
|
|
9
|
+
expect(result.sitemap.includeLastmod).toBe(true);
|
|
10
|
+
expect(result.robots.index).toBe(true);
|
|
11
|
+
expect(result.robots.follow).toBe(true);
|
|
12
|
+
expect(result.llms.depth).toBe("full");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("handles sitemap: false boolean shorthand", () => {
|
|
16
|
+
const result = resolvePageOutput({ sitemap: false });
|
|
17
|
+
expect(result.sitemap.include).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("handles sitemap object form", () => {
|
|
21
|
+
const result = resolvePageOutput({
|
|
22
|
+
sitemap: { include: false, category: "custom", lastmod: "2024-01-01", includeLastmod: false },
|
|
23
|
+
});
|
|
24
|
+
expect(result.sitemap.include).toBe(false);
|
|
25
|
+
expect(result.sitemap.category).toBe("custom");
|
|
26
|
+
expect(result.sitemap.lastmod).toBe("2024-01-01");
|
|
27
|
+
expect(result.sitemap.includeLastmod).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("uses legal category for legal semanticType", () => {
|
|
31
|
+
const result = resolvePageOutput({}, { semanticType: "legal" });
|
|
32
|
+
expect(result.sitemap.category).toBe("legal");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("handles robots: false boolean shorthand", () => {
|
|
36
|
+
const result = resolvePageOutput({ robots: false });
|
|
37
|
+
expect(result.robots.index).toBe(false);
|
|
38
|
+
expect(result.robots.follow).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("handles robots object form", () => {
|
|
42
|
+
const result = resolvePageOutput({ robots: { index: false, follow: true } });
|
|
43
|
+
expect(result.robots.index).toBe(false);
|
|
44
|
+
expect(result.robots.follow).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles llms string shorthand", () => {
|
|
48
|
+
const result = resolvePageOutput({ llms: "index-only" });
|
|
49
|
+
expect(result.llms.depth).toBe("index-only");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("handles llms object form with sections", () => {
|
|
53
|
+
const result = resolvePageOutput({
|
|
54
|
+
llms: { depth: "summary", sections: { exclude: ["section-1", "section-2"] } },
|
|
55
|
+
});
|
|
56
|
+
expect(result.llms.depth).toBe("summary");
|
|
57
|
+
expect(result.llms.sections?.exclude).toEqual(["section-1", "section-2"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("resolves content image", () => {
|
|
61
|
+
const result = resolvePageOutput({
|
|
62
|
+
image: { url: "https://example.com/img.jpg", alt: "Test", width: 800, height: 600 },
|
|
63
|
+
});
|
|
64
|
+
expect(result.image).toEqual({
|
|
65
|
+
url: "https://example.com/img.jpg",
|
|
66
|
+
alt: "Test",
|
|
67
|
+
width: 800,
|
|
68
|
+
height: 600,
|
|
69
|
+
contentImage: true,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("omits image when url is empty", () => {
|
|
74
|
+
const result = resolvePageOutput({ image: { url: "" } });
|
|
75
|
+
expect(result.image).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildPageMarkdown } from "../page-markdown.ts";
|
|
3
|
+
import { makePage } from "./helpers.ts";
|
|
4
|
+
|
|
5
|
+
describe("buildPageMarkdown", () => {
|
|
6
|
+
it("produces markdown with page title as h1", () => {
|
|
7
|
+
const page = makePage({ title: "Test Page" });
|
|
8
|
+
const result = buildPageMarkdown(page);
|
|
9
|
+
expect(result).toContain("# Test Page");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("includes Summary section from lead", () => {
|
|
13
|
+
const page = makePage({ lead: "This is the lead text." });
|
|
14
|
+
const result = buildPageMarkdown(page);
|
|
15
|
+
expect(result).toContain("## Summary");
|
|
16
|
+
expect(result).toContain("This is the lead text.");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("falls back to description for summary", () => {
|
|
20
|
+
const page = makePage({ description: "A test page description" });
|
|
21
|
+
const result = buildPageMarkdown(page);
|
|
22
|
+
expect(result).toContain("## Summary");
|
|
23
|
+
expect(result).toContain("A test page description");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("includes Business context section for generic blocks", () => {
|
|
27
|
+
const page = makePage({
|
|
28
|
+
blocks: [
|
|
29
|
+
{
|
|
30
|
+
id: "block-1",
|
|
31
|
+
blockType: "prose",
|
|
32
|
+
heading: "About Us",
|
|
33
|
+
summary: "We are great.",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
const result = buildPageMarkdown(page);
|
|
38
|
+
expect(result).toContain("## Business context");
|
|
39
|
+
expect(result).toContain("### About Us");
|
|
40
|
+
expect(result).toContain("We are great.");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("classifies legal blocks as Constraints", () => {
|
|
44
|
+
const page = makePage({
|
|
45
|
+
blocks: [
|
|
46
|
+
{
|
|
47
|
+
id: "block-1",
|
|
48
|
+
blockType: "prose",
|
|
49
|
+
heading: "Legal Notice",
|
|
50
|
+
summary: "Legal info.",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
const result = buildPageMarkdown(page);
|
|
55
|
+
expect(result).toContain("## Constraints");
|
|
56
|
+
expect(result).toContain("### Legal Notice");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("includes People section when page has people", () => {
|
|
60
|
+
const page = makePage({
|
|
61
|
+
people: [{ name: "Jane Doe", role: "CEO", description: "Leader" }],
|
|
62
|
+
});
|
|
63
|
+
const result = buildPageMarkdown(page);
|
|
64
|
+
expect(result).toContain("## People");
|
|
65
|
+
expect(result).toContain("### Jane Doe");
|
|
66
|
+
expect(result).toContain("**CEO**");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes Initiatives section when page has initiatives", () => {
|
|
70
|
+
const page = makePage({
|
|
71
|
+
initiatives: [{ id: "init-1", name: "Project Alpha", summary: "A project" }],
|
|
72
|
+
});
|
|
73
|
+
const result = buildPageMarkdown(page);
|
|
74
|
+
expect(result).toContain("## Initiatives");
|
|
75
|
+
expect(result).toContain("Project Alpha");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("includes FAQ section when page has faqEntries", () => {
|
|
79
|
+
const page = makePage({
|
|
80
|
+
faqEntries: [{ id: "q1", question: "What is this?", answer: "A test." }],
|
|
81
|
+
});
|
|
82
|
+
const result = buildPageMarkdown(page);
|
|
83
|
+
expect(result).toContain("## FAQ");
|
|
84
|
+
expect(result).toContain("### What is this?");
|
|
85
|
+
expect(result).toContain("A test.");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("collapses excessive blank lines", () => {
|
|
89
|
+
const page = makePage({
|
|
90
|
+
blocks: [
|
|
91
|
+
{ id: "b1", blockType: "prose", heading: "Block 1", summary: "S1" },
|
|
92
|
+
{ id: "b2", blockType: "prose", heading: "Block 2", summary: "S2" },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
const result = buildPageMarkdown(page);
|
|
96
|
+
expect(result).not.toMatch(/\n{3,}/);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
blocksToMarkdown,
|
|
4
|
+
extractAnswerBlocksFromMarkdown,
|
|
5
|
+
toPageEntryId,
|
|
6
|
+
toSemanticAnswerBlocks,
|
|
7
|
+
} from "../page-utils.ts";
|
|
8
|
+
|
|
9
|
+
describe("extractAnswerBlocksFromMarkdown", () => {
|
|
10
|
+
it("extracts h2 sections with content", () => {
|
|
11
|
+
const md = "## Section 1\n\nContent 1\n\n## Section 2\n\nContent 2";
|
|
12
|
+
const blocks = extractAnswerBlocksFromMarkdown(md);
|
|
13
|
+
expect(blocks).toHaveLength(2);
|
|
14
|
+
expect(blocks[0]).toEqual({ heading: "Section 1", content: "Content 1" });
|
|
15
|
+
expect(blocks[1]).toEqual({ heading: "Section 2", content: "Content 2" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("ignores content before first h2", () => {
|
|
19
|
+
const md = "Intro text\n\n## Section\n\nContent";
|
|
20
|
+
const blocks = extractAnswerBlocksFromMarkdown(md);
|
|
21
|
+
expect(blocks).toHaveLength(1);
|
|
22
|
+
expect(blocks[0].heading).toBe("Section");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns empty for no h2", () => {
|
|
26
|
+
expect(extractAnswerBlocksFromMarkdown("Just text")).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("blocksToMarkdown", () => {
|
|
31
|
+
it("converts blocks back to markdown", () => {
|
|
32
|
+
const blocks = [
|
|
33
|
+
{ heading: "Section 1", content: "Content 1" },
|
|
34
|
+
{ heading: "Section 2", content: "Content 2" },
|
|
35
|
+
];
|
|
36
|
+
expect(blocksToMarkdown(blocks)).toBe("## Section 1\n\nContent 1\n\n## Section 2\n\nContent 2");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("toPageEntryId", () => {
|
|
41
|
+
it("strips language prefix and .html", () => {
|
|
42
|
+
expect(toPageEntryId(new URL("https://example.com/de/about.html"))).toBe("about");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("handles nested paths", () => {
|
|
46
|
+
expect(toPageEntryId(new URL("https://example.com/de/section/page"))).toBe("section-page");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns index for root", () => {
|
|
50
|
+
expect(toPageEntryId(new URL("https://example.com/de/"))).toBe("index");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("toSemanticAnswerBlocks", () => {
|
|
55
|
+
it("produces SemanticBlock with blockType prose", () => {
|
|
56
|
+
const blocks = [{ heading: "Section", content: "Summary line\n- fact 1\n- fact 2" }];
|
|
57
|
+
const result = toSemanticAnswerBlocks(blocks);
|
|
58
|
+
expect(result).toHaveLength(1);
|
|
59
|
+
expect(result[0].blockType).toBe("prose");
|
|
60
|
+
expect(result[0].heading).toBe("Section");
|
|
61
|
+
expect(result[0].summary).toBe("Summary line");
|
|
62
|
+
expect(result[0].facts).toEqual(["- fact 1", "- fact 2"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("preserves table content as summary", () => {
|
|
66
|
+
const blocks = [
|
|
67
|
+
{
|
|
68
|
+
heading: "Pricing",
|
|
69
|
+
content: "| Col1 | Col2 |\n| --- | --- |\n| a | b |",
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
const result = toSemanticAnswerBlocks(blocks);
|
|
73
|
+
expect(result[0].summary).toContain("| Col1 | Col2 |");
|
|
74
|
+
expect(result[0].facts).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("preserves multi-paragraph content as summary", () => {
|
|
78
|
+
const blocks = [
|
|
79
|
+
{
|
|
80
|
+
heading: "Section",
|
|
81
|
+
content: "First paragraph.\n\nSecond paragraph.",
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
const result = toSemanticAnswerBlocks(blocks);
|
|
85
|
+
expect(result[0].summary).toBe("First paragraph.\n\nSecond paragraph.");
|
|
86
|
+
expect(result[0].facts).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|