Skip to content

Commit 9f74e59

Browse files
authored
Merge pull request #119 from automatiabcn/fix/audit-r4-honest-stat-graph-query-tamper
fix(panel): repair broken NL→Cypher, dead tamper warning, fake agent stat
2 parents b5e9ca4 + d8a6b25 commit 9f74e59

4 files changed

Lines changed: 122 additions & 4 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2+
import { configure, fireEvent, render, screen, waitFor } from "@testing-library/react";
3+
4+
import GraphPage from "@/app/admin/graph/page";
5+
import DashboardPage from "@/app/admin/dashboard/page";
6+
import AgentsPage from "@/app/panel/agents/page";
7+
8+
configure({ testIdAttribute: "data-test" });
9+
10+
afterEach(() => {
11+
vi.unstubAllGlobals();
12+
vi.restoreAllMocks();
13+
});
14+
15+
// ── Finding #2: graph NL→Cypher sent the wrong request key ───────────────────
16+
// Frontend POSTed { question } but the backend NLQueryRequest requires `intent`,
17+
// so every "Cypher üret" click returned HTTP 422 and the feature never worked.
18+
describe("Graph NL→Cypher — request key matches backend contract", () => {
19+
it("POSTs { intent } (not { question }) to /v1/graph/nl-query", async () => {
20+
const cap: { body?: string } = {};
21+
vi.stubGlobal(
22+
"fetch",
23+
vi.fn((url: string | URL | Request, init?: RequestInit) => {
24+
const u = String(url);
25+
if (u.includes("/v1/graph/nl-query")) {
26+
cap.body = String(init?.body ?? "");
27+
return Promise.resolve(
28+
new Response(JSON.stringify({ cypher: "MATCH (n) RETURN n LIMIT 1" }), {
29+
status: 200,
30+
}),
31+
);
32+
}
33+
// schema fetch on mount
34+
return Promise.resolve(
35+
new Response(JSON.stringify({ node_labels: [], relationship_types: [] }), {
36+
status: 200,
37+
}),
38+
);
39+
}),
40+
);
41+
42+
render(<GraphPage />);
43+
fireEvent.change(await screen.findByTestId("graph-nl-input"), {
44+
target: { value: "Acme çalışanları" },
45+
});
46+
fireEvent.click(screen.getByTestId("graph-nl-run"));
47+
48+
await waitFor(() => expect(cap.body).toBeTruthy());
49+
const parsed = JSON.parse(cap.body!);
50+
expect(parsed.intent).toBe("Acme çalışanları");
51+
expect(parsed.question).toBeUndefined();
52+
});
53+
});
54+
55+
// ── Finding #3: tamper warning was dead code ─────────────────────────────────
56+
// Backend emits audit_chain_integrity as the string "ok" | "tampered"; the panel
57+
// tested `=== false`, which a string can never satisfy, so the warning never fired.
58+
describe("Dashboard — audit chain tamper warning fires on a string status", () => {
59+
function mockDashboard(integrity: "ok" | "tampered") {
60+
vi.stubGlobal(
61+
"fetch",
62+
vi.fn(() =>
63+
Promise.resolve(
64+
new Response(
65+
JSON.stringify({ vault: { total_entries: 5, audit_chain_integrity: integrity } }),
66+
{ status: 200 },
67+
),
68+
),
69+
),
70+
);
71+
}
72+
73+
it("shows the tamper warning when integrity is 'tampered'", async () => {
74+
mockDashboard("tampered");
75+
render(<DashboardPage />);
76+
expect(await screen.findByText(/Zincir bütünlüğü bozuk/i)).toBeTruthy();
77+
});
78+
79+
it("does NOT show the tamper warning when integrity is 'ok'", async () => {
80+
mockDashboard("ok");
81+
render(<DashboardPage />);
82+
// Let the fetch resolve + state settle.
83+
await screen.findByText(/Vault audit/i);
84+
await new Promise((r) => setTimeout(r, 20));
85+
expect(screen.queryByText(/Zincir bütünlüğü bozuk/i)).toBeNull();
86+
});
87+
});
88+
89+
// ── Finding #1: fabricated "100%" stat ───────────────────────────────────────
90+
// "Structured Output: 100%" sat in the same styled stat grid as real, fetched
91+
// counts, reading as a measured per-tenant compliance rate. It is a design
92+
// invariant, not a measurement — reframed so it no longer masquerades as live.
93+
describe("Agent Registry — structured-output stat is honest, not a fake metric", () => {
94+
beforeEach(() => {
95+
vi.stubGlobal(
96+
"fetch",
97+
vi.fn(() =>
98+
Promise.resolve(
99+
new Response(
100+
JSON.stringify({ total: 12, approval_gated: 4, categories: [] }),
101+
{ status: 200 },
102+
),
103+
),
104+
),
105+
);
106+
});
107+
108+
it("does not render a fabricated 100% measurement", async () => {
109+
render(<AgentsPage />);
110+
await screen.findByText(/Structured Output/i);
111+
expect(screen.queryByText("100%")).toBeNull();
112+
});
113+
});

core/landing/app/admin/dashboard/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ export default function AdminDashboardPage() {
132132
};
133133
const vault = (data?.vault ?? {}) as {
134134
total_entries?: number;
135-
audit_chain_integrity?: boolean;
135+
// Backend (vault/audit_chain.py) emits a STATUS string "ok" | "tampered",
136+
// not a boolean — typing it as bool made the `=== false` tamper check below
137+
// dead code, so the warning never surfaced on a genuinely tampered chain.
138+
audit_chain_integrity?: "ok" | "tampered";
136139
};
137140

138141
const betaCount = (beta.pending ?? 0) + (beta.approved ?? 0);
@@ -220,7 +223,7 @@ export default function AdminDashboardPage() {
220223
title="Vault audit"
221224
value={vault.total_entries ?? 0}
222225
description={
223-
vault.audit_chain_integrity === false
226+
vault.audit_chain_integrity === "tampered"
224227
? "⚠ Zincir bütünlüğü bozuk"
225228
: "Audit zinciri kayıt sayısı"
226229
}

core/landing/app/admin/graph/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ async function nlToCypher(
106106
method: "POST",
107107
credentials: "include",
108108
headers: { "Content-Type": "application/json" },
109-
body: JSON.stringify({ question: nl }),
109+
// Backend NLQueryRequest requires `intent` (not `question`); sending the
110+
// wrong key returned 422 on every call and the feature never worked.
111+
body: JSON.stringify({ intent: nl }),
110112
});
111113
if (!res.ok) {
112114
const text = await res.text();

core/landing/app/panel/agents/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export default function AgentRegistryPage() {
8484
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
8585
<Stat label="Kayıtlı Agent" value={String(data.total)} hint="5 kategori · 120 MCP tool" />
8686
<Stat label="Onay-Kapılı" value={String(data.approval_gated)} hint="orta+ risk → Approval" />
87-
<Stat label="Structured Output" value="100%" hint="evidence_id + confidence" />
87+
<Stat label="Structured Output" value="Zorunlu" hint="şema ile zorunlu · evidence_id + confidence" />
8888
<Stat label="Kategori" value={String(data.categories.length)} hint="discovery · intel · engage · ops" />
8989
</div>
9090

0 commit comments

Comments
 (0)