Add protocol codec tests and vitest setup

This commit is contained in:
nav
2026-04-08 21:04:55 +00:00
parent 4d8c787dbc
commit fbe1457ab6
4 changed files with 284 additions and 0 deletions

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "yonexus-protocol",
"version": "0.1.0",
"description": "Yonexus shared communication protocol",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^1.0.0"
},
"files": [
"dist/",
"src/",
"PROTOCOL.md"
]
}

237
tests/codec.test.ts Normal file
View File

@@ -0,0 +1,237 @@
:
import { describe, it, expect, beforeEach } from "vitest";
import {
encodeBuiltin,
decodeBuiltin,
parseRuleMessage,
encodeRuleMessage,
parseRewrittenRuleMessage,
encodeRewrittenRuleMessage,
isBuiltinMessage,
CodecError
} from "../src/codec.js";
import type { BuiltinEnvelope, HelloPayload, HelloAckPayload } from "../src/types.js";
describe("Protocol Codec", () => {
describe("encodeBuiltin / decodeBuiltin", () => {
it("should encode and decode a hello message", () => {
const envelope: BuiltinEnvelope<"hello", HelloPayload> = {
type: "hello",
requestId: "req_001",
timestamp: 1711886400,
payload: {
identifier: "client-a",
hasSecret: true,
hasKeyPair: true,
publicKey: "pk_test",
protocolVersion: "1"
}
};
const encoded = encodeBuiltin(envelope);
expect(encoded).toBe(
'builtin::{"type":"hello","requestId":"req_001","timestamp":1711886400,"payload":{"identifier":"client-a","hasSecret":true,"hasKeyPair":true,"publicKey":"pk_test","protocolVersion":"1"}}'
);
const decoded = decodeBuiltin(encoded);
expect(decoded).toEqual(envelope);
});
it("should encode and decode a hello_ack message", () => {
const envelope: BuiltinEnvelope<"hello_ack", HelloAckPayload> = {
type: "hello_ack",
requestId: "req_001",
timestamp: 1711886401,
payload: {
identifier: "client-a",
nextAction: "pair_required"
}
};
const encoded = encodeBuiltin(envelope);
const decoded = decodeBuiltin(encoded);
expect(decoded).toEqual(envelope);
});
it("should handle envelope without optional fields", () => {
const envelope: BuiltinEnvelope = {
type: "heartbeat"
};
const encoded = encodeBuiltin(envelope);
expect(encoded).toBe('builtin::{"type":"heartbeat"}');
const decoded = decodeBuiltin(encoded);
expect(decoded).toEqual(envelope);
});
it("should throw CodecError for malformed builtin message", () => {
expect(() => decodeBuiltin("invalid")).toThrow(CodecError);
expect(() => decodeBuiltin("builtin::")).toThrow(CodecError);
expect(() => decodeBuiltin("builtin::invalid json")).toThrow(CodecError);
});
it("should throw CodecError for non-builtin message prefix", () => {
expect(() => decodeBuiltin("hello::{\"type\":\"test\"}")).toThrow(CodecError);
});
});
describe("isBuiltinMessage", () => {
it("should return true for builtin messages", () => {
expect(isBuiltinMessage('builtin::{"type":"hello"}')).toBe(true);
expect(isBuiltinMessage("builtin::{}")).toBe(true);
});
it("should return false for non-builtin messages", () => {
expect(isBuiltinMessage("hello::world")).toBe(false);
expect(isBuiltinMessage("rule::content")).toBe(false);
expect(isBuiltinMessage("")).toBe(false);
});
});
describe("parseRuleMessage", () => {
it("should parse simple rule message", () => {
const result = parseRuleMessage("chat_sync::{\"body\":\"hello\"}");
expect(result).toEqual({
rule: "chat_sync",
content: '{"body":"hello"}'
});
});
it("should parse rule message with content containing ::", () => {
const result = parseRuleMessage("chat_sync::sender::extra::{\"body\":\"hello::world\"}");
expect(result).toEqual({
rule: "chat_sync",
content: 'sender::extra::{"body":"hello::world"}'
});
});
it("should throw CodecError for malformed rule message", () => {
expect(() => parseRuleMessage("invalid")).toThrow(CodecError);
expect(() => parseRuleMessage("")).toThrow(CodecError);
});
it("should reject builtin as rule identifier", () => {
expect(() => parseRuleMessage('builtin::{"type":"hello"}')).toThrow(CodecError);
});
});
describe("encodeRuleMessage", () => {
it("should encode rule message", () => {
const encoded = encodeRuleMessage("chat_sync", '{"body":"hello"}');
expect(encoded).toBe('chat_sync::{"body":"hello"}');
});
it("should reject builtin as rule", () => {
expect(() => encodeRuleMessage("builtin", "content")).toThrow(CodecError);
});
});
describe("parseRewrittenRuleMessage", () => {
it("should parse rewritten server-side message", () => {
const result = parseRewrittenRuleMessage("chat_sync::client-a::{\"body\":\"hello\"}");
expect(result).toEqual({
rule: "chat_sync",
sender: "client-a",
content: '{"body":"hello"}'
});
});
it("should handle content with :: delimiters", () => {
const result = parseRewrittenRuleMessage("chat_sync::client-a::extra::{\"body\":\"hello::world\"}");
expect(result).toEqual({
rule: "chat_sync",
sender: "client-a",
content: 'extra::{"body":"hello::world"}'
});
});
it("should throw CodecError for malformed rewritten message", () => {
expect(() => parseRewrittenRuleMessage("rule::content")).toThrow(CodecError);
expect(() => parseRewrittenRuleMessage("invalid")).toThrow(CodecError);
});
});
describe("encodeRewrittenRuleMessage", () => {
it("should encode rewritten message", () => {
const encoded = encodeRewrittenRuleMessage("chat_sync", "client-a", '{"body":"hello"}');
expect(encoded).toBe('chat_sync::client-a::{"body":"hello"}');
});
it("should reject builtin as rule", () => {
expect(() => encodeRewrittenRuleMessage("builtin", "sender", "content")).toThrow(CodecError);
});
});
});
describe("Protocol Message Examples", () => {
describe("Hello Flow", () => {
it("should demonstrate complete hello flow", () => {
// Client sends hello
const hello: BuiltinEnvelope<"hello", HelloPayload> = {
type: "hello",
requestId: "req_001",
timestamp: Math.floor(Date.now() / 1000),
payload: {
identifier: "client-a",
hasSecret: false,
hasKeyPair: true,
publicKey: "ed25519_pk_test123",
protocolVersion: "1"
}
};
const helloWire = encodeBuiltin(hello);
expect(isBuiltinMessage(helloWire)).toBe(true);
// Server parses hello
const parsedHello = decodeBuiltin(helloWire);
expect(parsedHello.type).toBe("hello");
expect((parsedHello.payload as HelloPayload).identifier).toBe("client-a");
// Server responds with hello_ack
const helloAck: BuiltinEnvelope<"hello_ack", HelloAckPayload> = {
type: "hello_ack",
requestId: hello.requestId,
timestamp: Math.floor(Date.now() / 1000),
payload: {
identifier: "client-a",
nextAction: "pair_required"
}
};
const ackWire = encodeBuiltin(helloAck);
const parsedAck = decodeBuiltin(ackWire);
expect(parsedAck.type).toBe("hello_ack");
expect((parsedAck.payload as HelloAckPayload).nextAction).toBe("pair_required");
});
});
describe("Rule Message Flow", () => {
it("should demonstrate client to server message", () => {
const message = encodeRuleMessage("chat_sync", '{"conversationId":"abc","body":"hello"}');
expect(message).toBe('chat_sync::{"conversationId":"abc","body":"hello"}');
// Server parses
const parsed = parseRuleMessage(message);
expect(parsed.rule).toBe("chat_sync");
expect(parsed.content).toBe('{"conversationId":"abc","body":"hello"}');
});
it("should demonstrate server-side rewriting", () => {
// Server rewrites incoming message
const rewritten = encodeRewrittenRuleMessage(
"chat_sync",
"client-a",
'{"conversationId":"abc","body":"hello"}'
);
expect(rewritten).toBe('chat_sync::client-a::{"conversationId":"abc","body":"hello"}');
// Server-side handler parses
const parsed = parseRewrittenRuleMessage(rewritten);
expect(parsed.rule).toBe("chat_sync");
expect(parsed.sender).toBe("client-a");
expect(parsed.content).toBe('{"conversationId":"abc","body":"hello"}');
});
});
});

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}

8
vitest.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node"
}
});