Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/node-ui/src/ui/hooks/useSwmAttributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,14 @@ export function useSwmAttributions(contextGraphId: string | undefined): SwmAttri
headers: { 'Content-Type': 'application/json', ...authHeaders() },
signal: controller.signal,
body: JSON.stringify({
// Do NOT send contextGraphId here. The query already scopes to the
Comment thread
Jurij89 marked this conversation as resolved.
Comment thread
Jurij89 marked this conversation as resolved.
// CG via its STRSTARTS(?g, "<cgUri>") filter and must read EVERY
// sub-graph's <cg>/<sg>/_shared_memory_meta partition. Passing
// contextGraphId makes the engine constrain GRAPH ?g to CG-direct
// graphs only, dropping per-sub-graph attribution so the legend
// under-counts agents (B2). See dkg-query-engine.ts graph-variable
// allow-list.
sparql: buildAttributionsQuery(contextGraphId),
Comment thread
Jurij89 marked this conversation as resolved.
Comment thread
Jurij89 marked this conversation as resolved.
contextGraphId,
}),
});
if (!res.ok) throw new Error(`SPARQL query failed: ${res.status}`);
Expand Down
7 changes: 6 additions & 1 deletion packages/node-ui/src/ui/hooks/useVerifiedMemoryAnchors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,13 @@ export function useVerifiedMemoryAnchors(
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({
// Do NOT send contextGraphId here. buildAnchorsQuery already scopes to
// the CG via STRSTARTS(?g, "<cgUri>") and enumerates EVERY sub-graph's
// <cg>/<sg>/_shared_memory_meta partition. Passing contextGraphId makes
// the engine constrain GRAPH ?g to CG-direct graphs only, dropping
// per-sub-graph anchors/attribution (same bug class as B2). See
// dkg-query-engine.ts graph-variable allow-list.
sparql: buildAnchorsQuery(contextGraphId),
contextGraphId,
}),
});
if (!res.ok) throw new Error(`SPARQL query failed: ${res.status}`);
Expand Down
52 changes: 51 additions & 1 deletion packages/node-ui/test/use-swm-attributions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ describe('useSwmAttributions — stale-on-switch protection', () => {
originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async (_url: any, init?: any) => {
const body = init?.body ? JSON.parse(String(init.body)) : {};
const cgId: string = body.contextGraphId;
// B2: the POST body no longer carries contextGraphId (it scoped the
// engine to CG-direct graphs and dropped per-sub-graph attribution).
// Recover the cgId from the SPARQL's STRSTARTS filter to route the
// deferred promise, matching how the daemon now scopes the query.
const cgId: string = String(body.sparql).match(/context-graph:([^"/]+)/)?.[1] ?? '';
Comment thread
Jurij89 marked this conversation as resolved.
Outdated
const p = new Promise<any[]>((resolve) => {
pending.set(cgId, { resolve });
});
Expand Down Expand Up @@ -125,3 +129,49 @@ describe('useSwmAttributions — stale-on-switch protection', () => {
expect(latest!.events[0].rootUri).toBe('urn:e:cg-B');
});
});

// B2 (DKG-NODE-ISSUES-FOR-RC17) — the attribution fetch MUST NOT send
// contextGraphId. The SPARQL scopes itself to the CG via STRSTARTS(?g, …)
// and must read every sub-graph's <cg>/<sg>/_shared_memory_meta partition;
// sending contextGraphId makes the daemon constrain GRAPH ?g to CG-direct
// graphs only, so the legend under-counted agents (showed 1 of 5 agents live).
describe('useSwmAttributions — B2 POST body scoping', () => {
let root: Root;
let container: HTMLDivElement;
let originalFetch: typeof globalThis.fetch | undefined;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
json: async () => ({ result: { bindings: [] } }),
} as any)) as any;
});

afterEach(async () => {
await act(async () => { root.unmount(); });
container.remove();
if (originalFetch) globalThis.fetch = originalFetch;
});

it('omits contextGraphId from the /api/query POST body so all sub-graph partitions are reached', async () => {
function Probe({ id }: { id: string }) {
useSwmAttributions(id);
return null;
}
await act(async () => {
root.render(React.createElement(Probe, { id: 'cg-1' }));
});
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });

const calls = vi.mocked(fetch).mock.calls;
expect(calls.length).toBeGreaterThan(0);
const body = JSON.parse(String((calls[0][1] as any)?.body ?? '{}'));
expect(body.sparql).toContain('_shared_memory_meta');
expect(body).not.toHaveProperty('contextGraphId');
});
});
54 changes: 54 additions & 0 deletions packages/node-ui/test/use-verified-memory-anchors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// @vitest-environment happy-dom

import React, { act } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createRoot, type Root } from 'react-dom/client';
import { useVerifiedMemoryAnchors } from '../src/ui/hooks/useVerifiedMemoryAnchors.js';

(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;

// Same bug class as B2 (DKG-NODE-ISSUES-FOR-RC17): buildAnchorsQuery already
// scopes to the CG via STRSTARTS(?g, …) and enumerates EVERY sub-graph's
// <cg>/<sg>/_shared_memory_meta partition, so the fetch MUST NOT also send
// contextGraphId — that makes the daemon constrain GRAPH ?g to CG-direct
// graphs only, dropping per-sub-graph anchors/attribution.
describe('useVerifiedMemoryAnchors — POST body scoping', () => {
let root: Root;
let container: HTMLDivElement;
let originalFetch: typeof globalThis.fetch | undefined;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
json: async () => ({ result: { bindings: [] } }),
} as any)) as any;
});

afterEach(async () => {
await act(async () => { root.unmount(); });
container.remove();
if (originalFetch) globalThis.fetch = originalFetch;
});

it('omits contextGraphId from the /api/query POST body so all sub-graph partitions are reached', async () => {
function Probe({ id }: { id: string }) {
useVerifiedMemoryAnchors(id);
return null;
}
await act(async () => {
root.render(React.createElement(Probe, { id: 'cg-1' }));
});
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });

const calls = vi.mocked(fetch).mock.calls;
expect(calls.length).toBeGreaterThan(0);
const body = JSON.parse(String((calls[0][1] as any)?.body ?? '{}'));
expect(body.sparql).toContain('_shared_memory_meta');
expect(body).not.toHaveProperty('contextGraphId');
});
});
Loading