Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
52 changes: 51 additions & 1 deletion packages/node-ui/src/ui/mocks/data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NotificationsFeedResponse } from '../api.js';
import type { NotificationsFeedResponse, SubGraphInfo } from '../api.js';

export const MOCK_STATUS = {
name: 'my-dkg-node',
Expand Down Expand Up @@ -198,3 +198,53 @@ export const MOCK_SESSIONS = {
},
],
};

// GH#1763 — mock sub-graph lists keyed by CG id, mirroring the real
// `/api/sub-graph/list` response shape. `provider.ts` previously reached for
// this map through `(mock as any).MOCK_SUBGRAPHS`, which the module never
// exported: the optional lookup kept mock mode from crashing, but the
// per-CG override the comment promised could never fire and the production
// Vite build emitted a missing-export warning on every run.
//
// Only CGs whose sub-graph UI is worth exercising need an entry — the
// provider falls back to an empty list for anything absent, which is what
// `cg:supply-chain-eu` deliberately exercises.
export const MOCK_SUBGRAPHS: Record<

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — Only 1 of the 3 fetchSubGraphs consumers goes through the mock provider — mock mode now self-contradicts

The PR's premise is that "the per-CG override the provider's comment advertised could never fire." After this fix it fires for exactly one of the three consumers, because the other two bypass api-wrapper entirely:

  • src/ui/views/ProjectView.tsx:447api.fetchSubGraphs(...) (wrapped → mock path). This is the Overview Subgraphs stat.
  • src/ui/components/SubGraphBar.tsx:12,223import { fetchSubGraphs } from '../api.js' (direct, unwrapped).
  • src/ui/views/project/components/subgraph.tsx:2,64 (SubGraphOverviewGrid) → import { fetchSubGraphs } from '../../../api.js' (direct, unwrapped).

In mock mode the daemon is unreachable by definition (/api/status failed detection), so the two direct callers still reject: SubGraphBar swallows it (.catch(() => {})subGraphs = []merged.length === 0return null), and SubGraphOverviewGrid sets fetchError and renders the failure/teaching empty state. Only the wrapped Overview stat picks up MOCK_SUBGRAPHS.

Net effect for a demo-mode operator: the Overview stat strip reads Subgraphs: 2 (overview.tsx:524-526, subGraphCount.toLocaleString()), and clicking through to the Subgraph Explorer for the same CG shows no chip row and the "couldn't load subgraphs / no subgraphs yet" body. Before this PR all three read 0 / empty, which was at least self-consistent.

The new test suite structurally cannot catch this: it calls mockApi.fetchSubGraphs in isolation and never exercises a consumer, so "provider-level tests" (issue AC #3) pass while the rendered surfaces disagree. Either route SubGraphBar + SubGraphOverviewGrid through api-wrapper (the same "Codex review bug F" fix already applied to the Overview stat), or drop the two entries whose only observable effect today is to desync the stat from the page it links to.

Concrete failure scenario

Stop the daemon, load the UI (mock mode latches), open the Pharma Drug Interactions project. Overview stat strip shows Subgraphs 2; navigate to the Subgraph Explorer tab for the same CG and it renders zero chips plus the empty/failed state. On testnet-canary both read 0.

Self-review: raised by an independent reviewer pass, then verified against the PR head before posting.

string,
{ contextGraphId: string; subGraphs: SubGraphInfo[] }
> = {
'cg:pharma-drug-interactions': {
contextGraphId: 'cg:pharma-drug-interactions',
subGraphs: [
{
name: 'Interactions',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Mock sub-graph names use labels instead of daemon slugs

What's wrong
The mock API is now exercising a different contract than /api/sub-graph/list. SubGraphInfo.name is not just a label in this UI; it is the slug used for filtering, profile lookup, selection state, and API calls. These new values can drift from real sub-graph membership and one of them is invalid by the daemon's own validator, so mock mode can show sub-graphs that cannot behave like real sub-graphs.

Example
In mock mode, fetchSubGraphs('cg:climate-science') returns { name: 'Arctic Ice', uri: 'cg:climate-science/arctic-ice' }. A user selecting that sub-graph makes the UI treat Arctic Ice as the subGraphName, but that name cannot be created or queried against the daemon because whitespace is invalid; locally derived maps keyed by arctic-ice also will not match.

Suggested direction
Return daemon-valid slug names from the mock API and put human-readable labels somewhere the UI already expects display metadata.

For Agents
In packages/node-ui/src/ui/mocks/data.ts, make MOCK_SUBGRAPHS[*].subGraphs[*].name use the real daemon slug (interactions, contraindications, arctic-ice) and keep display labels in the profile binding or description path. Update mock-provider-subgraphs.test.ts to assert slug names and include at least one case proving name matches the URI tail / validates as a daemon subGraphName.

uri: 'cg:pharma-drug-interactions/interactions',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — uri drops the did:dkg:context-graph: prefix and its tail contradicts name

Real sub-graph URIs are did:dkg:context-graph:<cgId>/<name>contextGraphSubGraphUri (packages/core/src/constants.ts:563) and generateSubGraphRegistration (packages/publisher/src/metadata.ts:1785), which mints the registration subject from exactly that template. The daemon's list route echoes the stored sg.uri verbatim and keys its counts off the first path segment after did:dkg:context-graph:<cg>/, so in production uri === \did:dkg:context-graph:${contextGraphId}/${name}`` is an invariant.

The fixture breaks it twice over: the prefix is missing ('cg:pharma-drug-interactions/interactions'), and the trailing segment is not nameinteractions vs 'Interactions' (line 220), arctic-ice vs 'Arctic Ice' (line 243).

No current consumer reads SubGraphInfo.uri (grepped packages/node-ui/src — zero hits), so there is no broken code path today; the cost is fixture fidelity for a module whose stated job is "mirroring the real /api/sub-graph/list response shape", and a demo that would silently look correct to the next person who adds a uri-derived lookup.

Concrete failure scenario

A later change resolves the named graph to query it, or derives the slug as uri.split('/').pop(). Against a real node both work; against the fixture the graph URI matches nothing and the derived slug (interactions) is not the slug the rest of the UI uses (Interactions), so the demo path silently diverges from production.

Self-review: raised by an independent reviewer pass, then verified against the PR head before posting.

description: 'Pairwise drug interaction records',
createdBy: 'did:dkg:agent:0x1111111111111111111111111111111111111111',
createdAt: '2026-04-02T09:15:00Z',
entityCount: 148,
tripleCount: 1721,
},
{
name: 'Contraindications',
uri: 'cg:pharma-drug-interactions/contraindications',
createdBy: 'did:dkg:agent:0x2222222222222222222222222222222222222222',
createdAt: '2026-04-05T16:40:00Z',
entityCount: 79,
tripleCount: 604,
},
],
},
'cg:climate-science': {
contextGraphId: 'cg:climate-science',
subGraphs: [
{
name: 'Arctic Ice',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — name: 'Arctic Ice' is a sub-graph name the daemon can never emit (whitespace is rejected)

SubGraphInfo.name is the sub-graph slug, not a display label. Every consumer treats it that way: SubGraphBar.tsx builds slug: sg.name and profile.forSubGraph(sg.name); SubGraphOverviewGrid matches it against entity.subGraphs membership; lib/subGraphs.ts::isUserFacingSubGraph compares it against RESERVED_SUB_GRAPH_SLUGS. The human-readable label is supposed to arrive from the profile binding — displayName: binding.displayName ?? sg.name.

packages/core/src/constants.ts:650 (validateSubGraphName) rejects /[<>"{}|^\\s]/— whitespace included. SocreateSubGraph(cg, 'Arctic Ice')throwsInvalid sub-graph name "Arctic Ice": Sub-graph name contains characters unsafe for IRIs, and subGraphDeregistrationSparql (publisher/src/metadata.ts) asserts the same guard. There is no reachable state in which /api/sub-graph/listreturns this row. The fixture's own comment claims it "mirrors the real/api/sub-graph/list response shape"; this row does not. Use a real slug (arctic-ice`) and let the profile binding supply "Arctic Ice".

(Capitals are legal, so Interactions/Contraindications are representable — but they are the same slug-vs-display-label conflation, and they are what the demo renders as chip labels.)

Same defect class, fold in here: the climate entry omits createdBy and createdAt. generateSubGraphRegistration (publisher/src/metadata.ts:1785-1794) always emits both quads, and dkg-agent-cg-registry.ts:937 declares createdBy: string as required on the daemon's own return type — the route at cli/src/daemon/routes/context-graph.ts:1004-1010 passes them straight through. A response with no createdBy is likewise unreachable.

Concrete failure scenario

Take the fixture as the reference for the endpoint and try to reproduce it against a live node: agent.createSubGraph('cg:climate-science', 'Arctic Ice') throws Invalid sub-graph name "Arctic Ice". Meanwhile mock mode renders a chip whose slug contains a space, a state real data cannot reach, and whose profile.forSubGraph('Arctic Ice') can never match a daemon-emitted binding.

Self-review: raised by an independent reviewer pass, then verified against the PR head before posting.

uri: 'cg:climate-science/arctic-ice',
description: 'Sea-ice extent projections',
entityCount: 32,
tripleCount: 410,
},
],
},
};
2 changes: 1 addition & 1 deletion packages/node-ui/src/ui/mocks/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const mockApi = {
// sitting at "..." after a real /sub-graph/list 404. Default to an
// empty list; per-CG mocks can override via MOCK_SUBGRAPHS.
fetchSubGraphs: (id: string) =>
delay((mock as any).MOCK_SUBGRAPHS?.[id] ?? { contextGraphId: id, subGraphs: [] }),
delay(mock.MOCK_SUBGRAPHS[id] ?? { contextGraphId: id, subGraphs: [] }),
fetchNotificationsFeed: () => delay(mock.MOCK_NOTIFICATIONS_FEED),
fetchNodeLog: () => delay(mock.MOCK_NODE_LOG),
fetchMemorySessions: () => delay(mock.MOCK_SESSIONS),
Expand Down
60 changes: 60 additions & 0 deletions packages/node-ui/test/mock-provider-subgraphs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';
import { MOCK_CONTEXT_GRAPHS, MOCK_SUBGRAPHS } from '../src/ui/mocks/data.js';
import { mockApi } from '../src/ui/mocks/provider.js';

// GH#1763 — `provider.ts` used to read `(mock as any).MOCK_SUBGRAPHS`, a symbol
// `data.ts` never exported. The optional lookup stopped mock mode from
// crashing, so the only visible symptom was a Vite missing-export warning on
// every production build — and the per-CG override the provider's own comment
// advertised silently could not work. These tests pin both halves of the
// contract so the export cannot regress back into an untyped lookup.
describe('mockApi.fetchSubGraphs (GH#1763)', () => {
it('exports a typed MOCK_SUBGRAPHS map from the mock data module', () => {
expect(MOCK_SUBGRAPHS).toBeDefined();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — Test 1 is subsumed by the other tests and passes for null and {}

Test 1 (lines 12-15) adds a named test to the report but no detection power beyond what tests 2, 3, 5 and 6 already have.

It is not literally dead — I checked the runtime semantics rather than assuming, because it matters here. Under vite-node a named import of a missing export resolves to undefined at runtime rather than throwing a link error (confirmed with a minimal repro: expect(MOCK_SUBGRAPHS).toBeDefined()AssertionError: expected undefined to be defined, no import-time failure). So test 1 does fire when the export is deleted.

The problem is that so does everything else. In that same state, test 2 throws on MOCK_SUBGRAPHS['cg:pharma-drug-interactions'], test 3 throws on line 29, test 5 throws on Object.entries(undefined), and test 6 throws on Object.keys(undefined) — verified end to end: Tests 5 failed | 1 passed. Test 1 is a strict subset.

And its own assertions are weaker than they look:

  • expect(x).toBeDefined() only checks !== undefined, so it passes for null.
  • expect(typeof x).toBe('object') also passes for null (typeof null === 'object').

So export const MOCK_SUBGRAPHS = null as any satisfies test 1 in full, as does = {}. Neither is a state anyone wants. If the intent is "the export exists and is a usable map," expect(Object.keys(MOCK_SUBGRAPHS).length).toBeGreaterThan(0) is one line and actually distinguishes those cases; otherwise the test is fine to drop, since the export's existence is implied by every other test in the file.

Concrete failure scenario

Someone reduces the fixture to export const MOCK_SUBGRAPHS = {} while removing the two CG entries. Test 1's name — "exports a typed MOCK_SUBGRAPHS map from the mock data module" — reports green, as it also would for null as any. A reader scanning the suite output concludes the typed-export contract is verified; the only test that actually noticed the change was test 2.

Self-review: raised by an independent reviewer pass, then verified against the PR head before posting.

expect(typeof MOCK_SUBGRAPHS).toBe('object');
});

it('returns the per-context-graph override when one is defined', async () => {
const result = await mockApi.fetchSubGraphs('cg:pharma-drug-interactions');

expect(result).toEqual(MOCK_SUBGRAPHS['cg:pharma-drug-interactions']);
expect(result.contextGraphId).toBe('cg:pharma-drug-interactions');
expect(result.subGraphs).toHaveLength(2);
expect(result.subGraphs.map((s) => s.name)).toEqual(['Interactions', 'Contraindications']);
});

it('falls back to an empty list for a context graph with no override', async () => {
// `cg:supply-chain-eu` is a real mock CG deliberately left out of
// MOCK_SUBGRAPHS so the fallback path stays covered.
expect(MOCK_SUBGRAPHS['cg:supply-chain-eu']).toBeUndefined();

const result = await mockApi.fetchSubGraphs('cg:supply-chain-eu');

expect(result).toEqual({ contextGraphId: 'cg:supply-chain-eu', subGraphs: [] });
});

it('falls back to an empty list for an entirely unknown context graph', async () => {
const result = await mockApi.fetchSubGraphs('cg:does-not-exist');

expect(result).toEqual({ contextGraphId: 'cg:does-not-exist', subGraphs: [] });
});

it('keys every override by its own contextGraphId, matching the real response shape', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — The "matching the real response shape" test asserts nothing about the shape

The case is titled keys every override by its own contextGraphId, matching the real response shape, but the only per-sub-graph assertions are typeof subGraph.name === 'string', typeof subGraph.uri === 'string' and Number.isFinite(...) on the two counts (lines 46-49). That is satisfied by any two non-empty strings — it is precisely why the two shape defects above ship green.

The invariants that actually define the response are cheap to assert here:

  • expect(subGraph.uri).toBe(\did:dkg:context-graph:${id}/${subGraph.name}`)— matchescontextGraphSubGraphUri/generateSubGraphRegistration`.
  • expect(validateSubGraphName(subGraph.name).valid).toBe(true)@origintrail-official/dkg-core, the same guard createSubGraph runs.
  • expect(typeof subGraph.createdBy).toBe('string') — required on the daemon's own listSubGraphs return type.

As written, the suite pins the export against regression (which it does well) but gives no protection on the thing its title claims to cover.

Concrete failure scenario

Someone adds a fourth mock sub-graph with name: 'my/sub graph' and uri: 'nonsense'. Every assertion in this test passes, the fixture is impossible for the daemon to produce, and nothing in CI notices.

Self-review: raised by an independent reviewer pass, then verified against the PR head before posting.

for (const [id, entry] of Object.entries(MOCK_SUBGRAPHS)) {
expect(entry.contextGraphId).toBe(id);
for (const subGraph of entry.subGraphs) {
expect(typeof subGraph.name).toBe('string');
expect(typeof subGraph.uri).toBe('string');
expect(Number.isFinite(subGraph.entityCount)).toBe(true);
expect(Number.isFinite(subGraph.tripleCount)).toBe(true);
}
}
});

it('only overrides context graphs that mock mode actually lists', () => {
const knownCgIds = new Set(MOCK_CONTEXT_GRAPHS.contextGraphs.map((cg) => cg.id));
for (const id of Object.keys(MOCK_SUBGRAPHS)) {
expect(knownCgIds.has(id)).toBe(true);
}
});
});
Loading