@warpgogol/werkstatt-shared 0.13.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.
Files changed (178) hide show
  1. package/README.md +26 -16
  2. package/README.uk.md +24 -14
  3. package/package.json +27 -69
  4. package/src/checks/result-helpers.ts +21 -4
  5. package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
  6. package/src/integration/funnel.ts +78 -1
  7. package/src/integration/index.ts +19 -2
  8. package/src/integration/lagebild-ingress.ts +169 -0
  9. package/src/integration/orchestration.ts +7 -13
  10. package/src/integration/port-barrel.ts +9 -28
  11. package/src/integration/tests/funnel-bridge.test.ts +7 -7
  12. package/src/integration/tests/lagebild-ingress.test.ts +118 -0
  13. package/src/integration/vitest.config.ts +7 -0
  14. package/src/observability/typed-refs.ts +1 -1
  15. package/src/ontology/analytics/matomo-fleet.registry.yaml +1 -1
  16. package/src/ontology/archetypes/index.json +12 -9
  17. package/src/ontology/archetypes/index.yaml +26 -10
  18. package/src/ontology/blueprints/website-local.yaml +11 -11
  19. package/src/ontology/blueprints/website-service.yaml +20 -20
  20. package/src/ontology/capabilities/lead.submit.yaml +2 -2
  21. package/src/ontology/enums.ts +2 -2
  22. package/src/ontology/external-surfaces/jsonld-types.yaml +14 -14
  23. package/src/ontology/external-surfaces/url-schema.yaml +1 -1
  24. package/src/ontology/index.ts +2 -0
  25. package/src/ontology/manifest.ts +19 -0
  26. package/src/ontology/schemas/index.ts +7 -2
  27. package/src/ontology/schemas/system/integrations.ts +1 -1
  28. package/src/ontology/schemas/system/manifest.ts +38 -1
  29. package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
  30. package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
  31. package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
  32. package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
  33. package/src/ontology/schemas/system/tests/release.test.ts +22 -0
  34. package/src/ontology/schemas/system/tests/text.test.ts +18 -0
  35. package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
  36. package/src/ontology/schemas/system.ts +7 -2
  37. package/src/ontology/schemas/tests/biome.test.ts +19 -0
  38. package/src/ontology/schemas/tests/capability.test.ts +22 -0
  39. package/src/ontology/schemas/tests/constellation.test.ts +22 -0
  40. package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
  41. package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
  42. package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
  43. package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
  44. package/src/ontology/schemas/tests/site-family.test.ts +18 -0
  45. package/src/ontology/schemas/tests/system.test.ts +20 -0
  46. package/src/ontology/shared-section-props/common.ts +11 -0
  47. package/src/ontology/shared-section-props/visual-header.ts +1 -1
  48. package/src/share/agent/api-catalog.ts +5 -0
  49. package/src/share/agent/ard-catalog.ts +14 -0
  50. package/src/share/agent/index.ts +2 -0
  51. package/src/share/agent/manifest.ts +25 -2
  52. package/src/share/agent/mcp-card.ts +4 -0
  53. package/src/share/agent/openapi.ts +78 -0
  54. package/src/share/agent/search.test.ts +188 -0
  55. package/src/share/agent/search.ts +164 -0
  56. package/src/share/agent/tests/api-catalog.test.ts +8 -0
  57. package/src/share/agent/tests/ard-catalog.test.ts +8 -0
  58. package/src/share/agent/tests/capability.test.ts +14 -0
  59. package/src/share/agent/tests/fleet-catalog.test.ts +15 -0
  60. package/src/share/agent/tests/knowledge.test.ts +20 -0
  61. package/src/share/agent/tests/manifest.test.ts +9 -0
  62. package/src/share/agent/tests/mcp-card.test.ts +8 -0
  63. package/src/share/agent/tests/omit-empty.test.ts +33 -0
  64. package/src/share/agent/tests/openapi.test.ts +8 -0
  65. package/src/share/agent/tests/proof.test.ts +15 -0
  66. package/src/share/env.d.ts +0 -4
  67. package/src/share/knowledge/tests/claim.test.ts +11 -0
  68. package/src/share/knowledge/tests/derivation.test.ts +30 -0
  69. package/src/share/knowledge/tests/freshness.test.ts +18 -0
  70. package/src/share/knowledge/tests/ledger.test.ts +35 -0
  71. package/src/share/knowledge/tests/plan.test.ts +19 -0
  72. package/src/share/knowledge/tests/source.test.ts +19 -0
  73. package/src/share/remediation/remediation-catalog.ts +196 -0
  74. package/src/share/schemas/effects.ts +16 -0
  75. package/src/share/schemas/media.ts +4 -0
  76. package/src/share/schemas/section-shell.ts +2 -1
  77. package/src/share/schemas/tests/claim-records.test.ts +52 -0
  78. package/src/share/schemas/tests/claims.test.ts +23 -0
  79. package/src/share/schemas/tests/effects.test.ts +116 -0
  80. package/src/share/schemas/tests/features.test.ts +61 -0
  81. package/src/share/schemas/tests/horizontal-align.test.ts +16 -0
  82. package/src/share/schemas/tests/icon-color.test.ts +16 -0
  83. package/src/share/schemas/tests/live-photo.test.ts +40 -0
  84. package/src/share/schemas/tests/material-credit.test.ts +51 -0
  85. package/src/share/schemas/tests/media.test.ts +26 -0
  86. package/src/share/schemas/tests/navigation.test.ts +58 -0
  87. package/src/share/schemas/tests/page-base.test.ts +19 -0
  88. package/src/share/schemas/tests/participant.test.ts +26 -0
  89. package/src/share/schemas/tests/person.test.ts +12 -0
  90. package/src/share/schemas/tests/print.test.ts +30 -0
  91. package/src/share/schemas/tests/section-background.test.ts +31 -0
  92. package/src/share/schemas/tests/section-body.test.ts +16 -0
  93. package/src/share/schemas/tests/section-cards.test.ts +17 -0
  94. package/src/share/schemas/tests/section-cta.test.ts +41 -0
  95. package/src/share/schemas/tests/section-header.test.ts +31 -0
  96. package/src/share/schemas/tests/section-image.test.ts +24 -0
  97. package/src/share/schemas/tests/section-motion.test.ts +29 -0
  98. package/src/share/schemas/tests/section-shell.test.ts +42 -0
  99. package/src/share/schemas/tests/section-stats.test.ts +17 -0
  100. package/src/share/schemas/tests/site-background.test.ts +33 -0
  101. package/src/share/schemas/tests/standard-list-item.test.ts +16 -0
  102. package/src/share/scripts/external-link-qr.ts +9 -1
  103. package/src/share/scripts/gsap-counter.ts +2 -0
  104. package/src/share/scripts/inline-number-animation.ts +2 -0
  105. package/src/share/scripts/live-photos.ts +2 -0
  106. package/src/share/scripts/tests/external-link-qr.test.ts +8 -0
  107. package/src/share/scripts/tests/external-links.test.ts +25 -0
  108. package/src/share/scripts/tests/gsap-counter.test.ts +8 -0
  109. package/src/share/scripts/tests/gsap-mountain-journey.test.ts +8 -0
  110. package/src/share/scripts/tests/gsap-parallax.test.ts +8 -0
  111. package/src/share/scripts/tests/gsap-reveal.test.ts +8 -0
  112. package/src/share/scripts/tests/gsap-stagger.test.ts +8 -0
  113. package/src/share/scripts/tests/inline-number-animation.test.ts +8 -0
  114. package/src/share/scripts/tests/lenis.test.ts +8 -0
  115. package/src/share/scripts/tests/live-photos.test.ts +8 -0
  116. package/src/share/scripts/tests/lordicon.test.ts +8 -0
  117. package/src/share/scripts/tests/orchestrator.test.ts +8 -0
  118. package/src/share/scripts/tests/scheduler.test.ts +23 -0
  119. package/src/share/scripts/video-player.test.ts +210 -0
  120. package/src/share/scripts/video-player.ts +73 -13
  121. package/src/share/semantic/extract.ts +25 -1
  122. package/src/share/semantic/jsonld/tests/article.test.ts +89 -0
  123. package/src/share/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
  124. package/src/share/semantic/jsonld/tests/collection-list.test.ts +39 -0
  125. package/src/share/semantic/jsonld/tests/faq.test.ts +60 -0
  126. package/src/share/semantic/jsonld/tests/initiative.test.ts +56 -0
  127. package/src/share/semantic/jsonld/tests/organization.test.ts +123 -0
  128. package/src/share/semantic/jsonld/tests/person.test.ts +89 -0
  129. package/src/share/semantic/jsonld/tests/service.test.ts +65 -0
  130. package/src/share/semantic/jsonld/tests/shared.test.ts +41 -0
  131. package/src/share/semantic/jsonld/tests/video.test.ts +84 -0
  132. package/src/share/semantic/jsonld/tests/webpage.test.ts +93 -0
  133. package/src/share/semantic/jsonld/tests/website.test.ts +27 -0
  134. package/src/share/semantic/jsonld/types.ts +1 -0
  135. package/src/share/semantic/models.ts +1 -0
  136. package/src/share/semantic/tests/ai.test.ts +55 -0
  137. package/src/share/semantic/tests/block-extraction.test.ts +34 -0
  138. package/src/share/semantic/tests/breadcrumbs.test.ts +127 -0
  139. package/src/share/semantic/tests/build-page.test.ts +122 -0
  140. package/src/share/semantic/tests/business-projection.test.ts +188 -0
  141. package/src/share/semantic/tests/derived-prices-loader.test.ts +46 -0
  142. package/src/share/semantic/tests/extract.test.ts +126 -0
  143. package/src/share/semantic/tests/feed.test.ts +113 -0
  144. package/src/share/semantic/tests/helpers.ts +32 -0
  145. package/src/share/semantic/tests/ids.test.ts +118 -0
  146. package/src/share/semantic/tests/image-sitemap.test.ts +109 -0
  147. package/src/share/semantic/tests/jsonld.test.ts +61 -0
  148. package/src/share/semantic/tests/llms-policy.test.ts +60 -0
  149. package/src/share/semantic/tests/llms.test.ts +118 -0
  150. package/src/share/semantic/tests/markdown-hygiene.test.ts +72 -0
  151. package/src/share/semantic/tests/markdown-page.test.ts +97 -0
  152. package/src/share/semantic/tests/markdown-twin-provenance.test.ts +152 -0
  153. package/src/share/semantic/tests/organization-profile.test.ts +107 -0
  154. package/src/share/semantic/tests/output-projection.test.ts +77 -0
  155. package/src/share/semantic/tests/page-markdown.test.ts +98 -0
  156. package/src/share/semantic/tests/page-utils.test.ts +88 -0
  157. package/src/share/semantic/tests/price-marker-resolver.test.ts +88 -0
  158. package/src/share/semantic/tests/robots.test.ts +70 -0
  159. package/src/share/semantic/tests/split-sentences.test.ts +57 -0
  160. package/src/share/semantic/tests/update-stamp.test.ts +102 -0
  161. package/src/share/tests/manifest.test.ts +51 -0
  162. package/src/share/tests/openapi.test.ts +30 -0
  163. package/src/share/types/qrcode-browser.d.ts +18 -0
  164. package/src/share/utility-registry.yaml +16 -2
  165. package/src/integration/crm-buffer.ts +0 -491
  166. package/src/integration-adapter-supabase-crm/adapter.ts +0 -437
  167. package/src/integration-adapter-supabase-crm/client.ts +0 -605
  168. package/src/integration-adapter-supabase-crm/index.ts +0 -28
  169. package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +0 -482
  170. package/src/integration-adapter-supabase-crm/supabase-rest.ts +0 -89
  171. package/src/integration-adapter-supabase-crm/tenant-registry.ts +0 -334
  172. package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +0 -83
  173. package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +0 -409
  174. package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +0 -288
  175. package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +0 -80
  176. package/src/integration-adapter-supabase-crm/vitest.config.ts +0 -8
  177. package/src/integration-adapter-supabase-crm/worker.ts +0 -223
  178. package/src/ontology/archetypes/components/currency-selector.yaml +0 -19
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildVideoObjectNodes } from "../video.ts";
3
+ import { createJsonLdContext } from "../context.ts";
4
+ import { makePage } from "../../tests/helpers.ts";
5
+
6
+ describe("buildVideoObjectNodes", () => {
7
+ it("returns empty array when no blocks have video data", () => {
8
+ const page = makePage({ blocks: [{ id: "b1", heading: "Test" }] });
9
+ const ctx = createJsonLdContext(page);
10
+ expect(buildVideoObjectNodes(ctx)).toEqual([]);
11
+ });
12
+
13
+ it("builds VideoObject nodes for blocks with video data", () => {
14
+ const page = makePage({
15
+ blocks: [
16
+ {
17
+ id: "vid-1",
18
+ heading: "Video Section",
19
+ video: {
20
+ seo: { name: "Intro Video", description: "An intro", uploadDate: "2024-01-15" },
21
+ manifest: {
22
+ posterUrl: "https://example.com/poster.jpg",
23
+ contentUrl: "https://example.com/video.mp4",
24
+ durationSec: 90,
25
+ },
26
+ },
27
+ },
28
+ ],
29
+ });
30
+ const ctx = createJsonLdContext(page);
31
+ const nodes = buildVideoObjectNodes(ctx);
32
+ expect(nodes).toHaveLength(1);
33
+ expect(nodes[0]["@type"]).toBe("VideoObject");
34
+ expect(nodes[0].name).toBe("Intro Video");
35
+ expect(nodes[0].description).toBe("An intro");
36
+ expect(nodes[0].uploadDate).toBe("2024-01-15");
37
+ expect(nodes[0].thumbnailUrl).toBe("https://example.com/poster.jpg");
38
+ expect(nodes[0].contentUrl).toBe("https://example.com/video.mp4");
39
+ expect(nodes[0].duration).toBe("PT1M30S");
40
+ });
41
+
42
+ it("formats duration correctly for hours, minutes, seconds", () => {
43
+ const page = makePage({
44
+ blocks: [
45
+ {
46
+ id: "vid-2",
47
+ heading: "Long Video",
48
+ video: {
49
+ seo: { name: "Long", description: "Long", uploadDate: "2024-01-15" },
50
+ manifest: {
51
+ posterUrl: "https://example.com/p.jpg",
52
+ contentUrl: "https://example.com/v.mp4",
53
+ durationSec: 3661,
54
+ },
55
+ },
56
+ },
57
+ ],
58
+ });
59
+ const ctx = createJsonLdContext(page);
60
+ const nodes = buildVideoObjectNodes(ctx);
61
+ expect(nodes[0].duration).toBe("PT1H1M1S");
62
+ });
63
+
64
+ it("omits duration when durationSec is not provided", () => {
65
+ const page = makePage({
66
+ blocks: [
67
+ {
68
+ id: "vid-3",
69
+ heading: "No Duration",
70
+ video: {
71
+ seo: { name: "NoDur", description: "No dur", uploadDate: "2024-01-15" },
72
+ manifest: {
73
+ posterUrl: "https://example.com/p.jpg",
74
+ contentUrl: "https://example.com/v.mp4",
75
+ },
76
+ },
77
+ },
78
+ ],
79
+ });
80
+ const ctx = createJsonLdContext(page);
81
+ const nodes = buildVideoObjectNodes(ctx);
82
+ expect(nodes[0].duration).toBeUndefined();
83
+ });
84
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildWebPageNode } from "../webpage.ts";
3
+ import { createJsonLdContext } from "../context.ts";
4
+ import { makePage } from "../../tests/helpers.ts";
5
+
6
+ describe("buildWebPageNode", () => {
7
+ it("builds WebPage node for content type", () => {
8
+ const page = makePage({ type: "content" });
9
+ const ctx = createJsonLdContext(page);
10
+ const node = buildWebPageNode(ctx);
11
+ expect(node["@type"]).toEqual(["WebPage"]);
12
+ expect(node["@id"]).toBe(ctx.webpageId);
13
+ expect(node.url).toBe(page.url);
14
+ expect(node.name).toBe("Test Page");
15
+ expect(node.description).toBe("A test page description");
16
+ expect(node.inLanguage).toBe("de");
17
+ });
18
+
19
+ it("uses heading for name when available", () => {
20
+ const page = makePage({ heading: "Custom Heading" });
21
+ const ctx = createJsonLdContext(page);
22
+ const node = buildWebPageNode(ctx);
23
+ expect(node.headline).toBe("Custom Heading");
24
+ });
25
+
26
+ it("uses title for headline", () => {
27
+ const page = makePage({ title: "Page Title" });
28
+ const ctx = createJsonLdContext(page);
29
+ const node = buildWebPageNode(ctx);
30
+ expect(node.name).toBe("Page Title");
31
+ });
32
+
33
+ it("emits CollectionPage for collection type", () => {
34
+ const page = makePage({ type: "collection" });
35
+ const ctx = createJsonLdContext(page);
36
+ const node = buildWebPageNode(ctx);
37
+ expect(node["@type"]).toEqual(["WebPage", "CollectionPage"]);
38
+ });
39
+
40
+ it("emits AboutPage for about type", () => {
41
+ const page = makePage({ type: "about" });
42
+ const ctx = createJsonLdContext(page);
43
+ const node = buildWebPageNode(ctx);
44
+ expect(node["@type"]).toEqual(["WebPage", "AboutPage"]);
45
+ });
46
+
47
+ it("emits ProfilePage for person type", () => {
48
+ const page = makePage({ type: "person" });
49
+ const ctx = createJsonLdContext(page);
50
+ const node = buildWebPageNode(ctx);
51
+ expect(node["@type"]).toEqual(["WebPage", "ProfilePage"]);
52
+ });
53
+
54
+ it("links to organization via publisher", () => {
55
+ const page = makePage();
56
+ const ctx = createJsonLdContext(page);
57
+ const node = buildWebPageNode(ctx);
58
+ expect(node.publisher).toEqual({ "@id": ctx.ids.organization });
59
+ });
60
+
61
+ it("links to breadcrumb when breadcrumbs > 1", () => {
62
+ const page = makePage({
63
+ breadcrumbs: [
64
+ { name: "Home", url: "https://example.com/de/" },
65
+ { name: "Page", url: "https://example.com/de/page/" },
66
+ ],
67
+ });
68
+ const ctx = createJsonLdContext(page);
69
+ const node = buildWebPageNode(ctx);
70
+ expect(node.breadcrumb).toEqual({ "@id": ctx.breadcrumbId });
71
+ });
72
+
73
+ it("omits breadcrumb when only one crumb", () => {
74
+ const page = makePage({
75
+ breadcrumbs: [{ name: "Home", url: "https://example.com/de/" }],
76
+ });
77
+ const ctx = createJsonLdContext(page);
78
+ const node = buildWebPageNode(ctx);
79
+ expect(node.breadcrumb).toBeUndefined();
80
+ });
81
+
82
+ it("emits primaryImage when present", () => {
83
+ const page = makePage({
84
+ primaryImage: { url: "https://example.com/img.jpg" },
85
+ });
86
+ const ctx = createJsonLdContext(page);
87
+ const node = buildWebPageNode(ctx);
88
+ expect(node.primaryImageOfPage).toEqual({
89
+ "@type": "ImageObject",
90
+ url: "https://example.com/img.jpg",
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildWebSiteNode } from "../website.ts";
3
+ import { createJsonLdContext } from "../context.ts";
4
+ import { makePage } from "../../tests/helpers.ts";
5
+
6
+ describe("buildWebSiteNode", () => {
7
+ it("builds WebSite node with organization URL and language", () => {
8
+ const page = makePage({ lang: "de" });
9
+ const ctx = createJsonLdContext(page);
10
+ const node = buildWebSiteNode(ctx);
11
+ expect(node["@type"]).toBe("WebSite");
12
+ expect(node["@id"]).toBe(ctx.ids.website);
13
+ expect(node.url).toBe(page.organization.url);
14
+ expect(node.inLanguage).toBe("de");
15
+ expect(node.publisher).toEqual({ "@id": ctx.ids.organization });
16
+ });
17
+
18
+ it("uses organization name for WebSite name", () => {
19
+ const page = makePage({
20
+ organization: makePage().organization,
21
+ });
22
+ page.organization.name = "Test Org";
23
+ const ctx = createJsonLdContext(page);
24
+ const node = buildWebSiteNode(ctx);
25
+ expect(node.name).toBe("Test Org");
26
+ });
27
+ });
@@ -1,3 +1,4 @@
1
+ // test-adjacency: exempt — pure type definitions
1
2
  /*
2
3
  <MODULE_CONTRACT>
3
4
  <purpose>Maintains packages/share/src/semantic/jsonld/types.ts as an authored share authored module so agents can evolve it without rediscovering local boundaries.</purpose>
@@ -1,3 +1,4 @@
1
+ // test-adjacency: exempt — pure type definitions
1
2
  /*
2
3
  <MODULE_CONTRACT>
3
4
  <purpose>Defines types for semantic data structures used throughout the Warpgogol ecosystem.</purpose>
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildAiTxt } from "../ai.ts";
3
+
4
+ describe("buildAiTxt", () => {
5
+ it("generates header from siteUrl", () => {
6
+ const result = buildAiTxt({ policy: "allow" }, "https://example.com");
7
+ expect(result).toContain("# ai.txt for example.com");
8
+ expect(result).toContain("policy: allow");
9
+ });
10
+
11
+ it("includes version and updated", () => {
12
+ const result = buildAiTxt({ policy: "allow", version: "1.0", updated: "2024-01-01" }, "https://example.com");
13
+ expect(result).toContain("version: 1.0");
14
+ expect(result).toContain("updated: 2024-01-01");
15
+ });
16
+
17
+ it("includes optional fields when provided", () => {
18
+ const result = buildAiTxt({
19
+ policy: "limited",
20
+ training: "disallow",
21
+ usage: ["search", "summarization"],
22
+ commercial: "no",
23
+ attribution: "required",
24
+ license: "CC-BY-4.0",
25
+ }, "https://example.com");
26
+ expect(result).toContain("training: disallow");
27
+ expect(result).toContain("usage: search, summarization");
28
+ expect(result).toContain("commercial: no");
29
+ expect(result).toContain("attribution: required");
30
+ expect(result).toContain("license: CC-BY-4.0");
31
+ });
32
+
33
+ it("emits provider sections", () => {
34
+ const result = buildAiTxt({
35
+ policy: "allow",
36
+ providers: [
37
+ { name: "OpenAI", policy: "disallow", training: "disallow" },
38
+ { name: "Anthropic", policy: "allow", url: "https://anthropic.com" },
39
+ ],
40
+ }, "https://example.com");
41
+ expect(result).toContain("[OpenAI]");
42
+ expect(result).toContain("[Anthropic]");
43
+ expect(result).toContain("url: https://anthropic.com");
44
+ });
45
+
46
+ it("includes url and contact in header comments", () => {
47
+ const result = buildAiTxt({
48
+ policy: "allow",
49
+ url: "https://example.com/policy",
50
+ contact: "info@example.com",
51
+ }, "https://example.com");
52
+ expect(result).toContain("# See: https://example.com/policy");
53
+ expect(result).toContain("# Contact: info@example.com");
54
+ });
55
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BlockExtractorRegistry, BLOCK_EXTRACTORS } from "../block-extraction.ts";
3
+ import "../block-extractors/index.ts";
4
+
5
+ describe("BlockExtractorRegistry", () => {
6
+ it("registers and retrieves extractors", () => {
7
+ const registry = new BlockExtractorRegistry();
8
+ registry.register({
9
+ blockType: "test-block",
10
+ extract: () => ({ heading: "Test" }),
11
+ });
12
+ expect(registry.has("test-block")).toBe(true);
13
+ expect(registry.get("test-block")?.blockType).toBe("test-block");
14
+ });
15
+
16
+ it("returns undefined for unregistered type", () => {
17
+ const registry = new BlockExtractorRegistry();
18
+ expect(registry.get("nonexistent")).toBeUndefined();
19
+ });
20
+
21
+ it("lists registered types", () => {
22
+ const registry = new BlockExtractorRegistry();
23
+ registry.register({ blockType: "a", extract: () => ({}) });
24
+ registry.register({ blockType: "b", extract: () => ({}) });
25
+ expect(registry.listTypes().sort()).toEqual(["a", "b"]);
26
+ });
27
+ });
28
+
29
+ describe("BLOCK_EXTRACTORS", () => {
30
+ it("is a registry instance with pre-registered extractors", () => {
31
+ expect(BLOCK_EXTRACTORS).toBeInstanceOf(BlockExtractorRegistry);
32
+ expect(BLOCK_EXTRACTORS.listTypes().length).toBeGreaterThan(0);
33
+ });
34
+ });
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildBreadcrumbTrail,
4
+ stripSiteNameFromTitle,
5
+ surfaceAncestorPageIds,
6
+ type BreadcrumbAncestorResolver,
7
+ } from "../breadcrumbs.ts";
8
+
9
+ describe("stripSiteNameFromTitle", () => {
10
+ it("strips site name suffix", () => {
11
+ expect(stripSiteNameFromTitle("About | Warpgogol")).toBe("About");
12
+ });
13
+
14
+ it("returns title unchanged when no separator", () => {
15
+ expect(stripSiteNameFromTitle("About")).toBe("About");
16
+ });
17
+
18
+ it("trims whitespace", () => {
19
+ expect(stripSiteNameFromTitle(" About ")).toBe("About");
20
+ });
21
+ });
22
+
23
+ describe("surfaceAncestorPageIds", () => {
24
+ it("returns empty for non-surface pageId", () => {
25
+ expect(surfaceAncestorPageIds("some-page")).toEqual([]);
26
+ });
27
+
28
+ it("returns empty for depth-0 root landing", () => {
29
+ expect(surfaceAncestorPageIds("industry:_root")).toEqual([]);
30
+ });
31
+
32
+ it("returns root + intermediate ancestors for depth-2", () => {
33
+ expect(surfaceAncestorPageIds("industry:tech:saas")).toEqual([
34
+ "industry:_root",
35
+ "industry:tech",
36
+ ]);
37
+ });
38
+ });
39
+
40
+ describe("buildBreadcrumbTrail", () => {
41
+ const noopResolver: BreadcrumbAncestorResolver = {
42
+ resolveAncestors: async () => [],
43
+ };
44
+
45
+ it("builds [Home, self] trail with no ancestors", async () => {
46
+ const trail = await buildBreadcrumbTrail({
47
+ pageId: "about",
48
+ pageTitle: "About | Site",
49
+ selfUrl: "https://example.com/de/about/",
50
+ homeLabel: "Home",
51
+ homeUrl: "https://example.com/de/",
52
+ lang: "de",
53
+ defaultLang: "de",
54
+ resolver: noopResolver,
55
+ });
56
+ expect(trail).toHaveLength(2);
57
+ expect(trail[0]).toEqual({ name: "Home", url: "https://example.com/de/" });
58
+ expect(trail[1]).toEqual({
59
+ name: "About",
60
+ url: "https://example.com/de/about/",
61
+ pageId: "about",
62
+ });
63
+ });
64
+
65
+ it("inserts ancestors between Home and self", async () => {
66
+ const resolver: BreadcrumbAncestorResolver = {
67
+ resolveAncestors: async () => [
68
+ { name: "Section", url: "https://example.com/de/section/" },
69
+ ],
70
+ };
71
+ const trail = await buildBreadcrumbTrail({
72
+ pageId: "page",
73
+ pageTitle: "Page",
74
+ selfUrl: "https://example.com/de/section/page/",
75
+ homeLabel: "Home",
76
+ homeUrl: "https://example.com/de/",
77
+ lang: "de",
78
+ defaultLang: "de",
79
+ resolver,
80
+ });
81
+ expect(trail).toHaveLength(3);
82
+ expect(trail[1]).toEqual({ name: "Section", url: "https://example.com/de/section/" });
83
+ });
84
+
85
+ it("deduplicates ancestors that collapse onto Home or self", async () => {
86
+ const resolver: BreadcrumbAncestorResolver = {
87
+ resolveAncestors: async () => [
88
+ { name: "Home", url: "https://example.com/de/" },
89
+ { name: "Self", url: "https://example.com/de/page/" },
90
+ { name: "Real Ancestor", url: "https://example.com/de/real/" },
91
+ ],
92
+ };
93
+ const trail = await buildBreadcrumbTrail({
94
+ pageId: "page",
95
+ pageTitle: "Page",
96
+ selfUrl: "https://example.com/de/page/",
97
+ homeLabel: "Home",
98
+ homeUrl: "https://example.com/de/",
99
+ lang: "de",
100
+ defaultLang: "de",
101
+ resolver,
102
+ });
103
+ expect(trail).toHaveLength(3);
104
+ expect(trail[1]).toEqual({ name: "Real Ancestor", url: "https://example.com/de/real/" });
105
+ });
106
+
107
+ it("drops ancestors with empty names", async () => {
108
+ const resolver: BreadcrumbAncestorResolver = {
109
+ resolveAncestors: async () => [
110
+ { name: " ", url: "https://example.com/de/empty/" },
111
+ { name: "Valid", url: "https://example.com/de/valid/" },
112
+ ],
113
+ };
114
+ const trail = await buildBreadcrumbTrail({
115
+ pageId: "page",
116
+ pageTitle: "Page",
117
+ selfUrl: "https://example.com/de/page/",
118
+ homeLabel: "Home",
119
+ homeUrl: "https://example.com/de/",
120
+ lang: "de",
121
+ defaultLang: "de",
122
+ resolver,
123
+ });
124
+ expect(trail).toHaveLength(3);
125
+ expect(trail[1]).toEqual({ name: "Valid", url: "https://example.com/de/valid/" });
126
+ });
127
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildSemanticPageModelWith } from "../build-page.ts";
3
+ import type { SemanticContentReader } from "../build-page.ts";
4
+
5
+ function makeReader(overrides: Partial<SemanticContentReader> = {}): SemanticContentReader {
6
+ return {
7
+ getPageFrontmatter: async () => ({
8
+ title: "Test Page",
9
+ description: "A test page",
10
+ blocks: [],
11
+ }),
12
+ getProseBody: async () => "",
13
+ getHomeLabel: async () => "Home",
14
+ getFaqEntries: async () => [],
15
+ getDerivedPrices: () => null,
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ const baseArgs = {
21
+ pageId: "test-page",
22
+ semanticType: "content" as const,
23
+ lang: "de",
24
+ url: "https://example.com/de/page/",
25
+ profile: {
26
+ organization: { name: "Org", description: "Desc", url: "https://example.com" },
27
+ people: [],
28
+ initiatives: [],
29
+ },
30
+ };
31
+
32
+ describe("buildSemanticPageModelWith", () => {
33
+ it("builds a SemanticPageModel from reader frontmatter", async () => {
34
+ const reader = makeReader();
35
+ const result = await buildSemanticPageModelWith(reader, baseArgs);
36
+ expect(result).not.toBeNull();
37
+ expect(result!.title).toBe("Test Page");
38
+ expect(result!.description).toBe("A test page");
39
+ expect(result!.lang).toBe("de");
40
+ });
41
+
42
+ it("returns null when reader returns null frontmatter and no fallback", async () => {
43
+ const reader = makeReader({ getPageFrontmatter: async () => null });
44
+ const result = await buildSemanticPageModelWith(reader, baseArgs);
45
+ expect(result).toBeNull();
46
+ });
47
+
48
+ it("uses fallbackFrontmatter when reader returns null", async () => {
49
+ const reader = makeReader({ getPageFrontmatter: async () => null });
50
+ const result = await buildSemanticPageModelWith(reader, {
51
+ ...baseArgs,
52
+ fallbackFrontmatter: { title: "Fallback", description: "Fallback desc", blocks: [] },
53
+ });
54
+ expect(result).not.toBeNull();
55
+ expect(result!.title).toBe("Fallback");
56
+ });
57
+
58
+ it("attaches people for about pages", async () => {
59
+ const reader = makeReader();
60
+ const result = await buildSemanticPageModelWith(reader, {
61
+ ...baseArgs,
62
+ semanticType: "about",
63
+ profile: {
64
+ organization: { name: "Org", description: "Desc", url: "https://example.com" },
65
+ people: [{ name: "Jane", role: "CEO" }],
66
+ initiatives: [],
67
+ },
68
+ });
69
+ expect(result!.people).toEqual([{ name: "Jane", role: "CEO" }]);
70
+ });
71
+
72
+ it("attaches initiatives for projects pages", async () => {
73
+ const reader = makeReader();
74
+ const result = await buildSemanticPageModelWith(reader, {
75
+ ...baseArgs,
76
+ semanticType: "projects",
77
+ profile: {
78
+ organization: { name: "Org", description: "Desc", url: "https://example.com" },
79
+ people: [],
80
+ initiatives: [{ id: "i1", name: "Project", summary: "A project" }],
81
+ },
82
+ });
83
+ expect(result!.initiatives).toEqual([{ id: "i1", name: "Project", summary: "A project" }]);
84
+ });
85
+
86
+ it("attaches FAQ entries for donationContact pages", async () => {
87
+ const reader = makeReader({
88
+ getFaqEntries: async () => [{ id: "q1", question: "Q?", answer: "A." }],
89
+ });
90
+ const result = await buildSemanticPageModelWith(reader, {
91
+ ...baseArgs,
92
+ semanticType: "donationContact",
93
+ });
94
+ expect(result!.faqEntries).toEqual([{ id: "q1", question: "Q?", answer: "A." }]);
95
+ });
96
+
97
+ it("uses provided breadcrumbs when supplied", async () => {
98
+ const reader = makeReader();
99
+ const result = await buildSemanticPageModelWith(reader, {
100
+ ...baseArgs,
101
+ breadcrumbs: [
102
+ { name: "Home", url: "https://example.com/de/" },
103
+ { name: "Section", url: "https://example.com/de/section/" },
104
+ { name: "Page", url: "https://example.com/de/page/" },
105
+ ],
106
+ });
107
+ expect(result!.breadcrumbs).toHaveLength(3);
108
+ });
109
+
110
+ it("resolves price markers in heading and description", async () => {
111
+ const reader = makeReader({
112
+ getPageFrontmatter: async () => ({
113
+ title: "Pricing",
114
+ description: "Cost: {amount:50}",
115
+ blocks: [],
116
+ }),
117
+ });
118
+ const result = await buildSemanticPageModelWith(reader, baseArgs);
119
+ expect(result!.description).toContain("€");
120
+ expect(result!.description).not.toContain("{amount:");
121
+ });
122
+ });