diff --git a/crates/buzz-relay/src/api/dkg_query.rs b/crates/buzz-relay/src/api/dkg_query.rs index 85cfc8c3df8..8f24d766e70 100644 --- a/crates/buzz-relay/src/api/dkg_query.rs +++ b/crates/buzz-relay/src/api/dkg_query.rs @@ -27,7 +27,9 @@ use super::{api_error, bridge, internal_error, not_found}; /// Maximum public request body accepted by `/api/dkg/query`. pub(crate) const MAX_REQUEST_BYTES: usize = 16 * 1024; -pub(super) const MAX_RESPONSE_BYTES: usize = 2 * 1024 * 1024; +// A channel graph may contain up to 10,000 bounded triples. Keep the relay +// ceiling finite, but large enough for that documented response contract. +pub(super) const MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024; const MAX_NAME_BYTES: usize = 256; const MAX_URI_BYTES: usize = 2048; const MAX_SPARQL_BYTES: usize = 8 * 1024; @@ -61,6 +63,7 @@ enum Operation { ReputationSummary, SubgraphGraph, SubgraphTriples, + ChannelTriples, Evidence, SemanticQuery, } @@ -231,7 +234,7 @@ fn parse_and_sanitize_request( } let arguments = match request.operation { - Operation::ChannelMemory => { + Operation::ChannelMemory | Operation::ChannelTriples => { let arguments: EmptyArguments = parse_arguments(request.arguments)?; serde_json::to_value(arguments) } @@ -513,6 +516,7 @@ mod tests { let requester = requester(); for (operation, arguments) in [ ("channel_memory", serde_json::json!({})), + ("channel_triples", serde_json::json!({})), ("trust_network", serde_json::json!({})), ( "reputation_summary", diff --git a/crates/buzz-relay/src/nip11.rs b/crates/buzz-relay/src/nip11.rs index bfe409f62cf..b5c172f9e84 100644 --- a/crates/buzz-relay/src/nip11.rs +++ b/crates/buzz-relay/src/nip11.rs @@ -245,6 +245,7 @@ fn dkg_memory_descriptor(trust_enabled: bool) -> serde_json::Value { "decision_trace", "subgraph_graph", "subgraph_triples", + "channel_triples", "evidence", "semantic_query", ]; @@ -469,6 +470,9 @@ mod tests { assert!(descriptor["query_operations"] .as_array() .is_some_and(|operations| operations.contains(&serde_json::json!("semantic_query")))); + assert!(descriptor["query_operations"] + .as_array() + .is_some_and(|operations| operations.contains(&serde_json::json!("channel_triples")))); assert!(descriptor["query_operations"].as_array().is_some_and( |operations| operations.contains(&serde_json::json!("reputation_summary")) )); diff --git a/crates/buzz-relay/src/router.rs b/crates/buzz-relay/src/router.rs index a15474e4b86..db555170754 100644 --- a/crates/buzz-relay/src/router.rs +++ b/crates/buzz-relay/src/router.rs @@ -14,7 +14,7 @@ use axum::{ }; use serde_json::json; use tower::ServiceExt; -use tower_http::cors::{AllowOrigin, CorsLayer}; +use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; use tower_http::limit::RequestBodyLimitLayer; use tower_http::services::ServeDir; use tower_http::trace::{HttpMakeClassifier, TraceLayer}; @@ -443,7 +443,10 @@ async fn mesh_status_handler(State(state): State>) -> impl IntoRes /// Build a CORS layer from the configured origins list. fn build_cors_layer(cors_origins: &[String]) -> CorsLayer { if cors_origins.is_empty() { - return CorsLayer::permissive(); + return CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods(tower_http::cors::Any) + .allow_headers(AllowHeaders::mirror_request()); } let origins: Vec = cors_origins @@ -463,7 +466,10 @@ fn build_cors_layer(cors_origins: &[String]) -> CorsLayer { CorsLayer::new() .allow_origin(AllowOrigin::list(origins)) .allow_methods(tower_http::cors::Any) - .allow_headers(tower_http::cors::Any) + // `Authorization` is a CORS non-wildcard request header. WebKit rejects + // `Access-Control-Allow-Headers: *` for NIP-98 requests, so echo the + // browser's requested header list explicitly. + .allow_headers(AllowHeaders::mirror_request()) } #[cfg(test)] @@ -511,6 +517,39 @@ mod tests { assert!(!should_serve_spa("/arbitrary", true)); } + #[tokio::test] + async fn cors_preflight_explicitly_allows_nip98_authorization() { + let app = Router::new() + .route("/api/dkg/query", axum::routing::post(|| async {})) + .layer(build_cors_layer(&[])); + + let response = app + .oneshot( + Request::builder() + .method("OPTIONS") + .uri("/api/dkg/query") + .header(axum::http::header::ORIGIN, "tauri://localhost") + .header(axum::http::header::ACCESS_CONTROL_REQUEST_METHOD, "POST") + .header( + axum::http::header::ACCESS_CONTROL_REQUEST_HEADERS, + "authorization,content-type", + ) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get(axum::http::header::ACCESS_CONTROL_ALLOW_HEADERS) + .unwrap(), + "authorization,content-type" + ); + } + #[tokio::test(flavor = "current_thread")] async fn http_and_datastore_spans_are_exported_in_the_same_trace() { let exporter = InMemorySpanExporter::default(); diff --git a/desktop/src/features/dkg-memory/api.ts b/desktop/src/features/dkg-memory/api.ts index 1d23ff2ba2c..db370b2ba27 100644 --- a/desktop/src/features/dkg-memory/api.ts +++ b/desktop/src/features/dkg-memory/api.ts @@ -302,7 +302,8 @@ export async function runDkgDiagnostics( const query = await timedDiagnostic(() => fetchSemanticQuery( channelId, - `PREFIX schema: \nASK WHERE { GRAPH ?g { ?entity schema:name ?name . } }`, + `PREFIX schema: \nSELECT ?entity WHERE { GRAPH ?g { ?entity schema:name ?name . } } LIMIT 1`, + "shared", ), ); checks.push({ diff --git a/desktop/src/features/dkg-memory/provider.ts b/desktop/src/features/dkg-memory/provider.ts index 9815d73481c..eef8e063c2a 100644 --- a/desktop/src/features/dkg-memory/provider.ts +++ b/desktop/src/features/dkg-memory/provider.ts @@ -3,6 +3,8 @@ import { getRelayHttpUrl, signRelayEvent } from "@/shared/api/tauri"; const LOCAL_EXPLORER = "http://127.0.0.1:9295"; const LOCAL_PROBE_TIMEOUT_MS = 1_500; const QUERY_TIMEOUT_MS = 25_000; +const CHANNEL_MEMORY_TIMEOUT_MS = 90_000; +const CHANNEL_GRAPH_TIMEOUT_MS = 115_000; const NIP98_KIND = 27235; export type ExplorerSource = "local" | "gateway"; @@ -16,6 +18,7 @@ export type DkgQueryOperation = | "reputation_summary" | "subgraph_graph" | "subgraph_triples" + | "channel_triples" | "evidence" | "semantic_query"; @@ -36,6 +39,7 @@ type DkgQueryArguments = { reputation_summary: { pubkey: string }; subgraph_graph: { name: string }; subgraph_triples: { name: string }; + channel_triples: Record; evidence: { uri: string }; semantic_query: { sparql: string; @@ -264,6 +268,7 @@ function adaptCommunityResult( case "reputation_summary": case "subgraph_graph": case "subgraph_triples": + case "channel_triples": case "semantic_query": return { ...envelope.result, gate: "ok", cg: envelope.cg }; case "evidence": @@ -288,6 +293,12 @@ async function communityGatewayQuery< const { result } = await postAuthenticatedDkgJson({ path: "/api/dkg/query", body, + timeoutMs: + query.operation === "channel_triples" + ? CHANNEL_GRAPH_TIMEOUT_MS + : query.operation === "channel_memory" + ? CHANNEL_MEMORY_TIMEOUT_MS + : QUERY_TIMEOUT_MS, }); const envelope = validateEnvelope(result, query); return adaptCommunityResult(envelope) as Result; diff --git a/desktop/src/features/dkg-memory/topology/TopologyView.tsx b/desktop/src/features/dkg-memory/topology/TopologyView.tsx index 6e13fe4657a..3549de851b0 100644 --- a/desktop/src/features/dkg-memory/topology/TopologyView.tsx +++ b/desktop/src/features/dkg-memory/topology/TopologyView.tsx @@ -69,7 +69,7 @@ export const NODE_UI_GRAPH_OPTIONS = { edgeWidth: 0.9, }, hexagon: { baseSize: 4, minSize: 3, maxSize: 6, scaleWithDegree: true }, - focus: { maxNodes: 3000, hops: 999 }, + focus: { maxNodes: 20_000, hops: 999 }, }; export interface TopologyNeighbor { @@ -113,8 +113,12 @@ export function TopologyView({ const shaped = useMemo(() => { const triples = query.data?.triples ?? []; const bounded = applyHeaviestSubjectsCap(triples); - const { canvasTriples, singletonItems } = - splitGraphTriplesForShelf(bounded); + const shelf = splitGraphTriplesForShelf(bounded); + // The channel action is explicitly the complete Context Graph view: keep + // every returned triple in the renderer. A named subgraph retains the + // compact singleton shelf used by the focused inspection UI. + const canvasTriples = channelWide ? bounded : shelf.canvasTriples; + const singletonItems = channelWide ? [] : shelf.singletonItems; return { canvasTriples, singletonItems, @@ -122,7 +126,7 @@ export function TopologyView({ legend: attributionLegend(bounded), dropped: triples.length - bounded.length, }; - }, [query.data]); + }, [channelWide, query.data]); const graphData = useMemo( () => @@ -219,14 +223,14 @@ export function TopologyView({ ))} - {summary.entities} connected entities · {summary.relationships}{" "} - relationships ·{" "} + {summary.entities} entities · {summary.relationships} relationships ·{" "} {channelWide - ? "bounded channel view · use search to narrow further" + ? `complete channel view · up to ${query.data?.limit?.toLocaleString() ?? "10,000"} triples` : colorMode === "attribution" ? "colors = recorded attribution, not verification" : "colors = entity types, as in your DKG node"} - {shaped.dropped > 0 && ` · ${shaped.dropped} triples beyond cap`} + {(query.data?.truncated || shaped.dropped > 0) && + " · graph reached the safety cap"} diff --git a/desktop/src/features/dkg-memory/topology/client.test.mjs b/desktop/src/features/dkg-memory/topology/client.test.mjs index 2832a62fd9d..27958acce50 100644 --- a/desktop/src/features/dkg-memory/topology/client.test.mjs +++ b/desktop/src/features/dkg-memory/topology/client.test.mjs @@ -69,7 +69,7 @@ test("topology falls back to the authenticated channel-scoped gateway operation" } }); -test("channel topology fetches labels for bounded relationship endpoints", async () => { +test("channel topology uses one authenticated full-graph operation", async () => { const previousFetch = globalThis.fetch; const previousWindow = globalThis.window; const previousLocalStorage = globalThis.localStorage; @@ -87,64 +87,27 @@ test("channel topology fetches labels for bounded relationship endpoints", async }, }; globalThis.localStorage = { getItem: () => null }; - globalThis.fetch = async (_url, init) => { + globalThis.fetch = async (url, init) => { const request = JSON.parse(init.body); - requests.push(request); - const sparql = request.arguments.sparql; - let bindings; - if (sparql.includes("VALUES ?subject")) { - bindings = [ - { - subject: "urn:memory:hello", - predicate: "http://schema.org/name", - object: '"Hello World memory"', - }, - { - subject: "urn:decision:responsive", - predicate: "http://schema.org/name", - object: '"Build a responsive page"', - }, - { - subject: "urn:component:page", - predicate: "http://schema.org/name", - object: '"Hello World page"', - }, - ]; - } else if (sparql.includes("memory:contains")) { - bindings = [ - { - subject: "urn:memory:hello", - predicate: "http://dkg.io/ontology/memory/contains", - object: "urn:decision:responsive", - }, - ]; - } else if (sparql.includes("decisions:affects")) { - bindings = [ - { - subject: "urn:decision:responsive", - predicate: "http://dkg.io/ontology/decisions/affects", - object: "urn:component:page", - }, - ]; - } else { - bindings = [ - { - subject: "urn:decision:responsive", - predicate: "http://www.w3.org/ns/prov#wasDerivedFrom", - object: "urn:nostr:event:source", - }, - ]; - } + requests.push({ url: String(url), request, init }); return new Response( JSON.stringify({ ok: true, channelId: request.channelId, cg: "server-cg", - operation: "semantic_query", + operation: "channel_triples", result: { - queryType: "select", - scope: { type: "current_channel" }, - layers: [{ layer: "SWM", bindings }], + limit: 10_000, + truncated: false, + triples: [ + { + subject: "urn:memory:hello", + predicate: "http://dkg.io/ontology/memory/contains", + object: "urn:decision:responsive", + layer: "SWM", + agent: "fizz", + }, + ], }, }), ); @@ -158,142 +121,17 @@ test("channel topology fetches labels for bounded relationship endpoints", async ); assert.equal(result.gate, "ok"); assert.equal(result.cg, "server-cg"); - assert.equal(requests.length, 4); - assert.ok( - requests.every((request) => request.operation === "semantic_query"), - ); - assert.match(requests[0].arguments.sparql, /memory:contains/); - assert.match(requests[1].arguments.sparql, /decisions:affects/); - assert.match(requests[2].arguments.sparql, /prov:wasDerivedFrom/); - const metadataQuery = requests.at(-1).arguments.sparql; - assert.match(metadataQuery, /VALUES \?subject/); - assert.match(metadataQuery, //); - assert.match(metadataQuery, //); - assert.match(metadataQuery, //); - assert.match(metadataQuery, //); - assert.deepEqual( - result.triples - .filter((triple) => !triple.object.startsWith('"')) - .map(({ subject, predicate, object }) => [subject, predicate, object]), - [ - [ - "urn:memory:hello", - "http://dkg.io/ontology/memory/contains", - "urn:decision:responsive", - ], - [ - "urn:decision:responsive", - "http://dkg.io/ontology/decisions/affects", - "urn:component:page", - ], - [ - "urn:decision:responsive", - "http://www.w3.org/ns/prov#wasDerivedFrom", - "urn:nostr:event:source", - ], - ], - ); - } finally { - resetDkgMemoryProvider(); - globalThis.fetch = previousFetch; - globalThis.window = previousWindow; - globalThis.localStorage = previousLocalStorage; - } -}); - -test("channel topology retains successful slices and rejects when every relation slice fails", async () => { - const previousFetch = globalThis.fetch; - const previousWindow = globalThis.window; - const previousLocalStorage = globalThis.localStorage; - globalThis.window = { - ...(globalThis.window ?? {}), - __TAURI_INTERNALS__: { - invoke: async (command, args) => { - if (command === "get_relay_http_url") return "https://relay.example"; - if (command === "sign_event") { - return JSON.stringify({ kind: 27235, content: "", tags: args.tags }); - } - throw new Error(`unexpected Tauri command: ${command}`); - }, - }, - }; - globalThis.localStorage = { getItem: () => null }; - - const ok = (request, bindings) => - new Response( - JSON.stringify({ - ok: true, - channelId: request.channelId, - cg: "server-cg", - operation: "semantic_query", - result: { - queryType: "select", - scope: { type: "current_channel" }, - layers: [{ layer: "SWM", bindings }], - }, - }), - ); - const busy = () => - new Response( - JSON.stringify({ - ok: false, - error: { code: "upstream_busy", message: "Blazegraph is busy" }, - }), - { status: 503, headers: { "content-type": "application/json" } }, - ); - - try { - const partialRequests = []; - globalThis.fetch = async (_url, init) => { - const request = JSON.parse(init.body); - partialRequests.push(request); - const sparql = request.arguments.sparql; - if (partialRequests.length === 1) return busy(); - if (sparql.includes("decisions:affects")) { - return ok(request, [ - { - subject: "urn:decision:x402", - predicate: "http://dkg.io/ontology/decisions/affects", - object: "urn:component:payments", - }, - ]); - } - if (sparql.includes("VALUES ?subject")) { - return ok(request, [ - { - subject: "urn:decision:x402", - predicate: "http://schema.org/name", - object: '"Adopt x402 payments"', - }, - ]); - } - return ok(request, []); - }; - - const partial = await fetchTopologyTriples("partial-channel", null, { - kind: "channel", + assert.equal(result.limit, 10_000); + assert.equal(result.truncated, false); + assert.equal(requests.length, 1); + assert.equal(requests[0].url, "https://relay.example/api/dkg/query"); + assert.deepEqual(requests[0].request, { + channelId: "550e8400-e29b-41d4-a716-446655440000", + operation: "channel_triples", + arguments: {}, }); - assert.equal(partial.gate, "ok"); - assert.equal(partialRequests.length, 4); - assert.match(partialRequests.at(-1).arguments.sparql, /VALUES \?subject/); - assert.ok( - partial.triples.some( - ({ predicate }) => - predicate === "http://dkg.io/ontology/decisions/affects", - ), - ); - - resetDkgMemoryProvider(); - let failedRequests = 0; - globalThis.fetch = async () => { - failedRequests += 1; - return busy(); - }; - await assert.rejects( - fetchTopologyTriples("failed-channel", null, { kind: "channel" }), - /Blazegraph is busy/, - ); - assert.equal(failedRequests, 3); + assert.match(requests[0].init.headers.Authorization, /^Nostr /); + assert.equal(result.triples.length, 1); } finally { resetDkgMemoryProvider(); globalThis.fetch = previousFetch; diff --git a/desktop/src/features/dkg-memory/topology/client.ts b/desktop/src/features/dkg-memory/topology/client.ts index 28483abee3d..da1710eae98 100644 --- a/desktop/src/features/dkg-memory/topology/client.ts +++ b/desktop/src/features/dkg-memory/topology/client.ts @@ -2,12 +2,6 @@ // selection and authenticated community fallback; render code remains transport // agnostic and remote authorization remains channel-scoped. import { queryDkgProvider } from "../provider"; -import { - buildTopologyEndpointMetadataQuery, - CHANNEL_TOPOLOGY_FALLBACK_NODE_QUERY, - CHANNEL_TOPOLOGY_RELATION_QUERIES, - semanticBindingString, -} from "../semanticQueries"; export type TopologyTarget = | { kind: "channel" } @@ -28,6 +22,8 @@ export interface TopologyData { cg?: string; subgraph?: string; triples?: TopologyTriple[]; + limit?: number; + truncated?: boolean; } export async function fetchTopologyTriples( @@ -36,7 +32,12 @@ export async function fetchTopologyTriples( target: TopologyTarget, ): Promise { if (target.kind === "channel") { - return fetchChannelTopologyTriples(channelId); + return queryDkgProvider({ + channelId, + operation: "channel_triples", + arguments: {}, + localPath: null, + }); } return queryDkgProvider({ channelId, @@ -47,168 +48,3 @@ export async function fetchTopologyTriples( : null, }); } - -type SemanticLayer = { - layer: "SWM" | "VM"; - bindings: Record[]; -}; - -type SemanticResult = { - gate: TopologyData["gate"]; - cg?: string; - layers: SemanticLayer[]; -}; - -const MAX_TOPOLOGY_NODES = 30; -const MAX_TOPOLOGY_EDGES = 80; - -const INVALID_IRI_PUNCTUATION = new Set([ - "<", - ">", - '"', - "{", - "}", - "|", - "\\", - "^", - "`", -]); - -function hasInvalidIriCharacter(value: string): boolean { - return [...value].some( - (character) => - character.charCodeAt(0) <= 0x20 || INVALID_IRI_PUNCTUATION.has(character), - ); -} - -function resourceIri(value: string): string | null { - const trimmed = value.trim(); - const iri = - trimmed.startsWith("<") && trimmed.endsWith(">") - ? trimmed.slice(1, -1) - : trimmed; - if (!/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(iri) || hasInvalidIriCharacter(iri)) { - return null; - } - return iri; -} - -function triplesFrom(result: SemanticResult): TopologyTriple[] { - const triples: TopologyTriple[] = []; - for (const layer of result.layers ?? []) { - for (const binding of layer.bindings ?? []) { - const subject = semanticBindingString(binding.subject); - const predicate = semanticBindingString(binding.predicate); - const object = semanticBindingString(binding.object); - if (!subject || !predicate || !object) continue; - triples.push({ - subject, - predicate, - object, - layer: layer.layer, - agent: layer.layer, - }); - } - } - return triples; -} - -function uniqueTriples(triples: TopologyTriple[]): TopologyTriple[] { - const seen = new Set(); - return triples.filter((triple) => { - const key = `${triple.layer}|${triple.subject}|${triple.predicate}|${triple.object}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); -} - -/** - * Select only relationship triples whose endpoints fit in one small, fully - * labelable graph. Input order is meaningful: containment establishes memory - * clusters first, domain edges enrich them, and provenance is last. - */ -function boundedRelationships(triples: TopologyTriple[]): { - relations: TopologyTriple[]; - endpoints: string[]; -} { - const relations: TopologyTriple[] = []; - const endpoints = new Set(); - for (const triple of uniqueTriples(triples)) { - if (relations.length >= MAX_TOPOLOGY_EDGES) break; - const subject = resourceIri(triple.subject); - const object = resourceIri(triple.object); - if (!subject || !object) continue; - const additions = - Number(!endpoints.has(subject)) + Number(!endpoints.has(object)); - if (endpoints.size + additions > MAX_TOPOLOGY_NODES) continue; - endpoints.add(subject); - endpoints.add(object); - relations.push(triple); - } - return { relations, endpoints: [...endpoints] }; -} - -async function semanticQuery( - channelId: string, - sparql: string, -): Promise { - return queryDkgProvider({ - channelId, - operation: "semantic_query", - arguments: { sparql, view: "both" }, - localPath: null, - }); -} - -async function fetchChannelTopologyTriples( - channelId: string, -): Promise { - const relationResults: SemanticResult[] = []; - const relationErrors: unknown[] = []; - // Blazegraph also serves the DKG node's mainnet workload. Keep this explicit - // graph action serialized so opening the panel does not create its own burst, - // and retain successful slices when one upstream read is transiently busy. - for (const sparql of CHANNEL_TOPOLOGY_RELATION_QUERIES) { - try { - relationResults.push(await semanticQuery(channelId, sparql)); - } catch (cause) { - relationErrors.push(cause); - } - } - if (relationResults.length === 0) throw relationErrors[0]; - const { relations, endpoints } = boundedRelationships( - relationResults.flatMap(triplesFrom), - ); - if (relations.length === 0 && relationErrors.length > 0) { - throw relationErrors[0]; - } - - // A genuinely relation-free channel still gets the old standalone-entity - // shelf. Otherwise labels and types are fetched only for endpoints that are - // guaranteed to appear in the connected canvas. - let metadata: SemanticResult | null = null; - try { - metadata = await semanticQuery( - channelId, - endpoints.length > 0 - ? buildTopologyEndpointMetadataQuery(endpoints) - : CHANNEL_TOPOLOGY_FALLBACK_NODE_QUERY, - ); - } catch (cause) { - if (relations.length === 0) throw cause; - // URI-labelled relationships remain useful and truthful while the DKG is - // busy; a later reopen can enrich them with human labels and types. - } - const triples = uniqueTriples([ - ...relations, - ...(metadata ? triplesFrom(metadata) : []), - ]); - return { - gate: "ok", - cg: - metadata?.cg ?? - relationResults.find((result) => typeof result.cg === "string")?.cg, - triples, - }; -} diff --git a/desktop/src/features/dkg-memory/topology/topology.ts b/desktop/src/features/dkg-memory/topology/topology.ts index 0d842561b5b..012f39efe0c 100644 --- a/desktop/src/features/dkg-memory/topology/topology.ts +++ b/desktop/src/features/dkg-memory/topology/topology.ts @@ -128,7 +128,7 @@ export function topologySummary(triples: TopologyTriple[]): { return { entities: entities.size, relationships: relationships.size }; } -const MAX_TRIPLES = 2500; +const MAX_TRIPLES = 10_000; export function applyHeaviestSubjectsCap( triples: TopologyTriple[], max: number = MAX_TRIPLES, diff --git a/desktop/tests/e2e/dkg-memory-beta.spec.ts b/desktop/tests/e2e/dkg-memory-beta.spec.ts index 0348d50aa6e..406d8a822d2 100644 --- a/desktop/tests/e2e/dkg-memory-beta.spec.ts +++ b/desktop/tests/e2e/dkg-memory-beta.spec.ts @@ -19,6 +19,7 @@ async function advertiseDkgMemory(page: import("@playwright/test").Page) { profiles: ["dkg-memory@1", "dkg-trust@1"], query_operations: [ "channel_memory", + "channel_triples", "semantic_query", "trust_network", "reputation_summary", @@ -49,6 +50,7 @@ test("channel memory exposes graph and authenticated search without named subgra profiles: ["dkg-memory@1", "dkg-trust@1"], query_operations: [ "channel_memory", + "channel_triples", "semantic_query", "trust_network", "reputation_summary", @@ -92,6 +94,41 @@ test("channel memory exposes graph and authenticated search without named subgra ], subgraphs: [], }; + } else if (request.operation === "channel_triples") { + result = { + limit: 10_000, + truncated: false, + triples: [ + { + subject: "urn:decision:query-proxy", + predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + object: "http://dkg.io/ontology/decisions/Decision", + layer: "SWM", + agent: "fizz", + }, + { + subject: "urn:decision:query-proxy", + predicate: "http://schema.org/name", + object: '"Use an authenticated relay query proxy"', + layer: "SWM", + agent: "fizz", + }, + { + subject: "urn:decision:query-proxy", + predicate: "http://dkg.io/ontology/decisions/affects", + object: "urn:component:memory-panel", + layer: "SWM", + agent: "fizz", + }, + { + subject: "urn:component:memory-panel", + predicate: "http://schema.org/name", + object: '"Buzz memory panel"', + layer: "SWM", + agent: "fizz", + }, + ], + }; } else if (request.operation === "semantic_query") { if (request.arguments.sparql?.includes('"query"')) { searchRequests.push({ diff --git a/desktop/tests/e2e/dkg-memory-fallback.spec.ts b/desktop/tests/e2e/dkg-memory-fallback.spec.ts index 95386e83a80..18a0eaa08d7 100644 --- a/desktop/tests/e2e/dkg-memory-fallback.spec.ts +++ b/desktop/tests/e2e/dkg-memory-fallback.spec.ts @@ -338,9 +338,9 @@ test("named subgraph lens queries the provider and keeps Graph available", async await expect( overlay.getByRole("button", { name: "Contributors" }), ).toBeVisible(); - await expect( - overlay.getByText(/2 connected entities · 1 relationships/i), - ).toBeVisible({ timeout: 15_000 }); + await expect(overlay.getByText(/2 entities · 1 relationships/i)).toBeVisible({ + timeout: 15_000, + }); await expectPaintedGraphCanvas(overlay); expect(tripleRequests).toHaveLength(1); expect(new URL(tripleRequests[0]).searchParams.get("name")).toBe(