Skip to content

Commit 87561a3

Browse files
DMontgomery40claude
andcommitted
test(graph): follow the reviewed-schema contract in the live pipeline test and the G1 explorer spec
Two lanes that only run against the paid gateway had drifted from the Task 7/8 contracts and surfaced once the key limit was raised: - tests/integration/test_graphrag_pipeline_live.py built the semantic pipeline without the route upstream (D25) and the extraction template (D24) the factory now requires; it passes both, resolved the way the index job does. - web/tests/e2e/exhaustive/curious_user_p1_fixes.spec.ts (G1/G2/G3) indexed a semantic corpus without a reviewed schema (409 graph_schema_approval_required since Task 8), expected the label-propagation community ids retired by Task 7's GDS Leiden cut, and searched the fullscreen canvas for the closed-vocabulary palette retired by D1. It now derives and approves the proposal through the same endpoint the Indexing page uses (indexCorpus takes the approved hash), expects Leiden's integer community ids, requires the legend to name the reviewed schema's labels, and reads node colours from the legend's swatches. Verification (LXC100 overlay against the deployed eec26d4): pipeline live suite 4 passed; the G1/G2/G3 spec 1 passed in 50 s after failing on each stale expectation in turn; ruff and tsc clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016TSkfA3Bn57dRokmK22MkE
1 parent 41cfd0a commit 87561a3

3 files changed

Lines changed: 52 additions & 15 deletions

File tree

tests/integration/test_graphrag_pipeline_live.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from server.config import load_config
2525
from server.db.neo4j import Neo4jClient
2626
from server.db.postgres import PostgresClient
27-
from server.gateway_catalog import warm_gateway_catalog
27+
from server.gateway_catalog import gateway_upstream_for_alias, warm_gateway_catalog
2828
from server.indexing.generations import GenerationManifest
2929
from server.indexing.graphrag_pipeline import (
3030
GraphScopeCollisionError,
@@ -137,9 +137,11 @@ async def test_semantic_and_code_files_use_scoped_official_writer_contract(
137137
route_model=str(route.model or ""),
138138
route_base_url=str(route.base_url or ""),
139139
route_api_key=str(route.api_key or ""),
140+
route_upstream=gateway_upstream_for_alias(str(route.model or "")),
140141
max_concurrency=2,
141142
llm_timeout_s=int(cfg.graph_indexing.semantic_kg_llm_timeout_s),
142143
reasoning_effort=str(cfg.graph_indexing.semantic_kg_reasoning_effort),
144+
prompt_template=str(cfg.system_prompts.semantic_kg_extraction),
143145
)
144146
semantic_chunks = [
145147
Chunk(

web/tests/e2e/exhaustive/corpus_fixture.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,18 @@ export async function indexCorpus(
9393
request: APIRequestContext,
9494
corpusId: string,
9595
corpusPath: string,
96-
opts: { timeoutMs?: number } = {}
96+
opts: { timeoutMs?: number; approvedGraphSchemaHash?: string } = {}
9797
): Promise<void> {
9898
const timeoutMs = opts.timeoutMs ?? INDEX_TIMEOUT_MS;
99+
// A semantic-policy corpus indexes only against its reviewed schema hash (Task 8);
100+
// callers that derived a proposal pass it through, everything else stays as before.
99101
const started = await request.post(`${API_BASE}/index`, {
100-
data: { corpus_id: corpusId, repo_path: corpusPath, force_reindex: true },
102+
data: {
103+
corpus_id: corpusId,
104+
repo_path: corpusPath,
105+
force_reindex: true,
106+
...(opts.approvedGraphSchemaHash ? { approved_graph_schema_hash: opts.approvedGraphSchemaHash } : {}),
107+
},
101108
});
102109
if (!started.ok()) await failWithBody(`POST /api/index for ${corpusId}`, started);
103110
const deadline = Date.now() + timeoutMs;

web/tests/e2e/exhaustive/curious_user_p1_fixes.spec.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ test.describe.serial('G1/G2/G3 on a semantic graph corpus provisioned by the sui
223223
// pre-existing corpus, so the graph assertions can never skip.
224224
let graphCorpus: ExhaustiveCorpus;
225225
let graphCorpusId = '';
226+
/** Node labels of the reviewed schema the run indexed against (the legend must show these). */
227+
let approvedNodeLabels: string[] = [];
226228

227229
test.beforeAll(async ({ request }) => {
228230
test.setTimeout(10 * 60 * 1000);
@@ -233,7 +235,21 @@ test.describe.serial('G1/G2/G3 on a semantic graph corpus provisioned by the sui
233235
build_code_graph: false,
234236
semantic_kg_llm_model: EXHAUSTIVE_CHAT_MODEL,
235237
});
236-
await indexCorpus(request, graphCorpusId, graphCorpus.corpusPath);
238+
// Semantic indexing runs only against a reviewed schema (Task 8): derive the proposal
239+
// the way the operator does on the Indexing page and approve its exact hash.
240+
const proposalRes = await request.post(`${API_BASE}/index/${encodeURIComponent(graphCorpusId)}/graph-schema/proposal`, {
241+
data: { force_refresh: false },
242+
});
243+
expect(proposalRes.ok(), await proposalRes.text()).toBeTruthy();
244+
const proposal = (await proposalRes.json()) as {
245+
schema_hash: string;
246+
schema?: { node_types?: Array<{ label: string }> };
247+
schema_payload?: { node_types?: Array<{ label: string }> };
248+
};
249+
expect(proposal.schema_hash).toMatch(/^[0-9a-f]{64}$/);
250+
approvedNodeLabels = ((proposal.schema ?? proposal.schema_payload)?.node_types ?? []).map((n) => n.label);
251+
expect(approvedNodeLabels.length, 'the reviewed schema names at least one node type').toBeGreaterThan(0);
252+
await indexCorpus(request, graphCorpusId, graphCorpus.corpusPath, { approvedGraphSchemaHash: proposal.schema_hash });
237253
const stats = await (await request.get(`${API_BASE}/graph/${encodeURIComponent(graphCorpusId)}/stats`)).json();
238254
expect(stats.total_entities, 'semantic extraction must produce entities').toBeGreaterThan(0);
239255
expect(stats.total_relationships, 'semantic extraction must produce relationships').toBeGreaterThan(0);
@@ -253,7 +269,9 @@ test.describe.serial('G1/G2/G3 on a semantic graph corpus provisioned by the sui
253269
const ids = await communities.evaluateAll((els) => els.map((el) => el.getAttribute('data-testid') || ''));
254270
expect(ids.length).toBeGreaterThan(1);
255271
for (const id of ids) {
256-
expect(id).toMatch(/^graph-community-c-[0-9a-f]{12}$/);
272+
// Communities are GDS Leiden partitions since Task 7 (integer communityId), not the
273+
// label-propagation hashes the 2026-08-25 fix wave named `c-<12 hex>`.
274+
expect(id).toMatch(/^graph-community-\d+$/);
257275
expect(id).not.toContain('__staging__');
258276
expect(id).not.toContain('(root)');
259277
}
@@ -290,8 +308,15 @@ test.describe.serial('G1/G2/G3 on a semantic graph corpus provisioned by the sui
290308
expect(sizes.canvasW, 'fullscreen canvas fills the modal width').toBeGreaterThan(sizes.modalW - 4);
291309
expect(sizes.canvasH, 'fullscreen canvas fills the modal height').toBeGreaterThan(sizes.hostH - 4);
292310
expect(sizes.headerText).toMatch(/\d+ nodes [1-9]\d* edges/);
311+
// Entity types are the reviewed schema's labels since Task 8 (D1), not a closed
312+
// vocabulary; the legend lists them and never the code policy's kinds.
293313
const legend = page.getByTestId('graph-fullscreen-legend');
294-
await expect(legend).toContainText('concept');
314+
const legendText = (await legend.innerText()).trim();
315+
expect(legendText.length).toBeGreaterThan(0);
316+
expect(
317+
approvedNodeLabels.some((label) => legendText.includes(label)),
318+
`legend "${legendText}" names none of the reviewed labels ${approvedNodeLabels.join(', ')}`,
319+
).toBe(true);
295320
await expect(legend).not.toContainText('function');
296321

297322
// G3: scroll-zoom changes the zoom transform AND repaints; clicking a node selects it.
@@ -321,21 +346,24 @@ test.describe.serial('G1/G2/G3 on a semantic graph corpus provisioned by the sui
321346
expect(k1! > k0!, `wheel must zoom in (k ${k0} -> ${k1})`).toBeTruthy();
322347
expect(h1, 'the canvas must repaint after zoom').not.toBe(h0);
323348

324-
// Find a painted node (person = #f97316 / org = #0ea5e9) and click it.
349+
// Find a painted node and click it. Node colours belong to the reviewed schema's
350+
// labels (D1), so the targets are read from the legend's own swatches rather than a
351+
// fixed palette.
325352
const nodePoint = await page.evaluate(() => {
326353
const canvas = document.querySelector('[data-testid="graph-fullscreen-canvas"] canvas') as HTMLCanvasElement;
327354
const rect = canvas.getBoundingClientRect();
328355
const scaleX = canvas.width / rect.width;
329356
const scaleY = canvas.height / rect.height;
330357
const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height).data;
331-
// person, org, location, event, concept — the visualizer's node palette.
332-
const targets = [
333-
[249, 115, 22],
334-
[14, 165, 233],
335-
[16, 185, 129],
336-
[234, 179, 8],
337-
[148, 163, 184],
338-
];
358+
const swatches = Array.from(
359+
document.querySelectorAll('[data-testid="graph-fullscreen-legend"] span span'),
360+
) as HTMLElement[];
361+
const targets: number[][] = [];
362+
for (const swatch of swatches) {
363+
const match = getComputedStyle(swatch).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
364+
if (match) targets.push([Number(match[1]), Number(match[2]), Number(match[3])]);
365+
}
366+
if (targets.length === 0) return null;
339367
for (let y = Math.floor(canvas.height * 0.12); y < canvas.height * 0.88; y += 2) {
340368
for (let x = Math.floor(canvas.width * 0.05); x < canvas.width * 0.95; x += 2) {
341369
const i = (y * canvas.width + x) * 4;

0 commit comments

Comments
 (0)