From 6747263eb28dce04eed9e79ffb9e79efba4fca06 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Tue, 28 Apr 2026 17:41:22 +0800 Subject: [PATCH] test(authority): expand e2e roundtrip coverage --- package.json | 3 + tests/e2e/authority-checkpoint-restore.mjs | 156 ++++++++++++++ tests/e2e/authority-diagnostics-roundtrip.mjs | 140 ++++++++++++ tests/e2e/authority-server-primary.mjs | 190 +++-------------- tests/helpers/authority-e2e-context.mjs | 200 ++++++++++++++++++ 5 files changed, 527 insertions(+), 162 deletions(-) create mode 100644 tests/e2e/authority-checkpoint-restore.mjs create mode 100644 tests/e2e/authority-diagnostics-roundtrip.mjs create mode 100644 tests/helpers/authority-e2e-context.mjs diff --git a/package.json b/package.json index 239cb74..76d945d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,9 @@ "test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb", "test:stable": "node scripts/run-test-suite.mjs", "test:authority:e2e": "node tests/e2e/authority-server-primary.mjs", + "test:authority:e2e:diagnostics": "node tests/e2e/authority-diagnostics-roundtrip.mjs", + "test:authority:e2e:restore": "node tests/e2e/authority-checkpoint-restore.mjs", + "test:authority:e2e:all": "npm run test:authority:e2e && npm run test:authority:e2e:diagnostics && npm run test:authority:e2e:restore", "test:all": "npm run test:stable", "check": "node scripts/check-syntax.mjs" }, diff --git a/tests/e2e/authority-checkpoint-restore.mjs b/tests/e2e/authority-checkpoint-restore.mjs new file mode 100644 index 0000000..babb6c7 --- /dev/null +++ b/tests/e2e/authority-checkpoint-restore.mjs @@ -0,0 +1,156 @@ +import assert from "node:assert/strict"; + +import { buildLukerGraphCheckpointV2 } from "../../graph/graph-persistence.js"; +import { + applyAuthorityCheckpointToStore, + buildAuthorityConsistencyAudit, + buildAuthorityCheckpointImportSnapshot, +} from "../../maintenance/authority-consistency.js"; +import { createAuthorityBlobAdapter } from "../../maintenance/authority-blob-adapter.js"; +import { AuthorityGraphStore } from "../../sync/authority-graph-store.js"; +import { + createAuthorityE2eContext, + createAuthorityE2eContractGraph, + runAuthorityE2eStep, +} from "../helpers/authority-e2e-context.mjs"; + +const context = createAuthorityE2eContext({ + skipMessage: + "authority checkpoint restore E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server", +}); + +if (context.skip) { + console.log(context.skipMessage); + process.exit(0); +} + +const checkpointPath = String( + context.env.AUTHORITY_E2E_CHECKPOINT_PATH || + `user/files/ST-BME_luker_checkpoint_${context.runId}-restore.json`, +); +const graph = createAuthorityE2eContractGraph(context.chatId, `${context.runId}-restore`, { + revision: 5, +}); +const checkpoint = buildLukerGraphCheckpointV2(graph, { + revision: graph.meta.revision, + chatId: context.chatId, + integrity: `${context.runId}-integrity`, + reason: "authority-e2e-checkpoint-restore", + storageTier: "authority-e2e", +}); + +console.log( + `authority-checkpoint-restore E2E started: ${JSON.stringify({ + baseUrl: context.baseUrl, + chatId: context.chatId, + checkpointPath, + })}`, +); + +await runAuthorityE2eStep("checkpoint-restore-roundtrip", async () => { + const adapter = createAuthorityBlobAdapter( + { + authorityBaseUrl: context.baseUrl, + authorityBlobNamespace: context.namespace, + }, + { + fetchImpl: context.fetchImpl, + headerProvider: context.headerProvider, + }, + ); + const store = new AuthorityGraphStore(context.chatId, { + baseUrl: context.baseUrl, + fetchImpl: context.fetchImpl, + headerProvider: context.headerProvider, + }); + let deleteResult = null; + try { + const prepared = buildAuthorityCheckpointImportSnapshot(checkpoint, { + chatId: context.chatId, + path: checkpointPath, + source: "authority-e2e-checkpoint-restore", + }); + assert.equal(prepared.ok, true); + + const writeResult = await adapter.writeJson(checkpointPath, checkpoint, { + metadata: { + chatId: context.chatId, + revision: graph.meta.revision, + reason: "authority-e2e-checkpoint-restore", + kind: "luker-checkpoint", + }, + }); + assert.equal(writeResult.ok, true); + + await store.open(); + await store.clearAll(); + const restoreResult = await applyAuthorityCheckpointToStore(store, checkpoint, { + chatId: context.chatId, + path: checkpointPath, + source: "authority-e2e-checkpoint-restore", + markSyncDirty: false, + }); + assert.equal(restoreResult.ok, true); + assert.equal(restoreResult.restored, true); + + const snapshot = await store.exportSnapshot({ includeTombstones: false }); + assert.equal(snapshot.meta.chatId, context.chatId); + assert.equal(snapshot.meta.revision, graph.meta.revision); + assert.equal(snapshot.nodes.length, graph.nodes.length); + assert.ok(snapshot.nodes.some((node) => node.id === graph.nodes[0].id)); + + const blobRead = await adapter.readJson(checkpointPath); + assert.equal(blobRead.exists, true); + const audit = buildAuthorityConsistencyAudit({ + chatId: context.chatId, + collectionId: context.collectionId, + capability: { + blobReady: true, + }, + runtimeGraph: { + meta: { revision: graph.meta.revision }, + nodes: graph.nodes, + edges: graph.edges, + vectorIndexState: { + collectionId: context.collectionId, + dirty: false, + }, + }, + graphPersistenceState: { + chatId: context.chatId, + revision: graph.meta.revision, + authorityBlobCheckpointPath: checkpointPath, + authorityBlobCheckpointRevision: graph.meta.revision, + }, + sqlSnapshot: snapshot, + blobResult: { + ok: true, + exists: true, + path: checkpointPath, + checkpoint: blobRead.payload, + }, + triviumStat: { + revision: graph.meta.revision, + namespace: context.collectionId, + itemCount: graph.nodes.length, + linkCount: graph.edges.length, + }, + }); + assert.equal(audit.summary.level, "success"); + assert.equal(audit.drift.checkpointRestorable, true); + return { + revision: snapshot.meta.revision, + nodes: snapshot.nodes.length, + actions: audit.actions, + }; + } finally { + await store.clearAll().catch(() => null); + await store.close().catch(() => null); + deleteResult = await adapter.delete(checkpointPath).catch(() => null); + if (deleteResult) { + assert.equal(deleteResult.ok, true); + } + } +}); + +console.log("authority-checkpoint-restore E2E passed"); diff --git a/tests/e2e/authority-diagnostics-roundtrip.mjs b/tests/e2e/authority-diagnostics-roundtrip.mjs new file mode 100644 index 0000000..f408a70 --- /dev/null +++ b/tests/e2e/authority-diagnostics-roundtrip.mjs @@ -0,0 +1,140 @@ +import assert from "node:assert/strict"; + +import { + buildAuthorityDiagnosticsBundle, + buildAuthorityPerformanceBaseline, + writeAuthorityDiagnosticsBundle, +} from "../../maintenance/authority-diagnostics-bundle.js"; +import { createAuthorityBlobAdapter } from "../../maintenance/authority-blob-adapter.js"; +import { + createAuthorityE2eContext, + createAuthorityE2eContractGraph, + runAuthorityE2eStep, +} from "../helpers/authority-e2e-context.mjs"; + +const context = createAuthorityE2eContext({ + skipMessage: + "authority diagnostics E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server", +}); + +if (context.skip) { + console.log(context.skipMessage); + process.exit(0); +} + +const diagnosticsPath = String( + context.env.AUTHORITY_E2E_DIAGNOSTICS_PATH || `st-bme/e2e/${context.runId}/diagnostics.json`, +); +const graph = createAuthorityE2eContractGraph(context.chatId, `${context.runId}-diagnostics`, { + revision: 3, +}); +const graphPersistence = { + chatId: context.chatId, + revision: graph.meta.revision, + loadState: "loaded", + loadDiagnostics: { + source: "authority-sql", + totalMs: 12, + hydrateMs: 4, + updatedAt: new Date().toISOString(), + }, + persistDelta: { + totalMs: 8, + commitMs: 3, + commitPayloadBytes: 256, + updatedAt: new Date().toISOString(), + }, + authorityRecentJobs: [ + { id: `${context.runId}-job-ok`, queueState: "success" }, + { id: `${context.runId}-job-running`, queueState: "running" }, + ], + authorityLastJobId: `${context.runId}-job-running`, + authorityLastJobStatus: "running", + authorityConsistencyState: "success", + authorityBlobCheckpointPath: `st-bme/e2e/${context.runId}/checkpoint.json`, + authorityBlobCheckpointRevision: graph.meta.revision, +}; +const baseline = buildAuthorityPerformanceBaseline({ + chatId: context.chatId, + graphPersistence, + graph, + consistencyAudit: { + issues: [], + sql: { revision: graph.meta.revision }, + trivium: { revision: graph.meta.revision }, + blob: { revision: graph.meta.revision }, + summary: { level: "success" }, + }, +}); + +console.log( + `authority-diagnostics-roundtrip E2E started: ${JSON.stringify({ + baseUrl: context.baseUrl, + chatId: context.chatId, + namespace: context.namespace, + diagnosticsPath, + })}`, +); + +await runAuthorityE2eStep("diagnostics-roundtrip", async () => { + const adapter = createAuthorityBlobAdapter( + { + authorityBaseUrl: context.baseUrl, + authorityBlobNamespace: context.namespace, + }, + { + fetchImpl: context.fetchImpl, + headerProvider: context.headerProvider, + }, + ); + const bundle = buildAuthorityDiagnosticsBundle({ + chatId: context.chatId, + reason: "authority-e2e-diagnostics-roundtrip", + settings: { + authorityBaseUrl: context.baseUrl, + authorityApiKey: "secret-for-redaction-check", + }, + graphPersistence, + graph, + runtimeStatus: { + text: "authority-e2e", + level: "success", + updatedAt: Date.now(), + }, + performanceBaseline: baseline, + lastExtract: graph.nodes.slice(0, 1), + lastRecall: graph.nodes.slice(1), + }); + assert.equal(bundle.settings.authorityApiKey, "[REDACTED]"); + let deleteResult = null; + try { + const writeResult = await writeAuthorityDiagnosticsBundle(adapter, bundle, { + chatId: context.chatId, + reason: "authority-e2e-diagnostics-roundtrip", + path: diagnosticsPath, + }); + assert.equal(writeResult.ok, true); + + const statResult = await adapter.stat(diagnosticsPath); + assert.equal(statResult.exists, true); + + const readResult = await adapter.readJson(diagnosticsPath); + assert.equal(readResult.exists, true); + assert.equal(readResult.payload.kind, "st-bme-authority-diagnostics"); + assert.equal(readResult.payload.chatId, context.chatId); + assert.equal(readResult.payload.performanceBaseline.graphRevision, graph.meta.revision); + assert.equal(readResult.payload.settings.authorityApiKey, "[REDACTED]"); + return { + path: writeResult.path, + graphRevision: readResult.payload.performanceBaseline.graphRevision, + nodeCount: readResult.payload.graphSummary.nodeCount, + }; + } finally { + deleteResult = await adapter.delete(diagnosticsPath).catch(() => null); + if (deleteResult) { + assert.equal(deleteResult.ok, true); + } + } +}); + +console.log("authority-diagnostics-roundtrip E2E passed"); diff --git a/tests/e2e/authority-server-primary.mjs b/tests/e2e/authority-server-primary.mjs index fde9666..9c5b98c 100644 --- a/tests/e2e/authority-server-primary.mjs +++ b/tests/e2e/authority-server-primary.mjs @@ -1,5 +1,4 @@ import assert from "node:assert/strict"; -import { randomUUID } from "node:crypto"; import { probeAuthorityCapabilities } from "../../runtime/authority-capabilities.js"; import { AuthorityGraphStore } from "../../sync/authority-graph-store.js"; @@ -18,161 +17,28 @@ import { createAuthorityJobAdapter, } from "../../maintenance/authority-job-adapter.js"; import { createAuthorityBlobAdapter } from "../../maintenance/authority-blob-adapter.js"; +import { + buildAuthorityE2eVectorEntries, + createAuthorityE2eContext, + createAuthorityE2eContractGraph, + createAuthorityE2eContractNode, + runAuthorityE2eStep, +} from "../helpers/authority-e2e-context.mjs"; -const env = process.env; -const baseUrl = String(env.AUTHORITY_E2E_BASE_URL || "").trim(); +const context = createAuthorityE2eContext({ + skipMessage: + "authority-server-primary E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server", +}); -if (!baseUrl) { - console.log("authority-server-primary E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server"); +if (context.skip) { + console.log(context.skipMessage); process.exit(0); } +const resolvedBaseUrl = context.baseUrl; +const { chatId, namespace, collectionId, blobPath, fetchImpl, headerProvider, runId } = context; +const graph = createAuthorityE2eContractGraph(chatId, runId); -function parsePositiveInteger(value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) { - const parsed = Number(value); - if (!Number.isFinite(parsed)) return fallback; - return Math.min(max, Math.max(min, Math.trunc(parsed))); -} - -function parseJsonObject(value, fallback = {}) { - if (!value) return fallback; - try { - const parsed = JSON.parse(String(value)); - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback; - } catch { - return fallback; - } -} - -function resolveBaseUrl(value) { - const normalized = String(value || "").replace(/\/+$/g, ""); - if (/^https?:\/\//i.test(normalized)) return normalized; - const origin = String(env.AUTHORITY_E2E_ORIGIN || "").replace(/\/+$/g, ""); - if (origin && normalized.startsWith("/")) return `${origin}${normalized}`; - throw new Error("AUTHORITY_E2E_BASE_URL must be absolute, or set AUTHORITY_E2E_ORIGIN for relative plugin paths"); -} - -function createHeaderProvider() { - const staticHeaders = parseJsonObject(env.AUTHORITY_E2E_HEADER_JSON, {}); - const token = String(env.AUTHORITY_E2E_TOKEN || "").trim(); - const cookie = String(env.AUTHORITY_E2E_COOKIE || "").trim(); - return () => ({ - ...staticHeaders, - ...(token ? { Authorization: /^Bearer\s+/i.test(token) ? token : `Bearer ${token}` } : {}), - ...(cookie ? { Cookie: cookie } : {}), - }); -} - -function createFetchWithTimeout(timeoutMs) { - return async (url, options = {}) => { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(new Error(`Authority E2E request timeout after ${timeoutMs}ms`)), timeoutMs); - try { - return await fetch(url, { - ...options, - signal: controller.signal, - }); - } finally { - clearTimeout(timer); - } - }; -} - -function createContractNode(id, title, nowMs) { - return { - id, - type: "fact", - fields: { - title, - summary: `${title} generated by Authority server-primary E2E contract smoke`, - }, - seqRange: [1, 1], - scope: { - layer: "global", - ownerType: "system", - ownerId: "authority-e2e", - bucket: "contract", - regionKey: "authority-e2e-region", - }, - storySegmentId: "authority-e2e-segment", - importance: 0.8, - archived: false, - updatedAt: nowMs, - }; -} - -function createContractGraph(chatId, runId) { - const nowMs = Date.now(); - const nodeA = createContractNode(`${runId}-node-a`, "Authority E2E Alpha", nowMs); - const nodeB = createContractNode(`${runId}-node-b`, "Authority E2E Beta", nowMs); - return { - meta: { - schemaVersion: 1, - chatId, - deviceId: "authority-e2e", - revision: 1, - lastModified: nowMs, - nodeCount: 2, - edgeCount: 1, - tombstoneCount: 0, - }, - nodes: [nodeA, nodeB], - edges: [ - { - id: `${runId}-edge-a-b`, - fromId: nodeA.id, - toId: nodeB.id, - relation: "related", - type: "semantic", - strength: 0.7, - updatedAt: nowMs, - }, - ], - tombstones: [], - state: { - lastProcessedFloor: 1, - extractionCount: 1, - }, - }; -} - -function buildVectorEntries(graph) { - return graph.nodes.map((node, index) => ({ - nodeId: node.id, - index, - hash: `${node.id}:hash`, - text: `${node.fields.title}. ${node.fields.summary}`, - })); -} - -async function runStep(name, fn) { - const startedAt = Date.now(); - try { - const result = await fn(); - const durationMs = Date.now() - startedAt; - console.log(`authority E2E ${name}: ok (${durationMs}ms)`); - return { name, ok: true, durationMs, result }; - } catch (error) { - const durationMs = Date.now() - startedAt; - console.error(`authority E2E ${name}: failed (${durationMs}ms)`); - console.error(error?.stack || error?.message || String(error)); - throw error; - } -} - -const resolvedBaseUrl = resolveBaseUrl(baseUrl); -const timeoutMs = parsePositiveInteger(env.AUTHORITY_E2E_TIMEOUT_MS, 15000, 1000, 300000); -const jobWaitTimeoutMs = parsePositiveInteger(env.AUTHORITY_E2E_JOB_WAIT_TIMEOUT_MS, 15000, 1000, 300000); -const headerProvider = createHeaderProvider(); -const fetchImpl = createFetchWithTimeout(timeoutMs); -const runId = String(env.AUTHORITY_E2E_RUN_ID || `authority-e2e-${Date.now()}-${randomUUID().slice(0, 8)}`) - .replace(/[^A-Za-z0-9._:-]+/g, "-"); -const chatId = String(env.AUTHORITY_E2E_CHAT_ID || `st-bme-${runId}`); -const namespace = String(env.AUTHORITY_E2E_NAMESPACE || `st-bme-e2e-${runId}`); -const collectionId = String(env.AUTHORITY_E2E_COLLECTION_ID || `${namespace}::${chatId}`); -const blobPath = String(env.AUTHORITY_E2E_BLOB_PATH || `st-bme/e2e/${runId}/contract.json`); -const graph = createContractGraph(chatId, runId); - -const context = { +const runContext = { baseUrl: resolvedBaseUrl, chatId, namespace, @@ -180,9 +46,9 @@ const context = { blobPath, }; -console.log(`authority-server-primary E2E started: ${JSON.stringify(context)}`); +console.log(`authority-server-primary E2E started: ${JSON.stringify(runContext)}`); -await runStep("probe", async () => { +await runAuthorityE2eStep("probe", async () => { const state = await probeAuthorityCapabilities({ settings: { authorityBaseUrl: resolvedBaseUrl }, fetchImpl, @@ -198,7 +64,7 @@ await runStep("probe", async () => { }; }); -await runStep("sql", async () => { +await runAuthorityE2eStep("sql", async () => { const store = new AuthorityGraphStore(chatId, { baseUrl: resolvedBaseUrl, fetchImpl, @@ -216,7 +82,7 @@ await runStep("sql", async () => { const commitResult = await store.commitDelta( { - upsertNodes: [createContractNode(`${runId}-node-c`, "Authority E2E Gamma", Date.now())], + upsertNodes: [createAuthorityE2eContractNode(`${runId}-node-c`, "Authority E2E Gamma", Date.now())], runtimeMetaPatch: { authorityE2eRunId: runId }, }, { reason: "authority-e2e-contract", markSyncDirty: false }, @@ -238,9 +104,9 @@ await runStep("sql", async () => { } }); -await runStep("trivium", async () => { +await runAuthorityE2eStep("trivium", async () => { const config = normalizeAuthorityVectorConfig({ authorityBaseUrl: resolvedBaseUrl }); - const entries = buildVectorEntries(graph); + const entries = buildAuthorityE2eVectorEntries(graph); await purgeAuthorityTriviumNamespace(config, { namespace, collectionId, @@ -328,12 +194,12 @@ await runStep("trivium", async () => { } }); -await runStep("jobs", async () => { +await runAuthorityE2eStep("jobs", async () => { const adapter = createAuthorityJobAdapter( { authorityBaseUrl: resolvedBaseUrl, pollIntervalMs: 500, - waitTimeoutMs: jobWaitTimeoutMs, + waitTimeoutMs: context.jobWaitTimeoutMs, }, { fetchImpl, @@ -343,7 +209,7 @@ await runStep("jobs", async () => { const listBefore = await adapter.listPage({ limit: 5 }); assert.ok(Array.isArray(listBefore.jobs)); - const kind = String(env.AUTHORITY_E2E_JOB_KIND || "authority.vector.rebuild"); + const kind = String(context.env.AUTHORITY_E2E_JOB_KIND || "authority.vector.rebuild"); const idempotencyKey = buildAuthorityJobIdempotencyKey({ kind, chatId, @@ -368,7 +234,7 @@ await runStep("jobs", async () => { assert.ok(submitted.id); const waited = await adapter.waitForCompletion(submitted.id, { - timeoutMs: jobWaitTimeoutMs, + timeoutMs: context.jobWaitTimeoutMs, pollIntervalMs: 500, }); assert.ok(waited.id || waited.status); @@ -387,7 +253,7 @@ await runStep("jobs", async () => { }; }); -await runStep("blob", async () => { +await runAuthorityE2eStep("blob", async () => { const adapter = createAuthorityBlobAdapter( { authorityBaseUrl: resolvedBaseUrl, authorityBlobNamespace: namespace }, { fetchImpl, headerProvider }, diff --git a/tests/helpers/authority-e2e-context.mjs b/tests/helpers/authority-e2e-context.mjs new file mode 100644 index 0000000..f008732 --- /dev/null +++ b/tests/helpers/authority-e2e-context.mjs @@ -0,0 +1,200 @@ +import { randomUUID } from "node:crypto"; + +function parsePositiveInteger(value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(max, Math.max(min, Math.trunc(parsed))); +} + +function parseJsonObject(value, fallback = {}) { + if (!value) return fallback; + try { + const parsed = JSON.parse(String(value)); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback; + } catch { + return fallback; + } +} + +export function resolveAuthorityE2eBaseUrl(value, env = process.env) { + const normalized = String(value || "").replace(/\/+$/g, ""); + if (/^https?:\/\//i.test(normalized)) return normalized; + const origin = String(env.AUTHORITY_E2E_ORIGIN || "").replace(/\/+$/g, ""); + if (origin && normalized.startsWith("/")) return `${origin}${normalized}`; + throw new Error( + "AUTHORITY_E2E_BASE_URL must be absolute, or set AUTHORITY_E2E_ORIGIN for relative plugin paths", + ); +} + +export function createAuthorityE2eHeaderProvider(env = process.env) { + const staticHeaders = parseJsonObject(env.AUTHORITY_E2E_HEADER_JSON, {}); + const token = String(env.AUTHORITY_E2E_TOKEN || "").trim(); + const cookie = String(env.AUTHORITY_E2E_COOKIE || "").trim(); + return () => ({ + ...staticHeaders, + ...(token ? { Authorization: /^Bearer\s+/i.test(token) ? token : `Bearer ${token}` } : {}), + ...(cookie ? { Cookie: cookie } : {}), + }); +} + +export function createAuthorityE2eFetchWithTimeout( + timeoutMs, + baseFetchImpl = typeof fetch === "function" ? fetch.bind(globalThis) : null, +) { + if (typeof baseFetchImpl !== "function") { + throw new Error("Authority E2E fetch unavailable"); + } + return async (url, options = {}) => { + const controller = new AbortController(); + const timer = setTimeout(() => { + controller.abort(new Error(`Authority E2E request timeout after ${timeoutMs}ms`)); + }, timeoutMs); + try { + return await baseFetchImpl(url, { + ...options, + signal: controller.signal, + }); + } finally { + clearTimeout(timer); + } + }; +} + +export function createAuthorityE2eContext(options = {}) { + const env = + options.env && typeof options.env === "object" && !Array.isArray(options.env) + ? options.env + : process.env; + const skipMessage = String( + options.skipMessage || + "authority E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server", + ); + const baseUrl = String(env.AUTHORITY_E2E_BASE_URL || "").trim(); + if (!baseUrl) { + return { + skip: true, + skipMessage, + env, + }; + } + const resolvedBaseUrl = resolveAuthorityE2eBaseUrl(baseUrl, env); + const timeoutMs = parsePositiveInteger(env.AUTHORITY_E2E_TIMEOUT_MS, 15000, 1000, 300000); + const jobWaitTimeoutMs = parsePositiveInteger( + env.AUTHORITY_E2E_JOB_WAIT_TIMEOUT_MS, + 15000, + 1000, + 300000, + ); + const fetchImpl = createAuthorityE2eFetchWithTimeout( + timeoutMs, + options.fetchImpl || (typeof fetch === "function" ? fetch.bind(globalThis) : null), + ); + const headerProvider = createAuthorityE2eHeaderProvider(env); + const runId = String( + env.AUTHORITY_E2E_RUN_ID || `authority-e2e-${Date.now()}-${randomUUID().slice(0, 8)}`, + ).replace(/[^A-Za-z0-9._:-]+/g, "-"); + const chatId = String(env.AUTHORITY_E2E_CHAT_ID || `st-bme-${runId}`); + const namespace = String(env.AUTHORITY_E2E_NAMESPACE || `st-bme-e2e-${runId}`); + const collectionId = String(env.AUTHORITY_E2E_COLLECTION_ID || `${namespace}::${chatId}`); + const blobPath = String(env.AUTHORITY_E2E_BLOB_PATH || `st-bme/e2e/${runId}/contract.json`); + return { + skip: false, + env, + baseUrl: resolvedBaseUrl, + timeoutMs, + jobWaitTimeoutMs, + fetchImpl, + headerProvider, + runId, + chatId, + namespace, + collectionId, + blobPath, + }; +} + +export function createAuthorityE2eContractNode(id, title, nowMs) { + return { + id, + type: "fact", + fields: { + title, + summary: `${title} generated by Authority server-primary E2E contract smoke`, + }, + seqRange: [1, 1], + scope: { + layer: "global", + ownerType: "system", + ownerId: "authority-e2e", + bucket: "contract", + regionKey: "authority-e2e-region", + }, + storySegmentId: "authority-e2e-segment", + importance: 0.8, + archived: false, + updatedAt: nowMs, + }; +} + +export function createAuthorityE2eContractGraph(chatId, runId, options = {}) { + const nowMs = Number(options.nowMs || Date.now()); + const revision = Number.isFinite(Number(options.revision)) + ? Math.max(1, Math.trunc(Number(options.revision))) + : 1; + const nodeA = createAuthorityE2eContractNode(`${runId}-node-a`, "Authority E2E Alpha", nowMs); + const nodeB = createAuthorityE2eContractNode(`${runId}-node-b`, "Authority E2E Beta", nowMs); + return { + meta: { + schemaVersion: 1, + chatId, + deviceId: "authority-e2e", + revision, + lastModified: nowMs, + nodeCount: 2, + edgeCount: 1, + tombstoneCount: 0, + }, + nodes: [nodeA, nodeB], + edges: [ + { + id: `${runId}-edge-a-b`, + fromId: nodeA.id, + toId: nodeB.id, + relation: "related", + type: "semantic", + strength: 0.7, + updatedAt: nowMs, + }, + ], + tombstones: [], + state: { + lastProcessedFloor: 1, + extractionCount: 1, + }, + }; +} + +export function buildAuthorityE2eVectorEntries(graph = null) { + const nodes = Array.isArray(graph?.nodes) ? graph.nodes : []; + return nodes.map((node, index) => ({ + nodeId: node.id, + index, + hash: `${node.id}:hash`, + text: `${node.fields?.title || node.id}. ${node.fields?.summary || ""}`, + })); +} + +export async function runAuthorityE2eStep(name, fn) { + const startedAt = Date.now(); + try { + const result = await fn(); + const durationMs = Date.now() - startedAt; + console.log(`authority E2E ${name}: ok (${durationMs}ms)`); + return { name, ok: true, durationMs, result }; + } catch (error) { + const durationMs = Date.now() - startedAt; + console.error(`authority E2E ${name}: failed (${durationMs}ms)`); + console.error(error?.stack || error?.message || String(error)); + throw error; + } +}