diff --git a/tests/cultlib-root.mjs b/tests/cultlib-root.mjs new file mode 100644 index 0000000..619c403 --- /dev/null +++ b/tests/cultlib-root.mjs @@ -0,0 +1,5 @@ +import { resolve } from "node:path"; + +// The CultLib checkout the tests load at run time, as the tools resolve it: +// $VOIDBOT_CULTLIB_ROOT, else the sibling ../CultLib. Its dist/ is gitignored; build it there first. +export const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(import.meta.dirname, "..", "..", "CultLib")); diff --git a/tests/persona-feedback-cli.test.mjs b/tests/persona-feedback-cli.test.mjs index f4fec1d..704d587 100644 --- a/tests/persona-feedback-cli.test.mjs +++ b/tests/persona-feedback-cli.test.mjs @@ -9,7 +9,8 @@ import { resolve } from "node:path"; import { buildFeedbackDefinitions } from "../tools/persona-feedback-documents.mjs"; import { conversationFromDiscordMessage, discordGatewayReady } from "../tools/bifrost-discord-persona-ingress.mjs"; -const root=resolve(import.meta.dirname,".."), cult=resolve(root,"..","CultLib"); +import { cultLibRoot } from "./cultlib-root.mjs"; +const root=resolve(import.meta.dirname,".."), cult=cultLibRoot; const cr=createRequire(resolve(cult,"packages","cultcache-ts","package.json")); const mr=createRequire(resolve(cult,"packages","cultmesh-ts","package.json")); const {defineDocumentType}=cr("@gamecult/cultcache-ts"), {CultMesh}=mr("@gamecult/cultmesh-ts"), defs=buildFeedbackDefinitions(defineDocumentType); diff --git a/tests/persona-feedback-idunn-health.test.mjs b/tests/persona-feedback-idunn-health.test.mjs index 360a15a..80c3a0f 100644 --- a/tests/persona-feedback-idunn-health.test.mjs +++ b/tests/persona-feedback-idunn-health.test.mjs @@ -8,7 +8,8 @@ import { resolve } from "node:path"; import { enrollBifrostFeedbackIdentity, providerHealthIdentity, signFeedbackAdmission } from "../tools/bifrost-feedback-identity.mjs"; import { createPersonaFeedbackHealthPublisher, createSignedHealthRawPut, createSignedPersonaFeedbackHealth, signedHealthSourceRole } from "../tools/persona-feedback-idunn-health.mjs"; -const root=resolve(import.meta.dirname,".."),cult=resolve(root,"..","CultLib"),require=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),{encode,decode}=require("@msgpack/msgpack"); +import { cultLibRoot } from "./cultlib-root.mjs"; +const root=resolve(import.meta.dirname,".."),cult=cultLibRoot,require=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),{encode,decode}=require("@msgpack/msgpack"); const idDomain=Buffer.from("gamecult.provider-health.identity.v1\0"),signatureDomain=Buffer.from("gamecult.provider-health.signature.v1\0"),purpose=Buffer.from("idunn.signed_daemon_health.v1"); test("Persona-feedback emits the exact canonical signed daemon-health tuple",async()=>{ diff --git a/tests/persona-matrix-mouth-runtime.test.mjs b/tests/persona-matrix-mouth-runtime.test.mjs index 098a422..4d2b0c6 100644 --- a/tests/persona-matrix-mouth-runtime.test.mjs +++ b/tests/persona-matrix-mouth-runtime.test.mjs @@ -1,9 +1,10 @@ // The parts of the Matrix mouth that sit on CultLib: the chat_* wire documents, the // CultCache cursor and brake stores, and the CultNet RUDP endpoint. Needs the built -// CultLib TypeScript packages (CULTLIB_ROOT, else ../CultLib), like the other Bifrost tests. CultLib's dist/ is gitignored: +// CultLib TypeScript packages (VOIDBOT_CULTLIB_ROOT, else ../CultLib), like the other Bifrost tests. CultLib's dist/ is gitignored: // run `npm ci && npm run build:ts` there first. Since CultLib 286a9af the packages are @gamecult/cultcache-ts, // @gamecult/cultnet-ts and @gamecult/cultmesh-ts; the directories under packages/ keep their short names. import test from "node:test"; +import { cultLibRoot } from "./cultlib-root.mjs"; import assert from "node:assert/strict"; import dgram from "node:dgram"; import { createServer } from "node:http"; @@ -15,7 +16,7 @@ import * as docs from "../tools/persona-chat-documents.mjs"; import { createMouth, cultcacheState, connectionIdFor, createBrakeReader, loadRuntime, pageOf, startChatEndpoint } from "../tools/persona-matrix-mouth.mjs"; const root = resolve(import.meta.dirname, ".."); -const runtime = loadRuntime(resolve(process.env.CULTLIB_ROOT ?? resolve(root, "..", "CultLib"))); +const runtime = loadRuntime(cultLibRoot); const { msgpack } = runtime; const fixtureDir = resolve(import.meta.dirname, "fixtures", "persona-chat-v1"); @@ -256,7 +257,7 @@ test("one process serves N Personas from systemd credentials: each token only in probe.close(); const child = spawn(process.execPath, [resolve(root, "tools", "persona-matrix-mouth.mjs")], { env: { - PATH: process.env.PATH, CULTLIB_ROOT: resolve(process.env.CULTLIB_ROOT ?? resolve(root, "..", "CultLib")), PERSONAS: "eureka,nova", CREDENTIALS_DIRECTORY: creds, CONTROL_ROOT: control, + PATH: process.env.PATH, CULTLIB_ROOT: cultLibRoot, PERSONAS: "eureka,nova", CREDENTIALS_DIRECTORY: creds, CONTROL_ROOT: control, STATE_DIRECTORY: stateDir, MATRIX_HOMESERVER: `http://127.0.0.1:${hs.address().port}`, MATRIX_ROOMS: "#swarm:t", MATRIX_SERVER_NAME: "t", CHAT_RUDP_LISTEN: `127.0.0.1:${port}`, }, }); diff --git a/tests/shared-value-stores.test.mjs b/tests/shared-value-stores.test.mjs new file mode 100644 index 0000000..092f15a --- /dev/null +++ b/tests/shared-value-stores.test.mjs @@ -0,0 +1,86 @@ +// Six document types once declared `indexes:` on grouping fields (status, repo, agent, topic...) that many rows share. +// CultLib's `indexes:` means unique, so each store must still write, close and reopen with rows that share those values. +// Real CLIs and store openers against temp files; CultLib comes from tests/cultlib-root.mjs. +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import { cultLibRoot } from "./cultlib-root.mjs"; +import { openCrossingReceiptStore } from "../tools/bifrost-crossing-documents.mjs"; +import { buildAuthorizedRelease, openRepositoryReleaseAuthorityStore } from "../tools/bifrost-repository-release-authority.mjs"; + +const root = resolve(import.meta.dirname, ".."); +const dir = mkdtempSync(resolve(tmpdir(), "bifrost-shared-")); +test.after(() => rmSync(dir, { recursive: true, force: true })); + +const env = { + ...process.env, + VOIDBOT_CULTLIB_ROOT: cultLibRoot, + BIFROST_SKIP_LOCAL_ENV: "true", + BIFROST_ROOT_VERSE: "test-verse", + BIFROST_MACHINE_ID: "test-machine", + BIFROST_CROSSING_RECEIPT_STORE: resolve(dir, "receipts.cc"), +}; +for (const name of ["BIFROST_BRIDGE_BASE_URL", "BIFROST_BRIDGE_TOKEN", "BIFROST_ALLOW_UNRECEIPTED_ACTIVITY"]) delete env[name]; + +function cli(tool, args) { + const run = spawnSync(process.execPath, [resolve(root, "tools", tool), ...args], { env, encoding: "utf8", cwd: root }); + assert.equal(run.status, 0, `${tool} ${args[0]}\n${run.stderr}`); + return run.stdout; +} + +test("agent-transport store holds requests that share repo, agent and status", () => { + const store = resolve(dir, "agent-transport.cc"); + for (const id of ["req-1", "req-2", "req-3"]) { + cli("agent-transport.mjs", ["enqueue", "--store", store, "--id", id, "--repo", "Bifrost", "--agent", "heimdall", "--title", id, "--request", "body", "--allow-unmirrored", "true"]); + } + const listed = JSON.parse(cli("agent-transport.mjs", ["list", "--store", store])); + assert.deepEqual(listed.map((request) => request.id).sort(), ["req-1", "req-2", "req-3"]); +}); + +test("governance-threads store holds topics and comments that share their grouping fields", () => { + const store = resolve(dir, "governance.cc"); + for (const id of ["topic-1", "topic-2"]) { + cli("governance-threads.mjs", ["open", "--store", store, "--id", id, "--title", id, "--repo", "Bifrost", "--agent", "heimdall", "--summary", "s", "--allow-unmirrored", "true"]); + } + for (const id of ["c-1", "c-2"]) { + cli("governance-threads.mjs", ["comment", "--store", store, "--topic", "topic-1", "--id", id, "--author", "heimdall", "--stance", "support", "--body", "same", "--allow-unmirrored", "true"]); + } + const topics = JSON.parse(cli("governance-threads.mjs", ["list", "--store", store])); + assert.deepEqual(topics.map((topic) => topic.id).sort(), ["topic-1", "topic-2"]); + const shown = JSON.parse(cli("governance-threads.mjs", ["show", "--store", store, "--topic", "topic-1"])); + assert.equal(JSON.stringify(shown).match(/"id": ?"c-\d"/g)?.length ?? 0, 2); +}); + +test("crossing receipt store reopens with receipts that share status, kind and command", async () => { + const receipts = await openCrossingReceiptStore(env.BIFROST_CROSSING_RECEIPT_STORE); + const before = receipts.getAll().length; + assert.ok(before >= 5, "the CLI runs above leave completed agent_transport.request receipts"); + const reopened = await openCrossingReceiptStore(env.BIFROST_CROSSING_RECEIPT_STORE); + assert.equal(reopened.getAll().length, before); + assert.ok(new Set(reopened.getAll().map((receipt) => `${receipt.status}/${receipt.crossingKind}`)).size < before); +}); + +test("release authority store reopens with authorities that share repository, ref and status", async () => { + const path = resolve(dir, "release-authority.cc"); + const authority = (commitSha) => buildAuthorizedRelease({ + repositoryFullName: "GameCult/Bifrost", upstreamRef: "refs/heads/main", commitSha, + commandId: `cmd-${commitSha[0]}`, crossingReceiptId: `crossing-${commitSha[0]}`, policyDecisionId: "policy", authorityReference: "ref", + actorIdentity: "heimdall", sourceKind: "test", sourceId: "src", epiphanyRunId: "run", epiphanyLaneId: "lane", epiphanyAgentIdentity: "heimdall", + externalReceiptUrl: "https://example.test/r", externalReceiptId: "r", authorizedAt: "2026-11-11T00:00:00.000Z", + }); + const store = await openRepositoryReleaseAuthorityStore(path); + await store.put(authority("a".repeat(40))); + await store.put(authority("b".repeat(40))); + const reopened = await openRepositoryReleaseAuthorityStore(path); + assert.equal(reopened.getAll().length, 2); +}); + +test("provider advertisement store exports and re-exports beside other documents", () => { + const out = resolve(dir, "provider.cc"); + cli("provider-advertisement.mjs", ["export", "--out", out]); + const second = JSON.parse(cli("provider-advertisement.mjs", ["export", "--out", out])); + assert.equal(second.ok, true); +}); diff --git a/tests/watchdog-notice.test.mjs b/tests/watchdog-notice.test.mjs index 21d27aa..954c051 100644 --- a/tests/watchdog-notice.test.mjs +++ b/tests/watchdog-notice.test.mjs @@ -1,3 +1,4 @@ +import { cultLibRoot } from "./cultlib-root.mjs"; import test from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; @@ -26,8 +27,7 @@ const T_OPEN = "2023-11-14T22:13:20.000Z"; const T_CLOSE = "2023-11-14T22:15:00.000Z"; const CLOSED_TEXT = `Idunn incident closed (recovered): continuity-exhausted on \`closed-target\`, opened ${T_OPEN}, closed ${T_CLOSE}`; -// CultLib is the sibling ../CultLib, like the other tests. -const cult = resolve(import.meta.dirname, "..", "..", "CultLib"); +const cult = cultLibRoot; const cache = createRequire(resolve(cult, "packages", "cultcache-ts", "package.json"))("@gamecult/cultcache-ts"); const msgpack = createRequire(resolve(cult, "packages", "cultnet-ts", "package.json"))("@msgpack/msgpack"); diff --git a/tools/agent-transport.mjs b/tools/agent-transport.mjs index ab124fa..b187b81 100644 --- a/tools/agent-transport.mjs +++ b/tools/agent-transport.mjs @@ -21,13 +21,15 @@ const defaultPersonaName = "Bifrost"; const defaultPersonaAvatarUrl = "https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png"; +const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(projectsRoot, "CultLib")); + const cultCachePackagePath = resolveFirstExisting("CultCache TypeScript runtime", [ - resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"), + resolve(cultLibRoot, "packages", "cultcache-ts", "package.json"), resolve(projectsRoot, "CultCacheTS", "package.json"), resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"), ]); const cultNetPackagePath = resolveFirstExisting("CultNet TypeScript runtime", [ - resolve(projectsRoot, "CultLib", "packages", "cultnet-ts", "package.json"), + resolve(cultLibRoot, "packages", "cultnet-ts", "package.json"), resolve(projectsRoot, "CultNetTS", "package.json"), ]); const cultCacheRequire = createRequire(cultCachePackagePath); diff --git a/tools/governance-threads.mjs b/tools/governance-threads.mjs index d2eae86..7359ceb 100644 --- a/tools/governance-threads.mjs +++ b/tools/governance-threads.mjs @@ -23,8 +23,10 @@ const defaultPersonaName = "Bifrost"; const defaultPersonaAvatarUrl = "https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png"; +const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(projectsRoot, "CultLib")); + const cultCachePackagePath = resolveFirstExisting("CultCache TypeScript runtime", [ - resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"), + resolve(cultLibRoot, "packages", "cultcache-ts", "package.json"), resolve(projectsRoot, "CultCacheTS", "package.json"), resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"), ]);