Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Varumärkesvakt (beslut D-026): varumärkesnamn får ENDAST finnas i brand.config.json.
|
||||
# Körs i CI och kan köras lokalt: ./scripts/brand-guard.sh
|
||||
#
|
||||
# Vaktar två saker:
|
||||
# 1. Det gamla arbetsnamnet får aldrig återinträda någonstans.
|
||||
# 2. Det AKTUELLA namnet i brand.config.json får inte hårdkodas i kod
|
||||
# (README/docs får nämna det; kod, seed och infrastruktur får det inte).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
FAIL=0
|
||||
|
||||
# 1. Historiska/förbjudna namn – uppdatera listan vid namnbyte.
|
||||
FORBIDDEN=("matrivo")
|
||||
for name in "${FORBIDDEN[@]}"; do
|
||||
HITS=$(grep -ril "$name" . \
|
||||
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist \
|
||||
--exclude-dir=.turbo --exclude-dir=.expo --exclude-dir=.data \
|
||||
--exclude=brand-guard.sh 2>/dev/null || true)
|
||||
if [ -n "$HITS" ]; then
|
||||
echo "FEL: förbjudet namn '$name' hittades i:"
|
||||
echo "$HITS"
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
|
||||
# 2. Aktuellt namn utanför brand.config.json i kod/infrastruktur.
|
||||
CURRENT=$(node -e "console.log(require('./brand.config.json').name)")
|
||||
HITS=$(grep -rl "$CURRENT" apps packages infrastructure \
|
||||
--include="*.ts" --include="*.tsx" --include="*.sql" --include="*.yml" \
|
||||
--include="*.yaml" --include="Dockerfile*" \
|
||||
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.turbo 2>/dev/null || true)
|
||||
if [ -n "$HITS" ]; then
|
||||
echo "FEL: aktuella namnet '$CURRENT' är hårdkodat utanför brand.config.json i:"
|
||||
echo "$HITS"
|
||||
echo "Använd BRAND.name / {brand}-interpolation i stället."
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "Varumärkesvakt: OK – namnet lever endast i brand.config.json."
|
||||
fi
|
||||
exit $FAIL
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Lasttest mot lokal API (körs mot dist-bygget):
|
||||
* node apps/api/dist/index.js & # starta API:t först
|
||||
* node scripts/loadtest.mjs
|
||||
*
|
||||
* Tre scenarier: oautentiserad healthz, autentiserad receptlista (DB + översättnings-
|
||||
* upplösning) och rekommendationer (tyngsta läsvägen: lager + motor + förklaringar).
|
||||
* Trösklar (encore-server, 1 CPU-klass): p99 < 500 ms, inga fel, > 100 req/s på recept.
|
||||
*/
|
||||
import autocannon from "autocannon";
|
||||
|
||||
const BASE = process.env.API_BASE_URL ?? "http://localhost:4000";
|
||||
|
||||
async function json(path, options = {}) {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...options,
|
||||
headers: { "content-type": "application/json", ...(options.headers ?? {}) },
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
if (!res.ok) throw new Error(`${path} -> ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// --- Setup: användare med hushåll + lite lager, så läsvägarna är realistiska ---
|
||||
const email = `loadtest-${process.pid}@example.com`;
|
||||
let auth;
|
||||
try {
|
||||
auth = await json("/v1/auth/register", {
|
||||
method: "POST",
|
||||
body: { email, password: "Lasttest1234!", displayName: "Lasttest" },
|
||||
});
|
||||
} catch {
|
||||
auth = await json("/v1/auth/login", {
|
||||
method: "POST",
|
||||
body: { email, password: "Lasttest1234!" },
|
||||
});
|
||||
}
|
||||
const token = auth.accessToken;
|
||||
const headers = { authorization: `Bearer ${token}` };
|
||||
const hh = await json("/v1/households", { method: "POST", headers, body: { name: "Lasttest" } });
|
||||
const full = await json(`/v1/households/${hh.id}`, { headers });
|
||||
const locId = full.storageLocations[0].id;
|
||||
for (const [ing, qty] of [["pasta_spaghetti", 500], ["minced_beef", 400], ["onion_yellow", 3]]) {
|
||||
await json("/v1/inventory/items", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: {
|
||||
ingredientId: ing,
|
||||
displayName: ing,
|
||||
storageLocationId: locId,
|
||||
quantity: qty,
|
||||
unit: qty > 10 ? "GRAM" : "COUNT",
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// --- Scenarier ---
|
||||
const scenarios = [
|
||||
{ name: "healthz (baslinje)", path: "/healthz", auth: false },
|
||||
{ name: "GET /v1/recipes (lista + i18n-upplösning)", path: "/v1/recipes?limit=20", auth: true },
|
||||
{
|
||||
name: "GET /v1/recommendations/what-to-eat (tyngsta läsvägen)",
|
||||
path: "/v1/recommendations/what-to-eat?meal=dinner",
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
const results = [];
|
||||
for (const s of scenarios) {
|
||||
const r = await autocannon({
|
||||
url: `${BASE}${s.path}`,
|
||||
connections: 25,
|
||||
duration: 10,
|
||||
headers: s.auth ? headers : {},
|
||||
});
|
||||
results.push({
|
||||
scenario: s.name,
|
||||
reqPerSec: Math.round(r.requests.average),
|
||||
latencyP50: r.latency.p50,
|
||||
latencyP99: r.latency.p99,
|
||||
errors: r.errors + r.non2xx,
|
||||
});
|
||||
console.log(
|
||||
`${s.name}: ${Math.round(r.requests.average)} req/s, p50 ${r.latency.p50} ms, p99 ${r.latency.p99} ms, fel ${r.errors + r.non2xx}`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Trösklar ---
|
||||
const failures = [];
|
||||
for (const r of results) {
|
||||
if (r.errors > 0) failures.push(`${r.scenario}: ${r.errors} fel`);
|
||||
if (r.latencyP99 > 500) failures.push(`${r.scenario}: p99 ${r.latencyP99} ms > 500 ms`);
|
||||
}
|
||||
const recipes = results[1];
|
||||
if (recipes && recipes.reqPerSec < 100) {
|
||||
failures.push(`receptlistan: ${recipes.reqPerSec} req/s < 100`);
|
||||
}
|
||||
|
||||
console.log("\n| Scenario | req/s | p50 | p99 | fel |");
|
||||
console.log("| -------- | ----- | --- | --- | --- |");
|
||||
for (const r of results) {
|
||||
console.log(
|
||||
`| ${r.scenario} | ${r.reqPerSec} | ${r.latencyP50} ms | ${r.latencyP99} ms | ${r.errors} |`,
|
||||
);
|
||||
}
|
||||
if (failures.length) {
|
||||
console.error("\nUNDERKÄND:\n" + failures.map((f) => ` ✗ ${f}`).join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\nGODKÄND – alla trösklar klarade.");
|
||||
Reference in New Issue
Block a user