Implement Phase 0b base project against the frozen test contract

All seven services (gateway, ledger, fraud, sanctions, insights, notifier,
audit), db/schema.sql plus optional_indexes.sql, scripts/seed.py and
smoke.py, the gateway-served frontend, and docker-compose.yml. Plain-text
stdlib logging and a weak manual X-Request-Id only -- no observability yet,
by design, per test_no_observability.py. Implements all 13 named chaos-flag
pathologies from docs/PATHOLOGIES.md behind Redis-backed runtime flags.

make smoke and make test-contract pass; make test-baseline and
make test-pathology pass except for a handful of load-sensitive tests
documented in tests/README.md rather than papered over.
This commit is contained in:
Movses Aslanyan
2026-09-05 08:12:30 +03:30
parent ccf1783779
commit 91afea92a1
51 changed files with 2757 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
const accountSelect = document.getElementById("account-select");
const txForm = document.getElementById("tx-form");
const resultJson = document.getElementById("result-json");
const resultRtt = document.getElementById("result-rtt");
const resultRequestId = document.getElementById("result-request-id");
const burstButton = document.getElementById("burst-button");
const burstN = document.getElementById("burst-n");
const burstTableBody = document.querySelector("#burst-table tbody");
const chaosFlagsDiv = document.getElementById("chaos-flags");
const healthStrip = document.getElementById("health-strip");
async function loadAccounts() {
const resp = await fetch("/api/accounts");
const accounts = await resp.json();
accountSelect.innerHTML = accounts
.slice(0, 200)
.map((a) => `<option value="${a.id}">${a.holder_name} (${a.currency})</option>`)
.join("");
}
txForm.addEventListener("submit", async (e) => {
e.preventDefault();
const payload = {
account_id: accountSelect.value,
kind: document.querySelector('input[name="kind"]:checked').value,
amount_minor: Number(document.getElementById("amount-input").value),
currency: "EUR",
merchant_name: document.getElementById("merchant-input").value,
};
const start = performance.now();
const resp = await fetch("/api/transactions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const rtt = performance.now() - start;
const body = await resp.json();
resultRtt.textContent = rtt.toFixed(1);
resultRequestId.textContent = body.request_id || "--";
resultJson.textContent = JSON.stringify(body, null, 2);
});
function percentile(sorted, p) {
if (sorted.length === 0) return 0;
const rank = Math.max(1, Math.min(sorted.length, Math.ceil(p * sorted.length)));
return sorted[rank - 1];
}
burstButton.addEventListener("click", async () => {
const n = Number(burstN.value);
const accounts = Array.from(accountSelect.options).map((o) => o.value);
burstTableBody.innerHTML = `<tr><td colspan="8">running ${n} requests...</td></tr>`;
const latencies = [];
let success = 0;
let errors = 0;
await Promise.all(
Array.from({ length: n }, async () => {
const accountId = accounts[Math.floor(Math.random() * accounts.length)];
const start = performance.now();
try {
const resp = await fetch("/api/transactions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
account_id: accountId,
kind: "purchase",
amount_minor: 500,
currency: "EUR",
merchant_name: "Load Burst",
}),
});
latencies.push(performance.now() - start);
if (resp.ok) success += 1;
else errors += 1;
} catch (e) {
latencies.push(performance.now() - start);
errors += 1;
}
})
);
latencies.sort((a, b) => a - b);
const mean = latencies.reduce((a, b) => a + b, 0) / latencies.length;
const row = [
latencies.length,
success,
errors,
latencies[0].toFixed(1),
mean.toFixed(1),
percentile(latencies, 0.5).toFixed(1),
percentile(latencies, 0.95).toFixed(1),
latencies[latencies.length - 1].toFixed(1),
];
burstTableBody.innerHTML = `<tr>${row.map((v) => `<td>${v}</td>`).join("")}</tr>`;
});
async function loadChaos() {
const resp = await fetch("/api/admin/chaos");
const { flags } = await resp.json();
chaosFlagsDiv.innerHTML = Object.entries(flags)
.map(
([flag, enabled]) => `
<label>
<input type="checkbox" data-flag="${flag}" ${enabled ? "checked" : ""}>
${flag}
</label>`
)
.join("");
chaosFlagsDiv.querySelectorAll("input[type=checkbox]").forEach((el) => {
el.addEventListener("change", async () => {
await fetch("/api/admin/chaos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ flag: el.dataset.flag, enabled: el.checked }),
});
});
});
}
document.querySelectorAll("#chaos-profiles button").forEach((btn) => {
btn.addEventListener("click", async () => {
await fetch("/api/admin/chaos/profile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ profile: btn.dataset.profile }),
});
await loadChaos();
});
});
async function pollHealth() {
try {
const resp = await fetch("/api/health/all");
const { services } = await resp.json();
healthStrip.innerHTML = Object.entries(services)
.map(([name, status]) => `<span class="dot ${status === "ok" ? "green" : "red"}">${name}</span>`)
.join("");
} catch (e) {
// ignore transient polling failures
}
}
loadAccounts();
loadChaos();
pollHealth();
setInterval(pollHealth, 5000);