49c22c9385
Clean re-implementation of the full product (not a port of the old M3 tree): - P0 foundation: 13-table schema + per-call sqlite factory (thread-safe), FastAPI app (health, JSON logging + secret redaction, CORS, startup guard). - P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure, png crop/inventory/overlay, EXIF decodeOrientedBitmap. - P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop to batch) + photos-ready lineup. - P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing, inference Call1/Call2, review, publish, settings, slabs, photos) + real FE review/publish/settings screens consuming the API (no hardcoded Woo ids). - P4 gate exit: check_requirements + check_coverage both exit 0 (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green. Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { compile, preprocess } from 'svelte/compiler';
|
|
import sp from 'svelte-preprocess';
|
|
import { draft } from './lib/types';
|
|
import { computeSqft, computeBdft, sampleWidths } from './lib/measure';
|
|
import { buildTitle as buildCopyTitle } from './review/copy';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const tsPreprocessor = { script: sp.typescript().script };
|
|
|
|
async function compileComponent(rel: string) {
|
|
const code = readFileSync(join(here, rel), 'utf8');
|
|
const processed = await preprocess(code, [tsPreprocessor], { filename: rel });
|
|
return compile(processed.code, { filename: rel, generate: 'client' });
|
|
}
|
|
|
|
// Each fresh component must compile clean (no warnings) — this catches Svelte
|
|
// template errors that tsc cannot see (e.g. binding to a reactive statement).
|
|
for (const rel of [
|
|
'./App.svelte',
|
|
'./capture/PhotoImport.svelte',
|
|
'./measure/MeasureInputs.svelte',
|
|
'./review/ReviewForm.svelte',
|
|
'./publish/PublishPanel.svelte',
|
|
'./settings/SettingsPanel.svelte'
|
|
]) {
|
|
test(`compiles ${rel}`, async () => {
|
|
const { warnings } = await compileComponent(rel);
|
|
assert.deepStrictEqual(warnings, [], warnings.map((w) => w.code + ': ' + w.message).join('; '));
|
|
});
|
|
}
|
|
|
|
test('auto title derives from the primary wood-category leaf (live edge vs dimensional)', () => {
|
|
const facts = { lengthIn: 48, widthAvgIn: 18, thicknessIn: 2, bdft: 12 };
|
|
assert.strictEqual(
|
|
buildCopyTitle(facts, 'Figured', 'Black Walnut', { slug: 'live-edge-wood', name: 'Live edge wood' }),
|
|
'Figured Black Walnut Live Edge Slab',
|
|
);
|
|
assert.strictEqual(
|
|
buildCopyTitle(facts, 'Plain', 'Red Oak', { slug: 'dimensional-wood', name: 'Dimensional wood' }),
|
|
'Plain Red Oak Dimensional',
|
|
);
|
|
});
|
|
|
|
test('a fresh draft reaches a ready, publishable state deterministically', () => {
|
|
// The full local workflow: capture -> measure -> review -> ready.
|
|
const slab = draft({
|
|
lengthIn: 96,
|
|
thicknessIn: 1.5,
|
|
sqft: computeSqft(921600, 100),
|
|
bdft: computeBdft(0.64, 1.5),
|
|
widthAvgIn: sampleWidths(96, [18, 20, 22]).avg,
|
|
sku: 'SLAB-DEMO-001',
|
|
speciesId: 101,
|
|
woodCategoryIds: [201],
|
|
edgeTypeTermId: 301,
|
|
figureTermIds: [402],
|
|
gradeTermId: 501,
|
|
thicknessTermId: 601,
|
|
moistureTermId: 701,
|
|
price: 11.52,
|
|
title: buildCopyTitle(
|
|
{ lengthIn: 96, widthAvgIn: 20, thicknessIn: 1.5, bdft: 0.96 },
|
|
'Figured',
|
|
'Black Walnut',
|
|
{ slug: 'live-edge-wood', name: 'Live edge wood' },
|
|
)
|
|
});
|
|
// Same readiness gate ReviewForm.checkReady() enforces.
|
|
const ready =
|
|
!!slab.sku && slab.sku.length >= 3 && slab.speciesId != null && slab.woodCategoryIds.length > 0 &&
|
|
slab.edgeTypeTermId != null && slab.figureTermIds.length > 0 && slab.gradeTermId != null &&
|
|
slab.thicknessTermId != null && slab.moistureTermId != null && slab.price > 0 && slab.title.length > 0;
|
|
assert.ok(ready);
|
|
assert.strictEqual(slab.bdft, 0.96);
|
|
assert.strictEqual(slab.price, Math.round(slab.bdft * 12 * 100) / 100);
|
|
});
|