CLose story 1.2
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createPendingOp,
|
||||
isValidPendingOp,
|
||||
} from "../../../src/contracts/pending-op.js";
|
||||
import { PENDING_OP_FIXTURES } from "../../fixtures/pending-op.js";
|
||||
|
||||
describe("pending-op contract", () => {
|
||||
describe("createPendingOp()", () => {
|
||||
it("creates a pending op with required fields", () => {
|
||||
const op = createPendingOp("op-1", "user-1", "hidden", "active");
|
||||
expect(op.opId).toBe("op-1");
|
||||
expect(op.userId).toBe("user-1");
|
||||
expect(op.targetState).toBe("hidden");
|
||||
expect(op.previousState).toBe("active");
|
||||
expect(typeof op.issuedAt).toBe("number");
|
||||
expect(Number.isFinite(op.issuedAt)).toBe(true);
|
||||
expect(op.issuedAt).toBeGreaterThan(0);
|
||||
expect(op.timeoutId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidPendingOp()", () => {
|
||||
it("accepts valid fixture", () => {
|
||||
expect(() => isValidPendingOp(PENDING_OP_FIXTURES.valid)).not.toThrow();
|
||||
});
|
||||
|
||||
it("accepts timeoutId: null", () => {
|
||||
expect(() => isValidPendingOp(PENDING_OP_FIXTURES.timeoutNull)).not.toThrow();
|
||||
});
|
||||
|
||||
it("accepts expired issuedAt (age check is caller's job)", () => {
|
||||
expect(() => isValidPendingOp(PENDING_OP_FIXTURES.expiredIssuedAt)).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws on empty opId", () => {
|
||||
expect(() => isValidPendingOp(PENDING_OP_FIXTURES.emptyOpId)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on non-finite issuedAt", () => {
|
||||
const bad = { ...PENDING_OP_FIXTURES.valid, issuedAt: NaN };
|
||||
expect(() => isValidPendingOp(bad)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on negative issuedAt", () => {
|
||||
const bad = { ...PENDING_OP_FIXTURES.valid, issuedAt: -1 };
|
||||
expect(() => isValidPendingOp(bad)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws if not an object", () => {
|
||||
expect(() => isValidPendingOp(null)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on unknown keys", () => {
|
||||
const bad = { ...PENDING_OP_FIXTURES.valid, extra: true };
|
||||
expect(() => isValidPendingOp(bad)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on string timeoutId", () => {
|
||||
const bad = { ...PENDING_OP_FIXTURES.valid, timeoutId: "not-a-number" };
|
||||
expect(() => isValidPendingOp(bad)).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createScenePreset,
|
||||
isValidScenePreset,
|
||||
} from "../../../src/contracts/scene-preset.js";
|
||||
import { SCENE_PRESET_FIXTURES } from "../../fixtures/scene-preset.js";
|
||||
|
||||
describe("scene-preset contract", () => {
|
||||
describe("createScenePreset()", () => {
|
||||
it("creates a preset with required fields", () => {
|
||||
const p = createScenePreset("My Preset", { "user-1": "active" });
|
||||
expect(p.name).toBe("My Preset");
|
||||
expect(p.matrix).toEqual({ "user-1": "active" });
|
||||
expect(typeof p.createdAt).toBe("number");
|
||||
expect(typeof p.updatedAt).toBe("number");
|
||||
expect(p.createdAt).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("creates a preset with empty matrix", () => {
|
||||
const p = createScenePreset("Empty", {});
|
||||
expect(p.matrix).toEqual({});
|
||||
});
|
||||
|
||||
it("returns a shallow copy of the matrix", () => {
|
||||
const input = { "user-1": "active" };
|
||||
const p = createScenePreset("Test", input);
|
||||
input["user-1"] = "hidden";
|
||||
expect(p.matrix["user-1"]).toBe("active"); // not mutated
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidScenePreset()", () => {
|
||||
it("accepts valid fixture", () => {
|
||||
expect(() => isValidScenePreset(SCENE_PRESET_FIXTURES.valid)).not.toThrow();
|
||||
});
|
||||
|
||||
it("accepts empty matrix edge case", () => {
|
||||
expect(() => isValidScenePreset(SCENE_PRESET_FIXTURES.emptyMatrix)).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws on empty name", () => {
|
||||
expect(() => isValidScenePreset(SCENE_PRESET_FIXTURES.missingName)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on wrong _version", () => {
|
||||
expect(() => isValidScenePreset(SCENE_PRESET_FIXTURES.wrongVersion)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws if not an object", () => {
|
||||
expect(() => isValidScenePreset(null)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on unknown keys", () => {
|
||||
const bad = { ...SCENE_PRESET_FIXTURES.valid, extra: true };
|
||||
expect(() => isValidScenePreset(bad)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on non-finite createdAt", () => {
|
||||
const bad = { ...SCENE_PRESET_FIXTURES.valid, createdAt: NaN };
|
||||
expect(() => isValidScenePreset(bad)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on negative createdAt", () => {
|
||||
const bad = { ...SCENE_PRESET_FIXTURES.valid, createdAt: -1 };
|
||||
expect(() => isValidScenePreset(bad)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on NaN updatedAt", () => {
|
||||
const bad = { ...SCENE_PRESET_FIXTURES.valid, updatedAt: NaN };
|
||||
expect(() => isValidScenePreset(bad)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on Infinity updatedAt", () => {
|
||||
const bad = { ...SCENE_PRESET_FIXTURES.valid, updatedAt: Infinity };
|
||||
expect(() => isValidScenePreset(bad)).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createSocketIntentMessage,
|
||||
createSocketEchoMessage,
|
||||
isValidSocketMessage,
|
||||
SOCKET_EVENTS,
|
||||
MAX_PAYLOAD_BYTES,
|
||||
} from "../../../src/contracts/socket-message.js";
|
||||
import { SOCKET_PAYLOADS } from "../../fixtures/socket-payloads.js";
|
||||
|
||||
describe("socket-message contract", () => {
|
||||
describe("createSocketIntentMessage()", () => {
|
||||
it("creates a valid intent message", () => {
|
||||
const msg = createSocketIntentMessage("op-1", "user-1", "hidden");
|
||||
expect(msg.event).toBe(SOCKET_EVENTS.VISIBILITY_SET);
|
||||
const p = /** @type {any} */ (msg.payload);
|
||||
expect(p.opId).toBe("op-1");
|
||||
expect(p.userId).toBe("user-1");
|
||||
expect(p.targetState).toBe("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSocketEchoMessage()", () => {
|
||||
it("creates a valid echo message", () => {
|
||||
const msg = createSocketEchoMessage("op-1", "user-1", "hidden", 1);
|
||||
expect(msg.event).toBe(SOCKET_EVENTS.VISIBILITY_UPDATED);
|
||||
const p = /** @type {any} */ (msg.payload);
|
||||
expect(p.opId).toBe("op-1");
|
||||
expect(p.state).toBe("hidden");
|
||||
expect(p.revision).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidSocketMessage()", () => {
|
||||
it("accepts a valid intent from fixture", () => {
|
||||
expect(() => isValidSocketMessage(SOCKET_PAYLOADS.validIntent)).not.toThrow();
|
||||
});
|
||||
|
||||
it("accepts a valid echo from fixture", () => {
|
||||
expect(() => isValidSocketMessage(SOCKET_PAYLOADS.validEcho)).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws on missing opId", () => {
|
||||
expect(() => isValidSocketMessage(SOCKET_PAYLOADS.missingOpId)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on unknown event", () => {
|
||||
expect(() => isValidSocketMessage(SOCKET_PAYLOADS.unknownEvent)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on extra payload keys (intent)", () => {
|
||||
expect(() => isValidSocketMessage(SOCKET_PAYLOADS.extraKeys)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws if not an object", () => {
|
||||
expect(() => isValidSocketMessage(null)).toThrow(TypeError);
|
||||
expect(() => isValidSocketMessage("string")).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on unknown top-level keys", () => {
|
||||
expect(() =>
|
||||
isValidSocketMessage({ event: SOCKET_EVENTS.VISIBILITY_SET, payload: { opId: "x", userId: "y", targetState: "active" }, extra: true })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on empty event string", () => {
|
||||
expect(() =>
|
||||
isValidSocketMessage({ event: "", payload: {} })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on non-finite revision in echo", () => {
|
||||
expect(() =>
|
||||
isValidSocketMessage({
|
||||
event: SOCKET_EVENTS.VISIBILITY_UPDATED,
|
||||
payload: { opId: "op-1", userId: "u-1", state: "active", revision: NaN },
|
||||
})
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on negative revision in echo", () => {
|
||||
expect(() =>
|
||||
isValidSocketMessage({
|
||||
event: SOCKET_EVENTS.VISIBILITY_UPDATED,
|
||||
payload: { opId: "op-1", userId: "u-1", state: "active", revision: -1 },
|
||||
})
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on missing revision in echo", () => {
|
||||
expect(() =>
|
||||
isValidSocketMessage(SOCKET_PAYLOADS.missingRevision)
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SOCKET_EVENTS", () => {
|
||||
it("is frozen", () => {
|
||||
expect(Object.isFrozen(SOCKET_EVENTS)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses scrying-pool. prefix for all events", () => {
|
||||
for (const event of Object.values(SOCKET_EVENTS)) {
|
||||
expect(event.startsWith("scrying-pool.")).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("MAX_PAYLOAD_BYTES", () => {
|
||||
it("is 4096", () => {
|
||||
expect(MAX_PAYLOAD_BYTES).toBe(4096);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createVisibilityMatrix,
|
||||
isValidVisibilityMatrix,
|
||||
VISIBILITY_STATES,
|
||||
VISIBILITY_MATRIX_VERSION,
|
||||
} from "../../../src/contracts/visibility-matrix.js";
|
||||
|
||||
describe("visibility-matrix contract", () => {
|
||||
describe("createVisibilityMatrix()", () => {
|
||||
it("creates a matrix with default empty state", () => {
|
||||
const m = createVisibilityMatrix();
|
||||
expect(m._version).toBe(VISIBILITY_MATRIX_VERSION);
|
||||
expect(m.matrix).toEqual({});
|
||||
});
|
||||
|
||||
it("creates a matrix with provided entries", () => {
|
||||
const m = createVisibilityMatrix({ "user-1": "active", "user-2": "hidden" });
|
||||
expect(m.matrix["user-1"]).toBe("active");
|
||||
expect(m.matrix["user-2"]).toBe("hidden");
|
||||
});
|
||||
|
||||
it("returns a shallow copy of the provided matrix", () => {
|
||||
const input = /** @type {{ "user-1": import("../../../src/contracts/visibility-matrix.js").VisibilityState }} */ ({ "user-1": "active" });
|
||||
const m = createVisibilityMatrix(input);
|
||||
input["user-1"] = "hidden";
|
||||
expect(m.matrix["user-1"]).toBe("active"); // not mutated
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidVisibilityMatrix()", () => {
|
||||
it("accepts a valid matrix with all known states", () => {
|
||||
for (const state of VISIBILITY_STATES) {
|
||||
const data = { _version: 1, matrix: { "user-1": state } };
|
||||
expect(() => isValidVisibilityMatrix(data)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts an empty matrix", () => {
|
||||
expect(() => isValidVisibilityMatrix({ _version: 1, matrix: {} })).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws if not an object", () => {
|
||||
expect(() => isValidVisibilityMatrix(null)).toThrow(TypeError);
|
||||
expect(() => isValidVisibilityMatrix("string")).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on unknown top-level keys", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: {}, extra: true })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on wrong _version", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 2, matrix: {} })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws if matrix is not a plain object", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: null })
|
||||
).toThrow(TypeError);
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: ["active"] })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on empty userId key", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: { "": "active" } })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on invalid state value", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: { "user-1": "invisible" } })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on null state value", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: { "user-1": null } })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on non-string state value (Symbol)", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: { "user-1": Symbol("test") } })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("throws on non-string state value (object)", () => {
|
||||
expect(() =>
|
||||
isValidVisibilityMatrix({ _version: 1, matrix: { "user-1": { value: "active" } } })
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VISIBILITY_STATES", () => {
|
||||
it("contains exactly 8 states", () => {
|
||||
expect(VISIBILITY_STATES).toHaveLength(8);
|
||||
});
|
||||
|
||||
it("is frozen", () => {
|
||||
expect(Object.isFrozen(VISIBILITY_STATES)).toBe(true);
|
||||
});
|
||||
|
||||
it("contains the 8 expected state values", () => {
|
||||
const expected = [
|
||||
"active", "hidden", "self-muted", "offline",
|
||||
"cam-lost", "reconnecting", "never-connected", "ghost",
|
||||
];
|
||||
expect([...VISIBILITY_STATES].sort()).toEqual(expected.sort());
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user