@se-studio/site-check 1.4.0 → 1.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Add `smoke-test-one` CLI bin for consumer repos (`smoke-test-one <port>` from app directory).
8
+
9
+ ## 1.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Add `@se-studio/site-check/smoke-test` for curated local marketing-site smoke tests (build + start, HTML + `.md` matrix from Contentful link getters).
14
+
3
15
  ## 1.4.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -92,6 +92,25 @@ Programmatic module for live production SEO audits (used by per-app `pnpm seo:au
92
92
 
93
93
  **Exit code:** `getProductionAuditExitCode` returns `1` when any row has `severity: 'error'` (including `markdown_missing`).
94
94
 
95
+ ## Smoke tests (`@se-studio/site-check/smoke-test`)
96
+
97
+ Curated local smoke tests for marketing sites: **`pnpm build` → `pnpm start`**, then verify HTML and `.md` for a structured URL matrix (home, sample pages, article types, articles, people, tags, listing shells).
98
+
99
+ Import from `@se-studio/site-check/smoke-test`:
100
+
101
+ - `buildSmokeConfig`, `planSmokeTestUrls`, `runSmokeTest`, `runFullSmokeTest`
102
+ - `formatSmokeTestReport`, `getSmokeTestExitCode`
103
+
104
+ **Per-app usage** (see `apps/example-empty`):
105
+
106
+ ```bash
107
+ pnpm build --filter @se-studio/site-check # once, or after package changes
108
+ cd apps/example-empty
109
+ pnpm smoke-test # build + start + smoke + stop
110
+ ```
111
+
112
+ Add `smoke.config.ts` (link getters + feature flags from the app) and `scripts/smoke-test-run.ts`. Set `SMOKE_TEST_IGNORE=true` in `.env.local` to skip. Listing-page `.md` URLs are included and will fail until listing markdown export is implemented in `markdown-renderer`.
113
+
95
114
  ## Exit codes
96
115
 
97
116
  - `0` — Validation passed (sitemap.xml required; llms.txt and sitemap-unindexed.xml optional). If `--check-rewrites` was set, no unexpected rewrites. Every sitemap page returned 200 for its `.md` URL and files were saved (or in compare mode: all production sitemap pages exist on development and, if `--check-rewrites`, no unexpected rewrites).
@@ -0,0 +1,4 @@
1
+ export { buildSmokeConfig, planSmokeTestUrls } from './planner.js';
2
+ export { formatSmokeTestReport, getSmokeTestExitCode, runFullSmokeTest, runSmokeTest, } from './runner.js';
3
+ export type { SmokeArticleLink, SmokeFailureReason, SmokeFeatureFlags, SmokeLinkGetters, SmokeTestCase, SmokeTestCaseResult, SmokeTestCategory, SmokeTestConfig, SmokeTestOverrides, SmokeTestPlannerConfig, SmokeTestResult, SmokeTestRunnerConfig, SmokeUrlCalculators, } from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/smoke-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,eAAe,EACf,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { buildSmokeConfig, planSmokeTestUrls } from './planner.js';
2
+ export { formatSmokeTestReport, getSmokeTestExitCode, runFullSmokeTest, runSmokeTest, } from './runner.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/smoke-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,GACb,MAAM,aAAa,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { SmokeTestCase, SmokeTestPlannerConfig } from './types.js';
2
+ export declare function planSmokeTestUrls(config: SmokeTestPlannerConfig): Promise<{
3
+ cases: SmokeTestCase[];
4
+ warnings: string[];
5
+ }>;
6
+ /** Assembles a planner config from app-specific deps (convenience for smoke.config.ts). */
7
+ export declare function buildSmokeConfig(config: Omit<SmokeTestPlannerConfig, 'baseUrl'> & {
8
+ port: number;
9
+ }): SmokeTestPlannerConfig;
10
+ //# sourceMappingURL=planner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/smoke-test/planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,aAAa,EAEb,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAyGpB,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC;IAC/E,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC,CA2ND;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GACjE,sBAAsB,CAMxB"}
@@ -0,0 +1,204 @@
1
+ const DEFAULT_SAMPLES = 2;
2
+ function isIndexedLink(link) {
3
+ return link.indexed !== false && link.hidden !== true;
4
+ }
5
+ function isExternalUrl(href) {
6
+ if (!href)
7
+ return false;
8
+ return href.startsWith('http://') || href.startsWith('https://');
9
+ }
10
+ function shouldIncludeArticle(link) {
11
+ if (!isIndexedLink(link))
12
+ return false;
13
+ if (link.articleType?.indexed === false || link.articleType?.hidden === true)
14
+ return false;
15
+ if (link.hasBodyContent)
16
+ return true;
17
+ if (link.href && isExternalUrl(link.href))
18
+ return false;
19
+ if (link.download)
20
+ return false;
21
+ return true;
22
+ }
23
+ function resolveArticlePageUrl(link, urlCalculators) {
24
+ if (link.href && !isExternalUrl(link.href))
25
+ return link.href;
26
+ const articleTypeSlug = link.articleType?.slug;
27
+ if (!articleTypeSlug || !link.slug)
28
+ return null;
29
+ const primaryTagSlug = link.primaryTag?.slug ?? link.tags?.[0]?.slug;
30
+ return urlCalculators.article(articleTypeSlug, link.slug, primaryTagSlug);
31
+ }
32
+ function joinBaseUrl(baseUrl, path) {
33
+ const normalizedBase = baseUrl.replace(/\/$/, '');
34
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
35
+ return `${normalizedBase}${normalizedPath}`;
36
+ }
37
+ function pickSamples(items, count, pick) {
38
+ return pick(items, count);
39
+ }
40
+ function pickFirst(items, count) {
41
+ return items.slice(0, count);
42
+ }
43
+ function pickPinnedOrFirst(items, count, pinnedSlugs, getSlug) {
44
+ if (!pinnedSlugs?.length)
45
+ return pickFirst(items, count);
46
+ const pinned = pinnedSlugs
47
+ .map((slug) => items.find((item) => getSlug(item) === slug))
48
+ .filter((item) => item != null);
49
+ const remaining = items.filter((item) => !pinnedSlugs.includes(getSlug(item) ?? ''));
50
+ return [...pinned, ...remaining].slice(0, count);
51
+ }
52
+ function addCase(cases, category, label, pageUrl, expectMarkdown, skipMarkdownPaths) {
53
+ const pathname = new URL(pageUrl).pathname;
54
+ const skipMarkdown = skipMarkdownPaths.some((prefix) => pathname.startsWith(prefix.startsWith('/') ? prefix : `/${prefix}`));
55
+ cases.push({
56
+ category,
57
+ label,
58
+ pageUrl,
59
+ expectMarkdown: expectMarkdown && !skipMarkdown,
60
+ });
61
+ }
62
+ function collectArticleTypeTagPairs(articleLinks) {
63
+ const pairs = [];
64
+ const seen = new Set();
65
+ for (const link of articleLinks) {
66
+ if (!shouldIncludeArticle(link))
67
+ continue;
68
+ const typeSlug = link.articleType?.slug;
69
+ if (!typeSlug || isExternalUrl(link.href))
70
+ continue;
71
+ for (const tag of link.tags ?? []) {
72
+ const tagSlug = tag.slug;
73
+ if (!tagSlug || tag.indexed === false || tag.hidden === true)
74
+ continue;
75
+ const key = `${typeSlug}\0${tagSlug}`;
76
+ if (seen.has(key))
77
+ continue;
78
+ seen.add(key);
79
+ pairs.push({ articleType: typeSlug, tag: tagSlug });
80
+ }
81
+ }
82
+ return pairs;
83
+ }
84
+ export async function planSmokeTestUrls(config) {
85
+ const { baseUrl, urlCalculators, linkGetters, flags, overrides = {}, samplesPerCategory = DEFAULT_SAMPLES, insufficientSamples = 'warn', } = config;
86
+ const skipCategories = new Set(overrides.skipCategories ?? []);
87
+ const skipMarkdownPaths = overrides.skipMarkdownPaths ?? [];
88
+ const cases = [];
89
+ const warnings = [];
90
+ const [pageRes, articleRes, articleTypeRes, tagRes, personRes] = await Promise.all([
91
+ linkGetters.getAllPageLinks(),
92
+ linkGetters.getAllArticleLinks(),
93
+ linkGetters.getAllArticleTypeLinks(),
94
+ linkGetters.getAllTagLinks(),
95
+ linkGetters.getAllPersonLinks(),
96
+ ]);
97
+ const warnInsufficient = (category, needed, found) => {
98
+ const message = `${category}: need ${needed} sample(s), found ${found}`;
99
+ if (insufficientSamples === 'fail') {
100
+ warnings.push(`ERROR: ${message}`);
101
+ }
102
+ else {
103
+ warnings.push(message);
104
+ }
105
+ };
106
+ if (!skipCategories.has('home')) {
107
+ addCase(cases, 'home', 'Home', joinBaseUrl(baseUrl, '/'), true, skipMarkdownPaths);
108
+ }
109
+ if (!skipCategories.has('page')) {
110
+ const pageLinks = pageRes.data.filter((link) => isIndexedLink(link) && !!link.href && link.href !== '/' && link.href !== '/index/');
111
+ const homeSlugCandidates = new Set(['', 'index', 'home']);
112
+ const nonHomePages = pageLinks.filter((link) => {
113
+ const slug = link.slug ?? link.href.replace(/^\/|\/$/g, '');
114
+ return !homeSlugCandidates.has(slug);
115
+ });
116
+ const selectedPages = pickSamples(nonHomePages, samplesPerCategory, (items, count) => pickPinnedOrFirst(items, count, overrides.pages, (link) => link.slug ?? link.href?.replace(/^\/|\/$/g, '')));
117
+ if (selectedPages.length < samplesPerCategory) {
118
+ warnInsufficient('page', samplesPerCategory, selectedPages.length);
119
+ }
120
+ for (const [index, link] of selectedPages.entries()) {
121
+ addCase(cases, 'page', `Page ${index + 1}`, joinBaseUrl(baseUrl, link.href), true, skipMarkdownPaths);
122
+ }
123
+ }
124
+ if (!skipCategories.has('articles-index') && flags.enableArticleTypeIndex && flags.articlesSlug) {
125
+ addCase(cases, 'articles-index', 'Articles index', joinBaseUrl(baseUrl, urlCalculators.customType(flags.articlesSlug)), true, skipMarkdownPaths);
126
+ }
127
+ const articleTypeLinks = articleTypeRes.data.filter((type) => isIndexedLink(type) && !!type.slug);
128
+ for (const articleType of articleTypeLinks) {
129
+ const typeSlug = articleType.slug;
130
+ const typeLabel = articleType.slug;
131
+ if (!skipCategories.has('article-type-index')) {
132
+ const indexUrl = articleType.href ?? urlCalculators.articleType(typeSlug);
133
+ addCase(cases, 'article-type-index', `Article type index: ${typeLabel}`, joinBaseUrl(baseUrl, indexUrl), true, skipMarkdownPaths);
134
+ }
135
+ if (!skipCategories.has('article')) {
136
+ const articlesForType = articleRes.data.filter((link) => link.articleType?.slug === typeSlug && shouldIncludeArticle(link));
137
+ const articleUrls = articlesForType
138
+ .map((link) => resolveArticlePageUrl(link, urlCalculators))
139
+ .filter((url) => !!url);
140
+ const uniqueUrls = [...new Set(articleUrls)];
141
+ const selectedArticles = uniqueUrls.slice(0, samplesPerCategory);
142
+ if (selectedArticles.length < samplesPerCategory) {
143
+ warnInsufficient(`article (${typeLabel})`, samplesPerCategory, selectedArticles.length);
144
+ }
145
+ for (const [index, url] of selectedArticles.entries()) {
146
+ addCase(cases, 'article', `Article ${index + 1} (${typeLabel})`, joinBaseUrl(baseUrl, url), true, skipMarkdownPaths);
147
+ }
148
+ }
149
+ }
150
+ if (!skipCategories.has('tags-index') && flags.enableTagsIndex) {
151
+ addCase(cases, 'tags-index', 'Tags index', joinBaseUrl(baseUrl, urlCalculators.customType(flags.tagsSlug)), true, skipMarkdownPaths);
152
+ }
153
+ if (!skipCategories.has('tag') && flags.enableTag) {
154
+ const tagLinks = tagRes.data.filter((link) => isIndexedLink(link) && !!link.href);
155
+ const selectedTags = pickFirst(tagLinks, samplesPerCategory);
156
+ if (selectedTags.length < samplesPerCategory) {
157
+ warnInsufficient('tag', samplesPerCategory, selectedTags.length);
158
+ }
159
+ for (const [index, link] of selectedTags.entries()) {
160
+ addCase(cases, 'tag', `Tag ${index + 1}`, joinBaseUrl(baseUrl, link.href), true, skipMarkdownPaths);
161
+ }
162
+ }
163
+ if (!skipCategories.has('article-type-tag-listing') && flags.enableArticleTypeTagIndex) {
164
+ const pairs = collectArticleTypeTagPairs(articleRes.data);
165
+ const selectedPairs = overrides.articleTypeTags?.length
166
+ ? overrides.articleTypeTags
167
+ : pairs.slice(0, samplesPerCategory);
168
+ if (selectedPairs.length < samplesPerCategory) {
169
+ warnInsufficient('article-type-tag-listing', samplesPerCategory, selectedPairs.length);
170
+ }
171
+ for (const [index, pair] of selectedPairs.entries()) {
172
+ const url = urlCalculators.articleTypeTag(pair.articleType, pair.tag);
173
+ addCase(cases, 'article-type-tag-listing', `Article-type tag listing ${index + 1} (${pair.articleType}/${pair.tag})`, joinBaseUrl(baseUrl, url), true, skipMarkdownPaths);
174
+ }
175
+ }
176
+ if (!skipCategories.has('people-listing') && flags.enablePeopleIndex) {
177
+ addCase(cases, 'people-listing', 'People listing', joinBaseUrl(baseUrl, urlCalculators.team()), true, skipMarkdownPaths);
178
+ }
179
+ if (!skipCategories.has('person') && flags.enablePerson) {
180
+ const people = personRes.data.filter((person) => isIndexedLink(person) && !!person.slug);
181
+ const selectedPeople = pickFirst(people, samplesPerCategory);
182
+ if (selectedPeople.length < samplesPerCategory) {
183
+ warnInsufficient('person', samplesPerCategory, selectedPeople.length);
184
+ }
185
+ for (const [index, person] of selectedPeople.entries()) {
186
+ addCase(cases, 'person', `Person ${index + 1}`, joinBaseUrl(baseUrl, urlCalculators.person(person.slug)), true, skipMarkdownPaths);
187
+ }
188
+ }
189
+ if (!skipCategories.has('custom-type-listing') && overrides.customTypeListings?.length) {
190
+ for (const slug of overrides.customTypeListings) {
191
+ addCase(cases, 'custom-type-listing', `Custom type listing: ${slug}`, joinBaseUrl(baseUrl, urlCalculators.customType(slug)), true, skipMarkdownPaths);
192
+ }
193
+ }
194
+ return { cases, warnings };
195
+ }
196
+ /** Assembles a planner config from app-specific deps (convenience for smoke.config.ts). */
197
+ export function buildSmokeConfig(config) {
198
+ const { port, ...rest } = config;
199
+ return {
200
+ ...rest,
201
+ baseUrl: `http://localhost:${port}`,
202
+ };
203
+ }
204
+ //# sourceMappingURL=planner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.js","sourceRoot":"","sources":["../../src/smoke-test/planner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /** biome-ignore-all lint/suspicious/noConsole: CLI output is intentional */
2
+ import type { SmokeTestResult, SmokeTestRunnerConfig } from './types.js';
3
+ export declare function runSmokeTest(config: SmokeTestRunnerConfig): Promise<SmokeTestResult>;
4
+ export declare function getSmokeTestExitCode(result: SmokeTestResult): number;
5
+ export declare function formatSmokeTestReport(result: SmokeTestResult): string;
6
+ export declare function runFullSmokeTest(plannerConfig: import('./types.js').SmokeTestPlannerConfig & {
7
+ minMarkdownLength?: number;
8
+ requestDelayMs?: number;
9
+ fetch?: typeof fetch;
10
+ }): Promise<SmokeTestResult>;
11
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/smoke-test/runner.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAG5E,OAAO,KAAK,EAIV,eAAe,EACf,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAuEpB,wBAAsB,YAAY,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqC1F;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAIpE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CA6CrE;AAED,wBAAsB,gBAAgB,CACpC,aAAa,EAAE,OAAO,YAAY,EAAE,sBAAsB,GAAG;IAC3D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB,GACA,OAAO,CAAC,eAAe,CAAC,CAkB1B"}
@@ -0,0 +1,153 @@
1
+ /** biome-ignore-all lint/suspicious/noConsole: CLI output is intentional */
2
+ import { htmlUrlToMarkdownUrl } from '../production-audit/index.js';
3
+ const DEFAULT_MIN_MARKDOWN_LENGTH = 50;
4
+ function sleep(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+ function isMarkdownContentType(contentType) {
8
+ if (!contentType)
9
+ return false;
10
+ return contentType.toLowerCase().includes('text/markdown');
11
+ }
12
+ async function checkCase(testCase, baseUrl, minMarkdownLength, fetchFn) {
13
+ const failures = [];
14
+ let htmlStatus = 0;
15
+ let markdownStatus = 0;
16
+ let markdownLength = 0;
17
+ let markdownContentType = null;
18
+ let markdownBodySnippet = null;
19
+ try {
20
+ const htmlRes = await fetchFn(testCase.pageUrl, { method: 'GET', redirect: 'follow' });
21
+ htmlStatus = htmlRes.status;
22
+ if (htmlStatus < 200 || htmlStatus >= 300) {
23
+ failures.push('html_non_2xx');
24
+ }
25
+ }
26
+ catch {
27
+ htmlStatus = 0;
28
+ failures.push('html_non_2xx');
29
+ }
30
+ if (testCase.expectMarkdown) {
31
+ const mdUrl = htmlUrlToMarkdownUrl(testCase.pageUrl);
32
+ try {
33
+ const mdRes = await fetchFn(mdUrl, { method: 'GET', redirect: 'follow' });
34
+ markdownStatus = mdRes.status;
35
+ markdownContentType = mdRes.headers.get('content-type');
36
+ const body = await mdRes.text();
37
+ markdownLength = body.length;
38
+ markdownBodySnippet = body.slice(0, 120).replace(/\s+/g, ' ').trim();
39
+ if (markdownStatus < 200 || markdownStatus >= 300) {
40
+ failures.push('markdown_missing');
41
+ }
42
+ else if (!isMarkdownContentType(markdownContentType)) {
43
+ failures.push('markdown_wrong_content_type');
44
+ }
45
+ else if (markdownLength < minMarkdownLength) {
46
+ failures.push('markdown_too_short');
47
+ }
48
+ }
49
+ catch {
50
+ markdownStatus = 0;
51
+ failures.push('markdown_missing');
52
+ }
53
+ }
54
+ return {
55
+ case: testCase,
56
+ htmlStatus,
57
+ markdownStatus,
58
+ markdownLength,
59
+ markdownContentType,
60
+ failures,
61
+ markdownBodySnippet,
62
+ };
63
+ }
64
+ export async function runSmokeTest(config) {
65
+ const { baseUrl, siteName, cases, minMarkdownLength = DEFAULT_MIN_MARKDOWN_LENGTH, requestDelayMs = 0, fetch: fetchFn = fetch, } = config;
66
+ const results = [];
67
+ const warnings = [];
68
+ for (const testCase of cases) {
69
+ if (requestDelayMs > 0 && results.length > 0) {
70
+ await sleep(requestDelayMs);
71
+ }
72
+ results.push(await checkCase(testCase, baseUrl, minMarkdownLength, fetchFn));
73
+ }
74
+ let errorCount = 0;
75
+ const warnCount = 0;
76
+ for (const result of results) {
77
+ if (result.failures.length > 0) {
78
+ errorCount++;
79
+ }
80
+ }
81
+ return {
82
+ siteName,
83
+ baseUrl,
84
+ cases: results,
85
+ warnings,
86
+ errorCount,
87
+ warnCount,
88
+ };
89
+ }
90
+ export function getSmokeTestExitCode(result) {
91
+ const insufficientErrors = result.warnings.filter((w) => w.startsWith('ERROR:')).length;
92
+ if (result.errorCount > 0 || insufficientErrors > 0)
93
+ return 1;
94
+ return 0;
95
+ }
96
+ export function formatSmokeTestReport(result) {
97
+ const lines = [];
98
+ lines.push(`Smoke test: ${result.siteName} (${result.baseUrl})`);
99
+ lines.push('');
100
+ const byCategory = new Map();
101
+ for (const row of result.cases) {
102
+ const key = row.case.category;
103
+ const group = byCategory.get(key) ?? [];
104
+ group.push(row);
105
+ byCategory.set(key, group);
106
+ }
107
+ for (const [category, rows] of byCategory) {
108
+ lines.push(`## ${category}`);
109
+ for (const row of rows) {
110
+ const status = row.failures.length === 0 ? 'ok' : `FAIL (${row.failures.join(', ')})`;
111
+ lines.push(` ${row.case.label}`);
112
+ lines.push(` page: ${row.case.pageUrl} → HTML ${row.htmlStatus} [${status}]`);
113
+ if (row.case.expectMarkdown) {
114
+ const mdUrl = htmlUrlToMarkdownUrl(row.case.pageUrl);
115
+ lines.push(` md: ${mdUrl} → ${row.markdownStatus} (${row.markdownLength} chars) [${status}]`);
116
+ if (row.failures.includes('markdown_missing') && row.markdownBodySnippet) {
117
+ lines.push(` md body: ${row.markdownBodySnippet}`);
118
+ }
119
+ }
120
+ }
121
+ lines.push('');
122
+ }
123
+ if (result.warnings.length > 0) {
124
+ lines.push('## Warnings');
125
+ for (const warning of result.warnings) {
126
+ lines.push(` - ${warning}`);
127
+ }
128
+ lines.push('');
129
+ }
130
+ lines.push(`Summary: ${result.cases.length} cases, ${result.errorCount} failed, ${result.warnings.length} warnings`);
131
+ return lines.join('\n');
132
+ }
133
+ export async function runFullSmokeTest(plannerConfig) {
134
+ const { planSmokeTestUrls } = await import('./planner.js');
135
+ const { cases, warnings: plannerWarnings } = await planSmokeTestUrls(plannerConfig);
136
+ const result = await runSmokeTest({
137
+ baseUrl: plannerConfig.baseUrl,
138
+ siteName: plannerConfig.siteName,
139
+ cases,
140
+ minMarkdownLength: plannerConfig.minMarkdownLength,
141
+ requestDelayMs: plannerConfig.requestDelayMs,
142
+ fetch: plannerConfig.fetch,
143
+ });
144
+ result.warnings = [...plannerWarnings, ...result.warnings];
145
+ if (plannerWarnings.some((w) => w.startsWith('ERROR:'))) {
146
+ result.warnCount += plannerWarnings.filter((w) => w.startsWith('ERROR:')).length;
147
+ }
148
+ else {
149
+ result.warnCount += plannerWarnings.length;
150
+ }
151
+ return result;
152
+ }
153
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/smoke-test/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ export type SmokeTestCategory = 'home' | 'page' | 'article-type-index' | 'article' | 'person' | 'people-listing' | 'tag' | 'article-type-tag-listing' | 'tags-index' | 'articles-index' | 'custom-type-listing';
2
+ export interface SmokeTestCase {
3
+ category: SmokeTestCategory;
4
+ label: string;
5
+ pageUrl: string;
6
+ expectMarkdown: boolean;
7
+ }
8
+ export interface SmokeUrlCalculators {
9
+ page: (slug: string) => string;
10
+ article: (articleTypeSlug: string, slug: string, primaryTagSlug?: string) => string;
11
+ articleType: (slug: string) => string;
12
+ articleTypeTag: (articleTypeSlug: string, tagSlug: string) => string;
13
+ tag: (slug: string) => string;
14
+ person: (slug: string) => string;
15
+ team: () => string;
16
+ customType: (slug: string) => string;
17
+ }
18
+ export interface SmokeLinkWithHref {
19
+ href?: string | null;
20
+ slug?: string | null;
21
+ indexed?: boolean | null;
22
+ hidden?: boolean | null;
23
+ }
24
+ export interface SmokeArticleTypeRef {
25
+ slug?: string;
26
+ indexed?: boolean | null;
27
+ hidden?: boolean | null;
28
+ }
29
+ export interface SmokeTagRef {
30
+ slug?: string;
31
+ indexed?: boolean | null;
32
+ hidden?: boolean | null;
33
+ }
34
+ export interface SmokeArticleLink extends SmokeLinkWithHref {
35
+ slug?: string | null;
36
+ articleType?: SmokeArticleTypeRef | null;
37
+ tags?: SmokeTagRef[] | null;
38
+ primaryTag?: SmokeTagRef | null;
39
+ hasBodyContent?: boolean;
40
+ download?: unknown;
41
+ }
42
+ export interface SmokeArticleTypeLink extends SmokeLinkWithHref {
43
+ slug: string;
44
+ }
45
+ export interface SmokePersonLink extends SmokeLinkWithHref {
46
+ slug: string;
47
+ }
48
+ export interface SmokeLinkGetters {
49
+ getAllPageLinks: () => Promise<{
50
+ data: SmokeLinkWithHref[];
51
+ }>;
52
+ getAllArticleLinks: () => Promise<{
53
+ data: SmokeArticleLink[];
54
+ }>;
55
+ getAllArticleTypeLinks: () => Promise<{
56
+ data: SmokeArticleTypeLink[];
57
+ }>;
58
+ getAllTagLinks: () => Promise<{
59
+ data: SmokeLinkWithHref[];
60
+ }>;
61
+ getAllPersonLinks: () => Promise<{
62
+ data: SmokePersonLink[];
63
+ }>;
64
+ }
65
+ export interface SmokeFeatureFlags {
66
+ articlesSlug?: string;
67
+ tagsSlug: string;
68
+ peopleSlug?: string;
69
+ enableArticleTypeIndex: boolean;
70
+ enableArticleTypeTagIndex: boolean;
71
+ enablePeopleIndex: boolean;
72
+ enablePerson: boolean;
73
+ enableTag: boolean;
74
+ enableTagsIndex: boolean;
75
+ }
76
+ export interface SmokeTestOverrides {
77
+ pages?: string[];
78
+ articleTypeTags?: Array<{
79
+ articleType: string;
80
+ tag: string;
81
+ }>;
82
+ customTypeListings?: string[];
83
+ skipCategories?: SmokeTestCategory[];
84
+ skipMarkdownPaths?: string[];
85
+ }
86
+ export interface SmokeTestPlannerConfig {
87
+ siteName: string;
88
+ baseUrl: string;
89
+ urlCalculators: SmokeUrlCalculators;
90
+ linkGetters: SmokeLinkGetters;
91
+ flags: SmokeFeatureFlags;
92
+ overrides?: SmokeTestOverrides;
93
+ samplesPerCategory?: number;
94
+ insufficientSamples?: 'warn' | 'fail';
95
+ }
96
+ export type SmokeFailureReason = 'html_non_2xx' | 'markdown_missing' | 'markdown_wrong_content_type' | 'markdown_too_short' | 'insufficient_samples';
97
+ export interface SmokeTestCaseResult {
98
+ case: SmokeTestCase;
99
+ htmlStatus: number;
100
+ markdownStatus: number;
101
+ markdownLength: number;
102
+ markdownContentType: string | null;
103
+ failures: SmokeFailureReason[];
104
+ markdownBodySnippet: string | null;
105
+ }
106
+ export interface SmokeTestResult {
107
+ siteName: string;
108
+ baseUrl: string;
109
+ cases: SmokeTestCaseResult[];
110
+ warnings: string[];
111
+ errorCount: number;
112
+ warnCount: number;
113
+ }
114
+ export interface SmokeTestRunnerConfig {
115
+ baseUrl: string;
116
+ siteName: string;
117
+ cases: SmokeTestCase[];
118
+ minMarkdownLength?: number;
119
+ requestDelayMs?: number;
120
+ fetch?: typeof fetch;
121
+ }
122
+ export interface SmokeTestConfig extends SmokeTestPlannerConfig {
123
+ minMarkdownLength?: number;
124
+ requestDelayMs?: number;
125
+ }
126
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/smoke-test/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/smoke-test/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@se-studio/site-check",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Validate SE marketing sites (sitemap, llms.txt) and download markdown files preserving structure",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,15 +23,21 @@
23
23
  "./production-audit": {
24
24
  "types": "./dist/production-audit/index.d.ts",
25
25
  "import": "./dist/production-audit/index.js"
26
+ },
27
+ "./smoke-test": {
28
+ "types": "./dist/smoke-test/index.d.ts",
29
+ "import": "./dist/smoke-test/index.js"
26
30
  }
27
31
  },
28
32
  "bin": {
29
33
  "site-check": "./dist/cli.js",
30
- "site-check-screaming-frog": "./dist/screaming-frog-cli.js"
34
+ "site-check-screaming-frog": "./dist/screaming-frog-cli.js",
35
+ "smoke-test-one": "./smoke-test-one.mjs"
31
36
  },
32
37
  "files": [
33
38
  "dist",
34
- "*.md"
39
+ "*.md",
40
+ "smoke-test-one.mjs"
35
41
  ],
36
42
  "keywords": [
37
43
  "sitemap",
@@ -50,7 +56,7 @@
50
56
  },
51
57
  "devDependencies": {
52
58
  "@biomejs/biome": "^2.4.16",
53
- "@types/node": "^24.13.1",
59
+ "@types/node": "^24.13.2",
54
60
  "typescript": "^6.0.3"
55
61
  },
56
62
  "scripts": {
@@ -59,6 +65,6 @@
59
65
  "type-check": "tsc --noEmit",
60
66
  "lint": "biome lint .",
61
67
  "clean": "rm -rf dist .turbo *.tsbuildinfo",
62
- "test": "pnpm build && node --test scripts/screaming-frog-audit.test.mjs scripts/production-audit.test.mjs"
68
+ "test": "pnpm build && node --test scripts/screaming-frog-audit.test.mjs scripts/production-audit.test.mjs scripts/smoke-test.test.mjs"
63
69
  }
64
70
  }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Build, start, and smoke-test one marketing app.
5
+ * Usage: smoke-test-one <port> (from app directory)
6
+ * Skips when SMOKE_TEST_IGNORE=true in .env.local.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { spawn, spawnSync } from 'node:child_process';
12
+
13
+ const port = process.argv[2];
14
+ if (!port || !/^\d+$/.test(port)) {
15
+ console.error('Usage: smoke-test-one <port>');
16
+ process.exit(1);
17
+ }
18
+
19
+ const appDir = process.cwd();
20
+ const baseUrl = `http://localhost:${port}`;
21
+ const startTimeoutMs = 120_000;
22
+ const pollIntervalMs = 1_000;
23
+
24
+ function parseEnvFile(filePath) {
25
+ const env = {};
26
+ if (!fs.existsSync(filePath)) return env;
27
+ const content = fs.readFileSync(filePath, 'utf8');
28
+ for (const line of content.split('\n')) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed || trimmed.startsWith('#')) continue;
31
+ const eq = trimmed.indexOf('=');
32
+ if (eq === -1) continue;
33
+ const key = trimmed.slice(0, eq).trim();
34
+ let value = trimmed.slice(eq + 1).trim();
35
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
36
+ if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
37
+ env[key] = value;
38
+ }
39
+ return env;
40
+ }
41
+
42
+ function loadEnv() {
43
+ const envLocal = parseEnvFile(path.join(appDir, '.env.local'));
44
+ const envExample = parseEnvFile(path.join(appDir, '.env.example'));
45
+ return { ...process.env, ...envExample, ...envLocal, PORT: port };
46
+ }
47
+
48
+ async function waitForServer(url) {
49
+ const deadline = Date.now() + startTimeoutMs;
50
+ while (Date.now() < deadline) {
51
+ try {
52
+ const res = await fetch(url, { method: 'GET', redirect: 'follow' });
53
+ if (res.status >= 200 && res.status < 500) {
54
+ return;
55
+ }
56
+ } catch {
57
+ // Server not ready yet
58
+ }
59
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
60
+ }
61
+ throw new Error(`Server did not respond at ${url} within ${startTimeoutMs}ms`);
62
+ }
63
+
64
+ function killProcessTree(child) {
65
+ if (!child?.pid) return;
66
+ try {
67
+ process.kill(-child.pid, 'SIGTERM');
68
+ } catch {
69
+ try {
70
+ child.kill('SIGTERM');
71
+ } catch {
72
+ // Already exited
73
+ }
74
+ }
75
+ }
76
+
77
+ const env = loadEnv();
78
+
79
+ if ((env.SMOKE_TEST_IGNORE ?? '').toLowerCase() === 'true') {
80
+ console.log('Skipped (SMOKE_TEST_IGNORE=true).');
81
+ process.exit(0);
82
+ }
83
+
84
+ console.log('Building app...');
85
+ const buildResult = spawnSync('pnpm', ['build'], {
86
+ cwd: appDir,
87
+ stdio: 'inherit',
88
+ env,
89
+ });
90
+ if (buildResult.status !== 0) {
91
+ process.exit(buildResult.status ?? 1);
92
+ }
93
+
94
+ console.log(`Starting server on ${baseUrl}...`);
95
+ const server = spawn('pnpm', ['start'], {
96
+ cwd: appDir,
97
+ env,
98
+ stdio: 'inherit',
99
+ detached: true,
100
+ });
101
+
102
+ let exitCode = 1;
103
+
104
+ try {
105
+ await waitForServer(baseUrl);
106
+ console.log('Server ready. Running smoke tests...');
107
+
108
+ const runResult = spawnSync('pnpm', ['smoke-test:run'], {
109
+ cwd: appDir,
110
+ stdio: 'inherit',
111
+ env: { ...env, SMOKE_TEST_PORT: port },
112
+ });
113
+ exitCode = runResult.status ?? 1;
114
+ } catch (error) {
115
+ console.error(error instanceof Error ? error.message : error);
116
+ exitCode = 1;
117
+ } finally {
118
+ killProcessTree(server);
119
+ }
120
+
121
+ process.exit(exitCode);