test: add client unit test coverage
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
1267
package-lock.json
generated
1267
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,16 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"check": "tsc -p tsconfig.json --noEmit"
|
"check": "tsc -p tsconfig.json --noEmit",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.3"
|
"@types/node": "^25.5.2",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vitest": "^4.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
tests/state-and-rules.test.ts
Normal file
114
tests/state-and-rules.test.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { mkdtemp, readFile, rm } 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,
|
||||||
|
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<string> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
8
vitest.config.ts
Normal file
8
vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "node"
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user