mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
test(authority): expand e2e roundtrip coverage
This commit is contained in:
@@ -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: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:stable": "node scripts/run-test-suite.mjs",
|
||||||
"test:authority:e2e": "node tests/e2e/authority-server-primary.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",
|
"test:all": "npm run test:stable",
|
||||||
"check": "node scripts/check-syntax.mjs"
|
"check": "node scripts/check-syntax.mjs"
|
||||||
},
|
},
|
||||||
|
|||||||
156
tests/e2e/authority-checkpoint-restore.mjs
Normal file
156
tests/e2e/authority-checkpoint-restore.mjs
Normal file
@@ -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");
|
||||||
140
tests/e2e/authority-diagnostics-roundtrip.mjs
Normal file
140
tests/e2e/authority-diagnostics-roundtrip.mjs
Normal file
@@ -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");
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
|
|
||||||
import { probeAuthorityCapabilities } from "../../runtime/authority-capabilities.js";
|
import { probeAuthorityCapabilities } from "../../runtime/authority-capabilities.js";
|
||||||
import { AuthorityGraphStore } from "../../sync/authority-graph-store.js";
|
import { AuthorityGraphStore } from "../../sync/authority-graph-store.js";
|
||||||
@@ -18,161 +17,28 @@ import {
|
|||||||
createAuthorityJobAdapter,
|
createAuthorityJobAdapter,
|
||||||
} from "../../maintenance/authority-job-adapter.js";
|
} from "../../maintenance/authority-job-adapter.js";
|
||||||
import { createAuthorityBlobAdapter } from "../../maintenance/authority-blob-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 context = createAuthorityE2eContext({
|
||||||
const baseUrl = String(env.AUTHORITY_E2E_BASE_URL || "").trim();
|
skipMessage:
|
||||||
|
"authority-server-primary E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server",
|
||||||
|
});
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (context.skip) {
|
||||||
console.log("authority-server-primary E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server");
|
console.log(context.skipMessage);
|
||||||
process.exit(0);
|
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 runContext = {
|
||||||
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 = {
|
|
||||||
baseUrl: resolvedBaseUrl,
|
baseUrl: resolvedBaseUrl,
|
||||||
chatId,
|
chatId,
|
||||||
namespace,
|
namespace,
|
||||||
@@ -180,9 +46,9 @@ const context = {
|
|||||||
blobPath,
|
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({
|
const state = await probeAuthorityCapabilities({
|
||||||
settings: { authorityBaseUrl: resolvedBaseUrl },
|
settings: { authorityBaseUrl: resolvedBaseUrl },
|
||||||
fetchImpl,
|
fetchImpl,
|
||||||
@@ -198,7 +64,7 @@ await runStep("probe", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await runStep("sql", async () => {
|
await runAuthorityE2eStep("sql", async () => {
|
||||||
const store = new AuthorityGraphStore(chatId, {
|
const store = new AuthorityGraphStore(chatId, {
|
||||||
baseUrl: resolvedBaseUrl,
|
baseUrl: resolvedBaseUrl,
|
||||||
fetchImpl,
|
fetchImpl,
|
||||||
@@ -216,7 +82,7 @@ await runStep("sql", async () => {
|
|||||||
|
|
||||||
const commitResult = await store.commitDelta(
|
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 },
|
runtimeMetaPatch: { authorityE2eRunId: runId },
|
||||||
},
|
},
|
||||||
{ reason: "authority-e2e-contract", markSyncDirty: false },
|
{ 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 config = normalizeAuthorityVectorConfig({ authorityBaseUrl: resolvedBaseUrl });
|
||||||
const entries = buildVectorEntries(graph);
|
const entries = buildAuthorityE2eVectorEntries(graph);
|
||||||
await purgeAuthorityTriviumNamespace(config, {
|
await purgeAuthorityTriviumNamespace(config, {
|
||||||
namespace,
|
namespace,
|
||||||
collectionId,
|
collectionId,
|
||||||
@@ -328,12 +194,12 @@ await runStep("trivium", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await runStep("jobs", async () => {
|
await runAuthorityE2eStep("jobs", async () => {
|
||||||
const adapter = createAuthorityJobAdapter(
|
const adapter = createAuthorityJobAdapter(
|
||||||
{
|
{
|
||||||
authorityBaseUrl: resolvedBaseUrl,
|
authorityBaseUrl: resolvedBaseUrl,
|
||||||
pollIntervalMs: 500,
|
pollIntervalMs: 500,
|
||||||
waitTimeoutMs: jobWaitTimeoutMs,
|
waitTimeoutMs: context.jobWaitTimeoutMs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fetchImpl,
|
fetchImpl,
|
||||||
@@ -343,7 +209,7 @@ await runStep("jobs", async () => {
|
|||||||
const listBefore = await adapter.listPage({ limit: 5 });
|
const listBefore = await adapter.listPage({ limit: 5 });
|
||||||
assert.ok(Array.isArray(listBefore.jobs));
|
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({
|
const idempotencyKey = buildAuthorityJobIdempotencyKey({
|
||||||
kind,
|
kind,
|
||||||
chatId,
|
chatId,
|
||||||
@@ -368,7 +234,7 @@ await runStep("jobs", async () => {
|
|||||||
assert.ok(submitted.id);
|
assert.ok(submitted.id);
|
||||||
|
|
||||||
const waited = await adapter.waitForCompletion(submitted.id, {
|
const waited = await adapter.waitForCompletion(submitted.id, {
|
||||||
timeoutMs: jobWaitTimeoutMs,
|
timeoutMs: context.jobWaitTimeoutMs,
|
||||||
pollIntervalMs: 500,
|
pollIntervalMs: 500,
|
||||||
});
|
});
|
||||||
assert.ok(waited.id || waited.status);
|
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(
|
const adapter = createAuthorityBlobAdapter(
|
||||||
{ authorityBaseUrl: resolvedBaseUrl, authorityBlobNamespace: namespace },
|
{ authorityBaseUrl: resolvedBaseUrl, authorityBlobNamespace: namespace },
|
||||||
{ fetchImpl, headerProvider },
|
{ fetchImpl, headerProvider },
|
||||||
|
|||||||
200
tests/helpers/authority-e2e-context.mjs
Normal file
200
tests/helpers/authority-e2e-context.mjs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user