import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createClientRuleRegistry, ClientRuleRegistryError } from "../plugin/core/rules.js"; import { createInitialClientState, createYonexusClientStateStore, ensureClientKeyPair, hasClientKeyPair, hasClientSecret, loadYonexusClientState, saveYonexusClientState, YonexusClientStateCorruptionError, type YonexusClientState } from "../plugin/core/state.js"; import { signMessage, verifySignature } from "../plugin/crypto/keypair.js"; const tempDirs: string[] = []; afterEach(async () => { await Promise.all( tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })) ); }); async function createTempStatePath(): Promise { const dir = await mkdtemp(join(tmpdir(), "yonexus-client-test-")); tempDirs.push(dir); return join(dir, "state.json"); } describe("Yonexus.Client state store", () => { it("creates minimal initial state when the file does not exist", async () => { const filePath = await createTempStatePath(); const state = await loadYonexusClientState(filePath, "client-a"); expect(state.identifier).toBe("client-a"); expect(hasClientSecret(state)).toBe(false); expect(hasClientKeyPair(state)).toBe(false); }); it("persists and reloads local trust material", async () => { const filePath = await createTempStatePath(); const state: YonexusClientState = { ...createInitialClientState("client-a"), publicKey: "pubkey", privateKey: "privkey", secret: "secret-value", pairedAt: 1_710_000_000, authenticatedAt: 1_710_000_100, updatedAt: 1_710_000_101 }; await saveYonexusClientState(filePath, state); const reloaded = await loadYonexusClientState(filePath, "client-a"); expect(reloaded).toEqual(state); const raw = JSON.parse(await readFile(filePath, "utf8")) as { version: number }; expect(raw.version).toBe(1); }); it("generates and persists an Ed25519 keypair only once", async () => { const filePath = await createTempStatePath(); const store = createYonexusClientStateStore(filePath); const initial = createInitialClientState("client-a"); const first = await ensureClientKeyPair(initial, store); expect(first.generated).toBe(true); expect(hasClientKeyPair(first.state)).toBe(true); const signature = await signMessage(first.state.privateKey!, "hello yonexus"); await expect( verifySignature(first.state.publicKey!, "hello yonexus", signature) ).resolves.toBe(true); const second = await ensureClientKeyPair(first.state, store); expect(second.generated).toBe(false); expect(second.state.privateKey).toBe(first.state.privateKey); expect(second.state.publicKey).toBe(first.state.publicKey); }); it("SR-06: raises a corruption error for malformed client state files", async () => { const filePath = await createTempStatePath(); await saveYonexusClientState(filePath, { ...createInitialClientState("client-a"), updatedAt: 1_710_000_000 }); await writeFile(filePath, '{"version":1,"identifier":"client-a","updatedAt":"bad"}\n', "utf8"); await expect(loadYonexusClientState(filePath, "client-a")).rejects.toBeInstanceOf( YonexusClientStateCorruptionError ); await expect(loadYonexusClientState(filePath, "client-a")).rejects.toThrow( "invalid updatedAt" ); }); }); describe("Yonexus.Client rule registry", () => { it("dispatches exact-match rule messages to the registered processor", () => { const registry = createClientRuleRegistry(); const processor = vi.fn(); registry.registerRule("chat_sync", processor); const handled = registry.dispatch("chat_sync::{\"body\":\"hello\"}"); expect(handled).toBe(true); expect(processor).toHaveBeenCalledWith("chat_sync::{\"body\":\"hello\"}"); expect(registry.getRules()).toEqual(["chat_sync"]); }); it("rejects reserved and duplicate registrations", () => { const registry = createClientRuleRegistry(); registry.registerRule("chat_sync", () => undefined); expect(() => registry.registerRule("builtin", () => undefined)).toThrow(ClientRuleRegistryError); expect(() => registry.registerRule("chat_sync", () => undefined)).toThrow( "Rule 'chat_sync' is already registered" ); }); it("returns false when no processor matches a message", () => { const registry = createClientRuleRegistry(); expect(registry.dispatch("chat_sync::{\"body\":\"hello\"}")).toBe(false); }); });