@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.
- package/README.md +26 -16
- package/README.uk.md +24 -14
- package/package.json +27 -69
- package/src/checks/result-helpers.ts +21 -4
- package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
- package/src/integration/funnel.ts +78 -1
- package/src/integration/index.ts +19 -2
- package/src/integration/lagebild-ingress.ts +169 -0
- package/src/integration/orchestration.ts +7 -13
- package/src/integration/port-barrel.ts +9 -28
- package/src/integration/tests/funnel-bridge.test.ts +7 -7
- package/src/integration/tests/lagebild-ingress.test.ts +118 -0
- package/src/integration/vitest.config.ts +7 -0
- package/src/observability/typed-refs.ts +1 -1
- package/src/ontology/analytics/matomo-fleet.registry.yaml +1 -1
- package/src/ontology/archetypes/index.json +12 -9
- package/src/ontology/archetypes/index.yaml +26 -10
- package/src/ontology/blueprints/website-local.yaml +11 -11
- package/src/ontology/blueprints/website-service.yaml +20 -20
- package/src/ontology/capabilities/lead.submit.yaml +2 -2
- package/src/ontology/enums.ts +2 -2
- package/src/ontology/external-surfaces/jsonld-types.yaml +14 -14
- package/src/ontology/external-surfaces/url-schema.yaml +1 -1
- package/src/ontology/index.ts +2 -0
- package/src/ontology/manifest.ts +19 -0
- package/src/ontology/schemas/index.ts +7 -2
- package/src/ontology/schemas/system/integrations.ts +1 -1
- package/src/ontology/schemas/system/manifest.ts +38 -1
- 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/system.ts +7 -2
- 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/ontology/shared-section-props/common.ts +11 -0
- package/src/ontology/shared-section-props/visual-header.ts +1 -1
- package/src/share/agent/api-catalog.ts +5 -0
- package/src/share/agent/ard-catalog.ts +14 -0
- package/src/share/agent/index.ts +2 -0
- package/src/share/agent/manifest.ts +25 -2
- package/src/share/agent/mcp-card.ts +4 -0
- package/src/share/agent/openapi.ts +78 -0
- package/src/share/agent/search.test.ts +188 -0
- package/src/share/agent/search.ts +164 -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/env.d.ts +0 -4
- 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/effects.ts +16 -0
- package/src/share/schemas/media.ts +4 -0
- package/src/share/schemas/section-shell.ts +2 -1
- 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/external-link-qr.ts +9 -1
- package/src/share/scripts/gsap-counter.ts +2 -0
- package/src/share/scripts/inline-number-animation.ts +2 -0
- package/src/share/scripts/live-photos.ts +2 -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/scripts/video-player.test.ts +210 -0
- package/src/share/scripts/video-player.ts +73 -13
- package/src/share/semantic/extract.ts +25 -1
- 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/split-sentences.test.ts +57 -0
- package/src/share/semantic/tests/update-stamp.test.ts +102 -0
- package/src/share/tests/manifest.test.ts +51 -0
- package/src/share/tests/openapi.test.ts +30 -0
- package/src/share/types/qrcode-browser.d.ts +18 -0
- package/src/share/utility-registry.yaml +16 -2
- package/src/integration/crm-buffer.ts +0 -491
- package/src/integration-adapter-supabase-crm/adapter.ts +0 -437
- package/src/integration-adapter-supabase-crm/client.ts +0 -605
- package/src/integration-adapter-supabase-crm/index.ts +0 -28
- package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +0 -482
- package/src/integration-adapter-supabase-crm/supabase-rest.ts +0 -89
- package/src/integration-adapter-supabase-crm/tenant-registry.ts +0 -334
- package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +0 -83
- package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +0 -409
- package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +0 -288
- package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +0 -80
- package/src/integration-adapter-supabase-crm/vitest.config.ts +0 -8
- package/src/integration-adapter-supabase-crm/worker.ts +0 -223
- package/src/ontology/archetypes/components/currency-selector.yaml +0 -19
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
BUSINESS_DOMAIN_VISIBILITY,
|
|
4
|
+
projectLocation,
|
|
5
|
+
projectOffer,
|
|
6
|
+
projectPeople,
|
|
7
|
+
projectServices,
|
|
8
|
+
projectWeb,
|
|
9
|
+
} from "../business-projection.ts";
|
|
10
|
+
|
|
11
|
+
describe("BUSINESS_DOMAIN_VISIBILITY", () => {
|
|
12
|
+
it("marks company as public", () => {
|
|
13
|
+
expect(BUSINESS_DOMAIN_VISIBILITY.company).toBe("public");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("marks compliance as none", () => {
|
|
17
|
+
expect(BUSINESS_DOMAIN_VISIBILITY.compliance).toBe("none");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("marks externalServices as none", () => {
|
|
21
|
+
expect(BUSINESS_DOMAIN_VISIBILITY.externalServices).toBe("none");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("marks meta as pageMeta", () => {
|
|
25
|
+
expect(BUSINESS_DOMAIN_VISIBILITY.meta).toBe("pageMeta");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("projectOffer", () => {
|
|
30
|
+
it("returns undefined for no data", () => {
|
|
31
|
+
expect(projectOffer(undefined)).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("projects prices with labels", () => {
|
|
35
|
+
const result = projectOffer({ price: { monthly: "10", yearly: "100" }, currency: "EUR" });
|
|
36
|
+
expect(result?.prices).toHaveLength(2);
|
|
37
|
+
expect(result?.prices![0]).toEqual({
|
|
38
|
+
id: "monthly",
|
|
39
|
+
label: "Monthly",
|
|
40
|
+
amount: "10",
|
|
41
|
+
currency: "EUR",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("projects guarantees", () => {
|
|
46
|
+
const result = projectOffer({
|
|
47
|
+
guarantees: { availability: { label: "99.9% Uptime", detail: "SLA guaranteed" } },
|
|
48
|
+
});
|
|
49
|
+
expect(result?.guarantees).toHaveLength(1);
|
|
50
|
+
expect(result?.guarantees![0]).toEqual({
|
|
51
|
+
id: "availability",
|
|
52
|
+
label: "99.9% Uptime",
|
|
53
|
+
detail: "SLA guaranteed",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("projects growth modules", () => {
|
|
58
|
+
const result = projectOffer({
|
|
59
|
+
growthModules: { pro: { label: "Pro", description: "Advanced features", price: "50" } },
|
|
60
|
+
});
|
|
61
|
+
expect(result?.growthModules).toHaveLength(1);
|
|
62
|
+
expect(result?.growthModules![0]).toEqual({
|
|
63
|
+
id: "pro",
|
|
64
|
+
label: "Pro",
|
|
65
|
+
description: "Advanced features",
|
|
66
|
+
price: "50",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("projects terms (changePrice, hourlyRate, billingDay)", () => {
|
|
71
|
+
const result = projectOffer({ changePrice: "5", hourlyRate: "80", billingDay: "15" });
|
|
72
|
+
expect(result?.changePrice).toBe("5");
|
|
73
|
+
expect(result?.hourlyRate).toBe("80");
|
|
74
|
+
expect(result?.billingDay).toBe("15");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("projectPeople", () => {
|
|
79
|
+
it("returns empty array for no records", () => {
|
|
80
|
+
expect(projectPeople(undefined)).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("projects people with name and role", () => {
|
|
84
|
+
const result = projectPeople([{ name: "Jane", role: "CEO", bio: "Leader" }]);
|
|
85
|
+
expect(result).toHaveLength(1);
|
|
86
|
+
expect(result[0].name).toBe("Jane");
|
|
87
|
+
expect(result[0].role).toBe("CEO");
|
|
88
|
+
expect(result[0].description).toBe("Leader");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("skips records without name", () => {
|
|
92
|
+
const result = projectPeople([{ role: "CEO" }, { name: "Jane" }]);
|
|
93
|
+
expect(result).toHaveLength(1);
|
|
94
|
+
expect(result[0].name).toBe("Jane");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("maps sameAs and affiliations", () => {
|
|
98
|
+
const result = projectPeople([
|
|
99
|
+
{
|
|
100
|
+
name: "Jane",
|
|
101
|
+
sameAs: ["https://twitter.com/jane"],
|
|
102
|
+
affiliations: ["Org A"],
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
expect(result[0].sameAs).toEqual(["https://twitter.com/jane"]);
|
|
106
|
+
expect(result[0].affiliations).toEqual(["Org A"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("maps birth/death dates", () => {
|
|
110
|
+
const result = projectPeople([{ name: "Jane", lifespan: { born: "1980", died: "2020" } }]);
|
|
111
|
+
expect(result[0].birthDate).toBe("1980");
|
|
112
|
+
expect(result[0].deathDate).toBe("2020");
|
|
113
|
+
expect(result[0].isDeceased).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("projectLocation", () => {
|
|
118
|
+
it("returns undefined for no data", () => {
|
|
119
|
+
expect(projectLocation(undefined)).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("projects city, region, country", () => {
|
|
123
|
+
const result = projectLocation({
|
|
124
|
+
city: { name: "Berlin" },
|
|
125
|
+
region: { name: "BE" },
|
|
126
|
+
country: { name: "DE" },
|
|
127
|
+
});
|
|
128
|
+
expect(result?.locality).toBe("Berlin");
|
|
129
|
+
expect(result?.region).toBe("BE");
|
|
130
|
+
expect(result?.country).toBe("DE");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("projects serviceArea", () => {
|
|
134
|
+
const result = projectLocation({ serviceArea: ["Berlin", "Potsdam"] });
|
|
135
|
+
expect(result?.serviceArea).toEqual(["Berlin", "Potsdam"]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("returns undefined when all fields empty", () => {
|
|
139
|
+
expect(projectLocation({})).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("projectServices", () => {
|
|
144
|
+
it("returns empty for no records", () => {
|
|
145
|
+
expect(projectServices(undefined)).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("projects services with name and description", () => {
|
|
149
|
+
const result = projectServices([
|
|
150
|
+
{ name: "Hosting", slug: "hosting", description: "Web hosting" },
|
|
151
|
+
]);
|
|
152
|
+
expect(result).toHaveLength(1);
|
|
153
|
+
expect(result[0]).toEqual({ id: "hosting", name: "Hosting", description: "Web hosting" });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("uses name as id when slug missing", () => {
|
|
157
|
+
const result = projectServices([{ name: "Consulting" }]);
|
|
158
|
+
expect(result[0].id).toBe("Consulting");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("drops records without name", () => {
|
|
162
|
+
expect(projectServices([{ description: "No name" }])).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("projectWeb", () => {
|
|
167
|
+
it("returns undefined for no data", () => {
|
|
168
|
+
expect(projectWeb(undefined)).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("projects primaryUrl and statusUrl", () => {
|
|
172
|
+
const result = projectWeb({
|
|
173
|
+
primaryUrl: "https://example.com",
|
|
174
|
+
statusUrl: "https://status.example.com",
|
|
175
|
+
});
|
|
176
|
+
expect(result?.primaryUrl).toBe("https://example.com");
|
|
177
|
+
expect(result?.statusUrl).toBe("https://status.example.com");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("projects domains", () => {
|
|
181
|
+
const result = projectWeb({ domains: { primary: "example.com", german: "example.de" } });
|
|
182
|
+
expect(result?.domains).toEqual({ primary: "example.com", german: "example.de" });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns undefined when all fields empty", () => {
|
|
186
|
+
expect(projectWeb({})).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { loadDerivedPrices } from "../derived-prices-loader.ts";
|
|
6
|
+
|
|
7
|
+
describe("loadDerivedPrices", () => {
|
|
8
|
+
it("returns null when file does not exist", () => {
|
|
9
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "test-prices-"));
|
|
10
|
+
try {
|
|
11
|
+
const result = loadDerivedPrices(tmpDir);
|
|
12
|
+
expect(result).toBeNull();
|
|
13
|
+
} finally {
|
|
14
|
+
rmSync(tmpDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("loads and parses JSON when file exists", () => {
|
|
19
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "test-prices-"));
|
|
20
|
+
try {
|
|
21
|
+
const srcDir = join(tmpDir, "src");
|
|
22
|
+
mkdirSync(srcDir, { recursive: true });
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(srcDir, "derived-prices.generated.json"),
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
"https://warpgogol.com/id/offerings/hosting": [
|
|
27
|
+
{
|
|
28
|
+
chargeRef: "monthly",
|
|
29
|
+
targetCurrency: "USD",
|
|
30
|
+
amount: { value: "15", currency: "USD" },
|
|
31
|
+
trace: {
|
|
32
|
+
source: { amount: "10", currency: "EUR" },
|
|
33
|
+
rate: { value: "0.9", pair: "EUR/USD" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
const result = loadDerivedPrices(tmpDir);
|
|
40
|
+
expect(result).not.toBeNull();
|
|
41
|
+
expect(Object.keys(result!)).toContain("https://warpgogol.com/id/offerings/hosting");
|
|
42
|
+
} finally {
|
|
43
|
+
rmSync(tmpDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
extractEmail,
|
|
4
|
+
extractListFacts,
|
|
5
|
+
extractParagraphs,
|
|
6
|
+
extractPostalAddress,
|
|
7
|
+
mergePeople,
|
|
8
|
+
normalizeWhitespace,
|
|
9
|
+
splitMarkdownSections,
|
|
10
|
+
splitSentences,
|
|
11
|
+
} from "../extract.ts";
|
|
12
|
+
|
|
13
|
+
describe("normalizeWhitespace", () => {
|
|
14
|
+
it("collapses multiple spaces", () => {
|
|
15
|
+
expect(normalizeWhitespace("a b\n c")).toBe("a b c");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("splitMarkdownSections", () => {
|
|
20
|
+
it("splits by h2 headings", () => {
|
|
21
|
+
const md = "## Section 1\n\nBody 1\n\n## Section 2\n\nBody 2";
|
|
22
|
+
const sections = splitMarkdownSections(md);
|
|
23
|
+
expect(sections).toHaveLength(2);
|
|
24
|
+
expect(sections[0]).toEqual({ heading: "Section 1", body: "Body 1" });
|
|
25
|
+
expect(sections[1]).toEqual({ heading: "Section 2", body: "Body 2" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("splits by h3 when level=3", () => {
|
|
29
|
+
const md = "### Sub 1\n\nBody 1\n\n### Sub 2\n\nBody 2";
|
|
30
|
+
const sections = splitMarkdownSections(md, 3);
|
|
31
|
+
expect(sections).toHaveLength(2);
|
|
32
|
+
expect(sections[0].heading).toBe("Sub 1");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns empty for empty input", () => {
|
|
36
|
+
expect(splitMarkdownSections("")).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("extractParagraphs", () => {
|
|
41
|
+
it("splits on blank lines", () => {
|
|
42
|
+
expect(extractParagraphs("Para 1\n\nPara 2\n\nPara 3")).toEqual(["Para 1", "Para 2", "Para 3"]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("splitSentences", () => {
|
|
47
|
+
it("splits on period + space + capital", () => {
|
|
48
|
+
expect(splitSentences("Hello world. Next sentence.")).toEqual([
|
|
49
|
+
"Hello world.",
|
|
50
|
+
"Next sentence.",
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("handles German abbreviations", () => {
|
|
55
|
+
const result = splitSentences("Das ist z.B. ein Test. Und noch einer.", "de");
|
|
56
|
+
expect(result).toHaveLength(2);
|
|
57
|
+
expect(result[0]).toContain("z.B.");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("handles English abbreviations", () => {
|
|
61
|
+
const result = splitSentences("See e.g. the docs. For more info.", "en");
|
|
62
|
+
expect(result).toHaveLength(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("skips numbered list markers", () => {
|
|
66
|
+
const result = splitSentences("1. First item 2. Second item", "en");
|
|
67
|
+
expect(result).toHaveLength(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns empty for empty input", () => {
|
|
71
|
+
expect(splitSentences("")).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("extractListFacts", () => {
|
|
76
|
+
it("extracts bullet list items", () => {
|
|
77
|
+
expect(extractListFacts("- Item 1\n- Item 2")).toEqual(["Item 1", "Item 2"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("extracts numbered list items", () => {
|
|
81
|
+
expect(extractListFacts("1. First\n2. Second")).toEqual(["First", "Second"]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("extractEmail", () => {
|
|
86
|
+
it("extracts email from text", () => {
|
|
87
|
+
expect(extractEmail("Contact us at info@example.com today")).toBe("info@example.com");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns undefined when no email", () => {
|
|
91
|
+
expect(extractEmail("No email here")).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("extractPostalAddress", () => {
|
|
96
|
+
it("extracts address with ZIP", () => {
|
|
97
|
+
const result = extractPostalAddress("Test Str. 1, 12345 Berlin");
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
streetAddress: "Test Str. 1",
|
|
100
|
+
postalCode: "12345",
|
|
101
|
+
addressLocality: "Berlin",
|
|
102
|
+
addressCountry: "DE",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns undefined when no match", () => {
|
|
107
|
+
expect(extractPostalAddress("No address here")).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("mergePeople", () => {
|
|
112
|
+
it("merges and deduplicates by name", () => {
|
|
113
|
+
const result = mergePeople(
|
|
114
|
+
[{ name: "Jane", role: "Founder" }],
|
|
115
|
+
[{ name: "Jane", description: "Bio" }, { name: "John" }],
|
|
116
|
+
);
|
|
117
|
+
expect(result).toHaveLength(2);
|
|
118
|
+
const jane = result.find((p) => p.name === "Jane");
|
|
119
|
+
expect(jane?.role).toBe("Founder");
|
|
120
|
+
expect(jane?.description).toBe("Bio");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("handles undefined groups", () => {
|
|
124
|
+
expect(mergePeople(undefined, [{ name: "A" }])).toEqual([{ name: "A" }]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildJsonFeed, buildRssFeed } from "../feed.ts";
|
|
3
|
+
|
|
4
|
+
const channel = {
|
|
5
|
+
title: "Test Feed",
|
|
6
|
+
url: "https://example.com",
|
|
7
|
+
description: "A test feed",
|
|
8
|
+
language: "de",
|
|
9
|
+
selfUrl: "https://example.com/feed.xml",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const items = [
|
|
13
|
+
{ title: "Item 1", url: "https://example.com/1", summary: "Summary 1", publishedAt: "2024-01-01" },
|
|
14
|
+
{ title: "Item 2", url: "https://example.com/2", summary: "Summary 2", publishedAt: "2024-06-01" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
describe("buildRssFeed", () => {
|
|
18
|
+
it("produces valid RSS XML", () => {
|
|
19
|
+
const result = buildRssFeed(channel, items);
|
|
20
|
+
expect(result).toContain('<?xml version="1.0"');
|
|
21
|
+
expect(result).toContain("<rss version=\"2.0\"");
|
|
22
|
+
expect(result).toContain("<channel>");
|
|
23
|
+
expect(result).toContain("<title>Test Feed</title>");
|
|
24
|
+
expect(result).toContain("<language>de</language>");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("sorts items by publishedAt descending", () => {
|
|
28
|
+
const result = buildRssFeed(channel, items);
|
|
29
|
+
const item1Idx = result.indexOf("Item 1");
|
|
30
|
+
const item2Idx = result.indexOf("Item 2");
|
|
31
|
+
expect(item2Idx).toBeLessThan(item1Idx);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("escapes XML special characters", () => {
|
|
35
|
+
const result = buildRssFeed(channel, [
|
|
36
|
+
{ title: "A & B <C>", url: "https://example.com/x", summary: "S", publishedAt: "2024-01-01" },
|
|
37
|
+
]);
|
|
38
|
+
expect(result).toContain("A & B <C>");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("includes atom self link", () => {
|
|
42
|
+
const result = buildRssFeed(channel, items);
|
|
43
|
+
expect(result).toContain('href="https://example.com/feed.xml"');
|
|
44
|
+
expect(result).toContain('rel="self"');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("derives lastBuildDate from latest item", () => {
|
|
48
|
+
const result = buildRssFeed(channel, items);
|
|
49
|
+
expect(result).toContain("<lastBuildDate>");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("omits lastBuildDate for empty feed", () => {
|
|
53
|
+
const result = buildRssFeed(channel, []);
|
|
54
|
+
expect(result).not.toContain("<lastBuildDate>");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("buildJsonFeed", () => {
|
|
59
|
+
it("produces JSON Feed v1.1", () => {
|
|
60
|
+
const result = buildJsonFeed({
|
|
61
|
+
title: "Test Feed",
|
|
62
|
+
home_page_url: "https://example.com",
|
|
63
|
+
feed_url: "https://example.com/feed.json",
|
|
64
|
+
description: "A test feed",
|
|
65
|
+
language: "de",
|
|
66
|
+
items,
|
|
67
|
+
});
|
|
68
|
+
const parsed = JSON.parse(result);
|
|
69
|
+
expect(parsed.version).toBe("https://jsonfeed.org/version/1.1");
|
|
70
|
+
expect(parsed.title).toBe("Test Feed");
|
|
71
|
+
expect(parsed.items).toHaveLength(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("sorts items by publishedAt descending", () => {
|
|
75
|
+
const result = buildJsonFeed({
|
|
76
|
+
title: "Test",
|
|
77
|
+
home_page_url: "https://example.com",
|
|
78
|
+
feed_url: "https://example.com/feed.json",
|
|
79
|
+
description: "",
|
|
80
|
+
language: "de",
|
|
81
|
+
items,
|
|
82
|
+
});
|
|
83
|
+
const parsed = JSON.parse(result);
|
|
84
|
+
expect(parsed.items[0].title).toBe("Item 2");
|
|
85
|
+
expect(parsed.items[1].title).toBe("Item 1");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("includes date_modified when updatedAt provided", () => {
|
|
89
|
+
const result = buildJsonFeed({
|
|
90
|
+
title: "Test",
|
|
91
|
+
home_page_url: "https://example.com",
|
|
92
|
+
feed_url: "https://example.com/feed.json",
|
|
93
|
+
description: "",
|
|
94
|
+
language: "de",
|
|
95
|
+
items: [{ title: "T", url: "https://example.com/1", summary: "S", publishedAt: "2024-01-01", updatedAt: "2024-06-01" }],
|
|
96
|
+
});
|
|
97
|
+
const parsed = JSON.parse(result);
|
|
98
|
+
expect(parsed.items[0].date_modified).toBe("2024-06-01");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("omits items array for empty feed", () => {
|
|
102
|
+
const result = buildJsonFeed({
|
|
103
|
+
title: "Test",
|
|
104
|
+
home_page_url: "https://example.com",
|
|
105
|
+
feed_url: "https://example.com/feed.json",
|
|
106
|
+
description: "",
|
|
107
|
+
language: "de",
|
|
108
|
+
items: [],
|
|
109
|
+
});
|
|
110
|
+
const parsed = JSON.parse(result);
|
|
111
|
+
expect(parsed.items).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SemanticOrganization, SemanticPageModel, SemanticPageType } from "../models.ts";
|
|
2
|
+
|
|
3
|
+
export function makeOrganization(
|
|
4
|
+
overrides: Partial<SemanticOrganization> = {},
|
|
5
|
+
): SemanticOrganization {
|
|
6
|
+
return {
|
|
7
|
+
name: "Warpgogol",
|
|
8
|
+
description: "Digital infrastructure for nonprofits",
|
|
9
|
+
url: "https://example.com",
|
|
10
|
+
...overrides,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function makePage(
|
|
15
|
+
overrides: Partial<SemanticPageModel> & { type?: SemanticPageType } = {},
|
|
16
|
+
): SemanticPageModel {
|
|
17
|
+
return {
|
|
18
|
+
type: overrides.type ?? "content",
|
|
19
|
+
lang: "de",
|
|
20
|
+
url: "https://example.com/de/page/",
|
|
21
|
+
title: "Test Page",
|
|
22
|
+
description: "A test page description",
|
|
23
|
+
heading: "Test Heading",
|
|
24
|
+
breadcrumbs: [
|
|
25
|
+
{ name: "Home", url: "https://example.com/de/" },
|
|
26
|
+
{ name: "Test Page", url: "https://example.com/de/page/" },
|
|
27
|
+
],
|
|
28
|
+
blocks: [],
|
|
29
|
+
organization: makeOrganization(),
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
canonicalRootUrl,
|
|
4
|
+
createSemanticIds,
|
|
5
|
+
getBaseUrl,
|
|
6
|
+
markdownTwinRelPath,
|
|
7
|
+
markdownTwinUrlPath,
|
|
8
|
+
toAbsoluteUrl,
|
|
9
|
+
toCanonicalUrl,
|
|
10
|
+
toPathname,
|
|
11
|
+
} from "../ids.ts";
|
|
12
|
+
|
|
13
|
+
describe("getBaseUrl", () => {
|
|
14
|
+
it("returns origin from string URL", () => {
|
|
15
|
+
expect(getBaseUrl("https://example.com/de/page")).toBe("https://example.com");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns origin from URL object", () => {
|
|
19
|
+
expect(getBaseUrl(new URL("https://example.com/de/page?q=1"))).toBe("https://example.com");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("toCanonicalUrl", () => {
|
|
24
|
+
it("strips search and hash", () => {
|
|
25
|
+
expect(toCanonicalUrl("https://example.com/de/page?q=1#top")).toBe(
|
|
26
|
+
"https://example.com/de/page",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("toAbsoluteUrl", () => {
|
|
32
|
+
it("resolves relative path against base", () => {
|
|
33
|
+
expect(toAbsoluteUrl("https://example.com", "/de/page")).toBe("https://example.com/de/page");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("canonicalRootUrl", () => {
|
|
38
|
+
it("returns unprefixed root URL", () => {
|
|
39
|
+
expect(canonicalRootUrl("https://example.com")).toBe("https://example.com/");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("toPathname", () => {
|
|
44
|
+
it("extracts pathname from URL string", () => {
|
|
45
|
+
expect(toPathname("https://example.com/de/page?q=1")).toBe("/de/page");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("markdownTwinRelPath", () => {
|
|
50
|
+
const opts = { supportedLangs: ["de", "uk"] };
|
|
51
|
+
|
|
52
|
+
it("returns index.md for root path", () => {
|
|
53
|
+
expect(markdownTwinRelPath("/", opts)).toBe("index.md");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns lang-root index for language root", () => {
|
|
57
|
+
expect(markdownTwinRelPath("/de/", opts)).toBe("de/index.md");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns nested path as .md file", () => {
|
|
61
|
+
expect(markdownTwinRelPath("/de/about/", opts)).toBe("de/about.md");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("markdownTwinUrlPath", () => {
|
|
66
|
+
it("prefixes rel path with slash", () => {
|
|
67
|
+
expect(markdownTwinUrlPath("/de/about/", { supportedLangs: ["de"] })).toBe("/de/about.md");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("createSemanticIds", () => {
|
|
72
|
+
const ids = createSemanticIds("https://example.com");
|
|
73
|
+
|
|
74
|
+
it("generates organization id", () => {
|
|
75
|
+
expect(ids.organization).toBe("https://example.com/#/schema/organization");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("generates website id", () => {
|
|
79
|
+
expect(ids.website).toBe("https://example.com/#/schema/website");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("generates webpage id from page URL", () => {
|
|
83
|
+
expect(ids.webpage("https://example.com/de/page")).toBe(
|
|
84
|
+
"https://example.com/de/page#/schema/webpage",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("generates breadcrumb id from page URL", () => {
|
|
89
|
+
expect(ids.breadcrumb("https://example.com/de/page")).toBe(
|
|
90
|
+
"https://example.com/de/page#/schema/breadcrumb",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("generates person id with slugified name", () => {
|
|
95
|
+
expect(ids.person("Jane Doe")).toBe("https://example.com/#/schema/person/jane-doe");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("generates initiative id with slugified name", () => {
|
|
99
|
+
expect(ids.initiative("Project Alpha")).toBe(
|
|
100
|
+
"https://example.com/#/schema/initiative/project-alpha",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("generates service id with slugified name", () => {
|
|
105
|
+
expect(ids.service("Web Hosting")).toBe("https://example.com/#/schema/service/web-hosting");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("generates faq id from page URL", () => {
|
|
109
|
+
expect(ids.faq("https://example.com/de/page")).toBe(
|
|
110
|
+
"https://example.com/de/page#/schema/faq",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("strips trailing slash from baseUrl", () => {
|
|
115
|
+
const ids2 = createSemanticIds("https://example.com/");
|
|
116
|
+
expect(ids2.organization).toBe("https://example.com/#/schema/organization");
|
|
117
|
+
});
|
|
118
|
+
});
|