feat: add private profile media and realtime contracts
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
authorizeMediaSurface,
|
||||
createMediaSurfaceLifecycle,
|
||||
isMediaSurface,
|
||||
parseMediaSurface,
|
||||
parseMediaViewerContext,
|
||||
transitionMediaSurface,
|
||||
type MediaSurface,
|
||||
type MediaSurfaceSource,
|
||||
} from "../media/index.ts";
|
||||
import type { VideoTexture } from "three";
|
||||
|
||||
const PRIVATE_SOURCE: MediaSurfaceSource = {
|
||||
kind: "live-stream",
|
||||
locator: "opaque:private-stream-token",
|
||||
privacy: "private",
|
||||
hasAudio: true,
|
||||
autoplay: false,
|
||||
muted: true,
|
||||
};
|
||||
|
||||
function surface(over: Partial<MediaSurface> = {}): MediaSurface {
|
||||
return {
|
||||
screenId: "screen-1",
|
||||
officeId: "office-1",
|
||||
levelId: "level-1",
|
||||
roomId: "room-1",
|
||||
acl: {
|
||||
audience: "office-members",
|
||||
viewerIds: [],
|
||||
presenterIds: ["presenter-1"],
|
||||
moderatorIds: ["moderator-1"],
|
||||
},
|
||||
source: PRIVATE_SOURCE,
|
||||
state: { status: "presenting", presenterId: "presenter-1", revision: 1 },
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe("media surface validation", () => {
|
||||
it("accepts a strict JSON round trip and rejects extra/non-JSON fields", () => {
|
||||
const parsed = parseMediaSurface(JSON.parse(JSON.stringify(surface())));
|
||||
assert.deepEqual(parsed, surface());
|
||||
assert.equal(isMediaSurface({ ...surface(), surprise: true }), false);
|
||||
assert.equal(isMediaSurface({ ...surface(), screenId: "bad id" }), false);
|
||||
assert.equal(isMediaSurface(Object.assign(Object.create(null), surface())), false);
|
||||
});
|
||||
|
||||
it("enforces state/source consistency and safe playback literals", () => {
|
||||
assert.throws(() => parseMediaSurface({ ...surface(), source: null }), /require a source/);
|
||||
assert.throws(() => parseMediaSurface({
|
||||
...surface(),
|
||||
source: { ...PRIVATE_SOURCE, autoplay: true },
|
||||
}), /autoplay/);
|
||||
assert.throws(() => parseMediaSurface({
|
||||
...surface(),
|
||||
source: { ...PRIVATE_SOURCE, muted: false },
|
||||
}), /muted/);
|
||||
assert.throws(() => parseMediaSurface({
|
||||
...surface(),
|
||||
acl: { ...surface().acl, audience: "public" },
|
||||
}), /private source/);
|
||||
});
|
||||
|
||||
it("strictly validates the server-created viewer context", () => {
|
||||
assert.deepEqual(parseMediaViewerContext({
|
||||
viewerId: "viewer-1", officeIds: ["office-1"], optedIn: true,
|
||||
}), { viewerId: "viewer-1", officeIds: ["office-1"], optedIn: true });
|
||||
assert.throws(() => parseMediaViewerContext({
|
||||
viewerId: null, officeIds: [], optedIn: true, clientClaimsAdmin: true,
|
||||
}), /exactly/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("server-enforceable authorization", () => {
|
||||
it("never returns a private source to an anonymous public viewer", () => {
|
||||
const decision = authorizeMediaSurface(surface(), { viewerId: null, officeIds: [], optedIn: true });
|
||||
assert.equal(decision.authorized, false);
|
||||
assert.equal(decision.canView, false);
|
||||
assert.equal(decision.reason, "authentication_required");
|
||||
assert.equal(decision.surface.source, null);
|
||||
assert.ok(!JSON.stringify(decision).includes(PRIVATE_SOURCE.locator));
|
||||
});
|
||||
|
||||
it("requires both trusted membership and explicit opt-in before releasing source", () => {
|
||||
const context = { viewerId: "viewer-1", officeIds: ["office-1"], optedIn: false };
|
||||
const waiting = authorizeMediaSurface(surface(), context);
|
||||
assert.equal(waiting.authorized, true);
|
||||
assert.equal(waiting.reason, "opt_in_required");
|
||||
assert.equal(waiting.surface.source, null);
|
||||
const ready = authorizeMediaSurface(surface(), { ...context, optedIn: true });
|
||||
assert.equal(ready.canView, true);
|
||||
assert.equal(ready.surface.source?.locator, PRIVATE_SOURCE.locator);
|
||||
assert.deepEqual(ready.surface.playback, { autoplay: false, muted: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("presenter lifecycle", () => {
|
||||
it("stops and revokes by clearing the sensitive source", () => {
|
||||
const stopped = transitionMediaSurface(surface(), { type: "stop", presenterId: "presenter-1" });
|
||||
assert.equal(stopped.state.status, "stopped");
|
||||
assert.equal(stopped.source, null);
|
||||
assert.equal(stopped.state.revision, 2);
|
||||
const revoked = transitionMediaSurface(surface(), { type: "revoke", actorId: "moderator-1" });
|
||||
assert.equal(revoked.state.status, "revoked");
|
||||
assert.equal(revoked.source, null);
|
||||
assert.throws(() => transitionMediaSurface(surface(), { type: "revoke", actorId: "stranger" }));
|
||||
});
|
||||
|
||||
it("retains authorization through a deterministic disconnect/reconnect", () => {
|
||||
const reconnecting = transitionMediaSurface(surface(), { type: "disconnect", presenterId: "presenter-1" });
|
||||
assert.equal(reconnecting.state.status, "reconnecting");
|
||||
const resumed = transitionMediaSurface(reconnecting, { type: "reconnect", presenterId: "presenter-1" });
|
||||
assert.equal(resumed.state.status, "presenting");
|
||||
assert.equal(resumed.state.revision, 3);
|
||||
assert.throws(() => transitionMediaSurface(reconnecting, { type: "reconnect", presenterId: "other" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("caller-owned video/texture lifecycle", () => {
|
||||
it("never plays, stops a track, or disposes a texture and releases references", () => {
|
||||
let pauses = 0;
|
||||
let stops = 0;
|
||||
let disposals = 0;
|
||||
const video = {
|
||||
autoplay: true,
|
||||
muted: false,
|
||||
pause: () => { pauses += 1; },
|
||||
} as unknown as HTMLVideoElement;
|
||||
const texture = { dispose: () => { disposals += 1; } } as unknown as VideoTexture;
|
||||
const track = { stop: () => { stops += 1; } } as unknown as MediaStreamTrack;
|
||||
const lifecycle = createMediaSurfaceLifecycle();
|
||||
lifecycle.bind({ video, texture, track });
|
||||
assert.equal(video.autoplay, false);
|
||||
assert.equal(video.muted, true);
|
||||
assert.equal(lifecycle.texture(), null);
|
||||
lifecycle.apply(authorizeMediaSurface(surface(), {
|
||||
viewerId: "viewer-1", officeIds: ["office-1"], optedIn: true,
|
||||
}));
|
||||
assert.equal(lifecycle.texture(), texture);
|
||||
lifecycle.dispose();
|
||||
lifecycle.dispose();
|
||||
assert.equal(lifecycle.texture(), null);
|
||||
assert.ok(pauses >= 2);
|
||||
assert.equal(stops, 0);
|
||||
assert.equal(disposals, 0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
actorKindForPresence,
|
||||
clearLocalProfile,
|
||||
createDefaultLocalProfile,
|
||||
createWebcamFaceConsent,
|
||||
decodeLocalProfile,
|
||||
encodeLocalProfile,
|
||||
initialWebcamFaceConsent,
|
||||
loadLocalProfile,
|
||||
resolveHumanoidAppearance,
|
||||
saveLocalProfile,
|
||||
transitionWebcamFaceConsent,
|
||||
type LocalProfileV1,
|
||||
type ProfileStorageAdapter,
|
||||
} from "../profile/index.ts";
|
||||
|
||||
class MemoryStorage implements ProfileStorageAdapter {
|
||||
readonly values = new Map<string, string>();
|
||||
getItem(key: string): string | null { return this.values.get(key) ?? null; }
|
||||
setItem(key: string, value: string): void { this.values.set(key, value); }
|
||||
removeItem(key: string): void { this.values.delete(key); }
|
||||
}
|
||||
|
||||
describe("local member profile", () => {
|
||||
it("makes stable, useful defaults from identity without retaining or leaking the id", () => {
|
||||
const identityId = "auth0|private-user-82914";
|
||||
const first = createDefaultLocalProfile(identityId, " Avery ");
|
||||
const second = createDefaultLocalProfile(identityId, "Avery");
|
||||
assert.deepEqual(first, second);
|
||||
assert.equal(first.displayName, "Avery");
|
||||
const encoded = encodeLocalProfile(first);
|
||||
assert.equal(encoded.includes(identityId), false);
|
||||
assert.deepEqual(Object.keys(first).sort(), ["appearance", "displayName", "version"]);
|
||||
assert.doesNotThrow(() => JSON.stringify(first));
|
||||
const resolved = resolveHumanoidAppearance(first.appearance);
|
||||
assert.match(resolved.skinTone, /^#[0-9a-f]{6}$/i);
|
||||
assert.match(resolved.outfitColor, /^#[0-9a-f]{6}$/i);
|
||||
assert.equal(resolved.bodyShape, first.appearance.bodyShape);
|
||||
});
|
||||
|
||||
it("strictly rejects unknown, malformed and future profile data", () => {
|
||||
const profile = createDefaultLocalProfile("member", "Member");
|
||||
assert.equal(decodeLocalProfile({ ...profile, identityId: "leak" }).ok, false);
|
||||
assert.equal(decodeLocalProfile({ ...profile, faceImageUrl: "camera://stream" }).ok, false);
|
||||
assert.equal(decodeLocalProfile({ ...profile, appearance: { ...profile.appearance, outfit: "rainbow" } }).ok, false);
|
||||
assert.deepEqual(decodeLocalProfile({ version: 99 }), { ok: false, error: "profile: unsupported version" });
|
||||
assert.deepEqual(decodeLocalProfile("{"), { ok: false, error: "profile: data is not valid JSON" });
|
||||
assert.throws(() => createDefaultLocalProfile("", "Member"), RangeError);
|
||||
});
|
||||
|
||||
it("migrates the one exact V1 shape into an isolated V2 appearance", () => {
|
||||
const legacy: LocalProfileV1 = {
|
||||
version: 1,
|
||||
displayName: "River",
|
||||
skinTone: "umber",
|
||||
outfit: "navy",
|
||||
accent: "amber",
|
||||
hair: "auburn",
|
||||
bodyShape: "slim",
|
||||
};
|
||||
const decoded = decodeLocalProfile(JSON.stringify(legacy));
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.migrated, true);
|
||||
assert.deepEqual(decoded.profile, {
|
||||
version: 2,
|
||||
displayName: "River",
|
||||
appearance: {
|
||||
skinTone: "umber",
|
||||
outfit: "navy",
|
||||
accent: "amber",
|
||||
hair: "auburn",
|
||||
bodyShape: "slim",
|
||||
},
|
||||
});
|
||||
assert.equal(decodeLocalProfile({ ...legacy, extra: true }).ok, false);
|
||||
});
|
||||
|
||||
it("maps signed-in and anonymous roles by environment", () => {
|
||||
assert.equal(actorKindForPresence(true, "outdoors"), "humanoid");
|
||||
assert.equal(actorKindForPresence(true, "office"), "humanoid");
|
||||
assert.equal(actorKindForPresence(false, "outdoors"), "crow");
|
||||
assert.equal(actorKindForPresence(false, "office"), "dog");
|
||||
});
|
||||
});
|
||||
|
||||
describe("profile persistence boundary", () => {
|
||||
it("round-trips, reports migration without an implicit rewrite, and clears", () => {
|
||||
const storage = new MemoryStorage();
|
||||
const profile = createDefaultLocalProfile("signed-in-1", "Morgan");
|
||||
assert.deepEqual(saveLocalProfile(storage, "tera.profile", profile), { ok: true });
|
||||
assert.deepEqual(loadLocalProfile(storage, "tera.profile"), {
|
||||
status: "loaded",
|
||||
profile,
|
||||
migrated: false,
|
||||
});
|
||||
|
||||
const legacy: LocalProfileV1 = {
|
||||
version: 1,
|
||||
displayName: "Morgan",
|
||||
skinTone: "sand",
|
||||
outfit: "sage",
|
||||
accent: "aqua",
|
||||
hair: "black",
|
||||
bodyShape: "average",
|
||||
};
|
||||
storage.values.set("legacy", JSON.stringify(legacy));
|
||||
const before = storage.values.get("legacy");
|
||||
const loaded = loadLocalProfile(storage, "legacy");
|
||||
assert.equal(loaded.status, "loaded");
|
||||
if (loaded.status === "loaded") assert.equal(loaded.migrated, true);
|
||||
assert.equal(storage.values.get("legacy"), before, "load never silently rewrites data");
|
||||
assert.deepEqual(clearLocalProfile(storage, "tera.profile"), { ok: true });
|
||||
assert.deepEqual(loadLocalProfile(storage, "tera.profile"), { status: "missing" });
|
||||
});
|
||||
|
||||
it("contains adapter and encoder exceptions", () => {
|
||||
const unavailable: ProfileStorageAdapter = {
|
||||
getItem: () => { throw new Error("blocked"); },
|
||||
setItem: () => { throw new Error("quota"); },
|
||||
removeItem: () => { throw new Error("blocked"); },
|
||||
};
|
||||
const profile = createDefaultLocalProfile("signed-in-2", "Taylor");
|
||||
assert.equal(loadLocalProfile(unavailable, "tera.profile").status, "unavailable");
|
||||
assert.equal(saveLocalProfile(unavailable, "tera.profile", profile).ok, false);
|
||||
assert.equal(clearLocalProfile(unavailable, "tera.profile").ok, false);
|
||||
assert.equal(loadLocalProfile(unavailable, " ").status, "invalid");
|
||||
const invalid = { ...profile, displayName: "" };
|
||||
assert.equal(saveLocalProfile(new MemoryStorage(), "x", invalid).ok, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ephemeral webcam face consent", () => {
|
||||
it("starts off with explicit privacy invariants and cannot auto-start", () => {
|
||||
const state = initialWebcamFaceConsent();
|
||||
assert.deepEqual(state, {
|
||||
status: "off",
|
||||
consentGranted: false,
|
||||
ephemeral: true,
|
||||
persistence: "none",
|
||||
autoFetch: false,
|
||||
revision: 0,
|
||||
});
|
||||
const rejected = transitionWebcamFaceConsent(state, { type: "start", explicitConsent: true });
|
||||
assert.equal(rejected.accepted, false);
|
||||
if (!rejected.accepted) assert.equal(rejected.reason, "consent-required");
|
||||
});
|
||||
|
||||
it("requires request then explicit start, and clears consent on stop", () => {
|
||||
const consent = createWebcamFaceConsent();
|
||||
assert.equal(consent.requestStart().accepted, true);
|
||||
assert.equal(consent.state().status, "awaiting-consent");
|
||||
assert.equal(consent.start(true).accepted, true);
|
||||
assert.deepEqual(consent.state(), {
|
||||
status: "active",
|
||||
consentGranted: true,
|
||||
ephemeral: true,
|
||||
persistence: "none",
|
||||
autoFetch: false,
|
||||
revision: 2,
|
||||
});
|
||||
assert.equal(consent.stop().accepted, true);
|
||||
assert.equal(consent.state().status, "stopped");
|
||||
assert.equal(consent.state().consentGranted, false);
|
||||
assert.equal(consent.start(true).accepted, false, "a stopped session needs a new request");
|
||||
});
|
||||
|
||||
it("revokes from any live state and requires a fresh user request before reuse", () => {
|
||||
const consent = createWebcamFaceConsent();
|
||||
consent.requestStart();
|
||||
consent.start(true);
|
||||
assert.equal(consent.revoke().accepted, true);
|
||||
assert.equal(consent.state().status, "revoked");
|
||||
assert.equal(consent.state().consentGranted, false);
|
||||
assert.equal(consent.start(true).accepted, false);
|
||||
assert.equal(consent.requestStart().accepted, true);
|
||||
assert.equal(consent.state().status, "awaiting-consent");
|
||||
const leaked = consent.state();
|
||||
leaked.status = "active";
|
||||
assert.equal(consent.state().status, "awaiting-consent");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
PoseInterpolationBuffer,
|
||||
isEntityPoseSnapshot,
|
||||
isInterestCell,
|
||||
isRealtimeSequence,
|
||||
isRealtimeTimestamp,
|
||||
validateClientRealtimeMessage,
|
||||
validateMotion,
|
||||
validateServerRealtimeMessage,
|
||||
type ActorPoseSnapshot,
|
||||
type InterestCell,
|
||||
type VehiclePoseSnapshot,
|
||||
} from "../realtime/index.ts";
|
||||
|
||||
const localActor = (
|
||||
sequence: number,
|
||||
timestampMs: number,
|
||||
xM: number,
|
||||
headingDeg = 0,
|
||||
): ActorPoseSnapshot => ({
|
||||
entity: "actor",
|
||||
actorId: "actor-karti",
|
||||
kind: "humanoid",
|
||||
sequence,
|
||||
timestampMs,
|
||||
pose: {
|
||||
space: "local",
|
||||
cell: { kind: "floor", officeId: "lumbridge-hq", floorId: "level-1" },
|
||||
xM,
|
||||
yM: 0,
|
||||
zM: 2,
|
||||
headingDeg,
|
||||
pitchDeg: 0,
|
||||
},
|
||||
velocity: { xMps: 10, yMps: 0, zMps: 0, yawDegPerSec: 10 },
|
||||
});
|
||||
|
||||
const geographicVehicle = (
|
||||
sequence: number,
|
||||
timestampMs: number,
|
||||
lat: number,
|
||||
): VehiclePoseSnapshot => ({
|
||||
entity: "vehicle",
|
||||
vehicleId: "model-x-hero",
|
||||
kind: "model-x",
|
||||
driverActorId: "actor-karti",
|
||||
sequence,
|
||||
timestampMs,
|
||||
pose: {
|
||||
space: "geographic",
|
||||
lat,
|
||||
lng: -120,
|
||||
altitudeM: 100,
|
||||
headingDeg: 0,
|
||||
pitchDeg: 0,
|
||||
},
|
||||
velocity: { xMps: 0, yMps: 0, zMps: 30, yawDegPerSec: 0 },
|
||||
steering: 0,
|
||||
wheelRadians: 2,
|
||||
});
|
||||
|
||||
describe("realtime spatial protocol", () => {
|
||||
it("validates all hierarchical interest cell kinds", () => {
|
||||
const cells: InterestCell[] = [
|
||||
{ kind: "california-tile", x: -4, y: 18, level: 6 },
|
||||
{ kind: "city", cityId: "bay-area" },
|
||||
{ kind: "office", officeId: "lumbridge-hq" },
|
||||
{ kind: "floor", officeId: "lumbridge-hq", floorId: "level-1" },
|
||||
{
|
||||
kind: "room",
|
||||
officeId: "lumbridge-hq",
|
||||
floorId: "level-1",
|
||||
roomId: "commons",
|
||||
},
|
||||
];
|
||||
assert.ok(cells.every(isInterestCell));
|
||||
assert.equal(isInterestCell({ kind: "city", cityId: "new-york" }), false);
|
||||
assert.equal(isInterestCell({ kind: "california-tile", x: 0.5, y: 2, level: 4 }), false);
|
||||
});
|
||||
|
||||
it("bounds sequence and timestamps before accepting an envelope", () => {
|
||||
assert.equal(isRealtimeSequence(0), true);
|
||||
assert.equal(isRealtimeSequence(4_294_967_295), true);
|
||||
assert.equal(isRealtimeSequence(4_294_967_296), false);
|
||||
assert.equal(isRealtimeTimestamp(1_765_000_000_000), true);
|
||||
assert.equal(isRealtimeTimestamp(Number.MAX_SAFE_INTEGER), false);
|
||||
});
|
||||
|
||||
it("accepts strict join/resume grants and membership revocation", () => {
|
||||
const joinRequest = {
|
||||
type: "join-request",
|
||||
protocolVersion: 1,
|
||||
requestId: "request-1",
|
||||
actorId: "actor-karti",
|
||||
resumeToken: null,
|
||||
lastReceivedSequence: null,
|
||||
interests: [{ kind: "city", cityId: "bay-area" }],
|
||||
};
|
||||
assert.equal(validateClientRealtimeMessage(joinRequest).ok, true);
|
||||
|
||||
const grant = {
|
||||
type: "join-grant",
|
||||
protocolVersion: 1,
|
||||
requestId: "request-1",
|
||||
sessionId: "session-1",
|
||||
actorId: "actor-karti",
|
||||
role: "member",
|
||||
serverEpoch: "epoch-a",
|
||||
serverTimeMs: 1_765_000_000_000,
|
||||
nextSequence: 4,
|
||||
resumeToken: "rotating-secret",
|
||||
interests: joinRequest.interests,
|
||||
initial: [localActor(3, 1_765_000_000_000, 0)],
|
||||
};
|
||||
assert.equal(validateServerRealtimeMessage(grant).ok, true);
|
||||
assert.equal(validateClientRealtimeMessage({
|
||||
type: "resume-request",
|
||||
protocolVersion: 1,
|
||||
requestId: "request-2",
|
||||
sessionId: "session-1",
|
||||
resumeToken: "rotating-secret",
|
||||
serverEpoch: "epoch-a",
|
||||
lastReceivedSequence: 3,
|
||||
interests: joinRequest.interests,
|
||||
}).ok, true);
|
||||
assert.equal(validateServerRealtimeMessage({
|
||||
type: "membership-revoked",
|
||||
protocolVersion: 1,
|
||||
serverEpoch: "epoch-a",
|
||||
sequence: 5,
|
||||
timestampMs: 1_765_000_000_010,
|
||||
sessionId: "session-1",
|
||||
actorId: "actor-karti",
|
||||
reason: "membership-revoked",
|
||||
reconnectAllowed: false,
|
||||
}).ok, true);
|
||||
});
|
||||
|
||||
it("rejects malformed, oversized, and non-finite server input", () => {
|
||||
const actor = localActor(1, 1000, 0);
|
||||
assert.equal(isEntityPoseSnapshot(actor), true);
|
||||
assert.equal(isEntityPoseSnapshot({ ...actor, pose: { ...actor.pose, xM: Number.NaN } }), false);
|
||||
assert.equal(validateServerRealtimeMessage({
|
||||
type: "pose-delta",
|
||||
protocolVersion: 1,
|
||||
serverEpoch: "epoch-a",
|
||||
sequence: 2,
|
||||
timestampMs: 1000,
|
||||
updates: [actor],
|
||||
removedEntityIds: Array.from({ length: 2_049 }, (_, index) => `actor-${index}`),
|
||||
}).ok, false);
|
||||
assert.equal(validateServerRealtimeMessage({ type: "pose-delta", protocolVersion: 2 }).ok, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authoritative motion validation", () => {
|
||||
const limits = {
|
||||
maximumHorizontalSpeedMps: 15,
|
||||
maximumVerticalSpeedMps: 8,
|
||||
maximumTurnRateDegPerSec: 180,
|
||||
positionSlackM: 0.25,
|
||||
};
|
||||
|
||||
it("accepts plausible actor movement and reports measured rates", () => {
|
||||
const result = validateMotion(localActor(1, 1_000, 0), localActor(2, 2_000, 10), limits);
|
||||
assert.equal(result.ok, true);
|
||||
if (result.ok) {
|
||||
assert.equal(result.horizontalSpeedMps, 9.75);
|
||||
assert.equal(result.verticalSpeedMps, 0);
|
||||
assert.equal(result.turnRateDegPerSec, 0);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects speed hacks, stale sequences, identity and interest-cell jumps", () => {
|
||||
assert.deepEqual(validateMotion(localActor(1, 1_000, 0), localActor(2, 1_100, 50), limits), {
|
||||
ok: false,
|
||||
reason: "speed-exceeded",
|
||||
});
|
||||
assert.deepEqual(validateMotion(localActor(2, 2_000, 0), localActor(2, 3_000, 1), limits), {
|
||||
ok: false,
|
||||
reason: "time-invalid",
|
||||
});
|
||||
const another = { ...localActor(2, 2_000, 1), actorId: "actor-other" };
|
||||
assert.deepEqual(validateMotion(localActor(1, 1_000, 0), another, limits), {
|
||||
ok: false,
|
||||
reason: "identity-mismatch",
|
||||
});
|
||||
const anotherFloor = localActor(2, 2_000, 1);
|
||||
if (anotherFloor.pose.space === "local" && anotherFloor.pose.cell.kind === "floor") {
|
||||
anotherFloor.pose.cell.floorId = "level-2";
|
||||
}
|
||||
assert.deepEqual(validateMotion(localActor(1, 1_000, 0), anotherFloor, limits), {
|
||||
ok: false,
|
||||
reason: "space-mismatch",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses real geographic distance for statewide vehicle checks", () => {
|
||||
const start = geographicVehicle(10, 10_000, 34);
|
||||
const plausible = geographicVehicle(11, 11_000, 34.0002);
|
||||
const result = validateMotion(start, plausible, {
|
||||
maximumHorizontalSpeedMps: 40,
|
||||
maximumVerticalSpeedMps: 5,
|
||||
maximumTurnRateDegPerSec: 90,
|
||||
positionSlackM: 1,
|
||||
});
|
||||
assert.equal(result.ok, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pose interpolation buffer", () => {
|
||||
it("interpolates deterministically at delayed server time", () => {
|
||||
const a = new PoseInterpolationBuffer({ interpolationDelayMs: 100 });
|
||||
const b = new PoseInterpolationBuffer({ interpolationDelayMs: 100 });
|
||||
for (const buffer of [a, b]) {
|
||||
assert.equal(buffer.push(localActor(1, 1_000, 0, 350)), true);
|
||||
assert.equal(buffer.push(localActor(2, 1_200, 2, 10)), true);
|
||||
}
|
||||
const first = a.sample(1_200);
|
||||
const second = b.sample(1_200);
|
||||
assert.deepEqual(first, second);
|
||||
assert.equal(first?.mode, "interpolated");
|
||||
assert.equal(first?.timestampMs, 1_100);
|
||||
assert.ok(first?.pose.space === "local");
|
||||
if (first?.pose.space === "local") {
|
||||
assert.equal(first.pose.xM, 1);
|
||||
assert.equal(first.pose.headingDeg, 360, "heading follows the short arc through north");
|
||||
}
|
||||
});
|
||||
|
||||
it("caps extrapolation and then holds the capped predicted pose", () => {
|
||||
const buffer = new PoseInterpolationBuffer({
|
||||
interpolationDelayMs: 0,
|
||||
maximumExtrapolationMs: 150,
|
||||
});
|
||||
buffer.push(localActor(1, 1_000, 0));
|
||||
const predicted = buffer.sample(1_100);
|
||||
assert.equal(predicted?.mode, "extrapolated");
|
||||
assert.ok(predicted?.pose.space === "local");
|
||||
if (predicted?.pose.space === "local") assert.equal(predicted.pose.xM, 1);
|
||||
const capped = buffer.sample(10_000);
|
||||
assert.equal(capped?.mode, "held");
|
||||
assert.equal(capped?.timestampMs, 1_150);
|
||||
assert.ok(capped?.pose.space === "local");
|
||||
if (capped?.pose.space === "local") assert.equal(capped.pose.xM, 1.5);
|
||||
});
|
||||
|
||||
it("rejects stale, duplicate, or mixed-entity input and caps capacity", () => {
|
||||
const buffer = new PoseInterpolationBuffer({ capacity: 2 });
|
||||
assert.equal(buffer.push(localActor(1, 1_000, 0)), true);
|
||||
assert.equal(buffer.push(localActor(1, 1_100, 1)), false);
|
||||
assert.equal(buffer.push(localActor(2, 1_200, 2)), true);
|
||||
assert.equal(buffer.push(localActor(3, 1_300, 3)), true);
|
||||
assert.equal(buffer.size(), 2);
|
||||
assert.equal(buffer.push({ ...localActor(4, 1_400, 4), actorId: "other" }), false);
|
||||
buffer.clear();
|
||||
assert.equal(buffer.size(), 0);
|
||||
assert.equal(buffer.sample(1_500), null);
|
||||
});
|
||||
});
|
||||
@@ -74,6 +74,20 @@ describe("playable city scene actor", () => {
|
||||
actor.dispose();
|
||||
});
|
||||
|
||||
it("can exaggerate presentation scale without changing metre-space motion", () => {
|
||||
const actor = createSceneActor(options({
|
||||
sceneUnitsPerMetre: 0.001,
|
||||
visualSceneUnitsPerMetre: 0.025,
|
||||
active: true,
|
||||
}));
|
||||
assert.equal(actor.root.scale.x, 0.025);
|
||||
actor.setActions({ forward: 1 });
|
||||
actor.tick(0.2);
|
||||
assert.ok(actor.state().z < 3);
|
||||
assert.ok(Math.abs(actor.root.position.z - actor.state().z * 0.001) < 1e-12);
|
||||
actor.dispose();
|
||||
});
|
||||
|
||||
it("keeps its stable root and identity while replacing procedural rigs", () => {
|
||||
const actor = createSceneActor(options({ active: true }));
|
||||
const root = actor.root;
|
||||
|
||||
@@ -69,6 +69,15 @@ describe("walker input and clock", () => {
|
||||
leaked.position.x = Number.NaN;
|
||||
assert.deepEqual(controller.state().position, { x: 2, z: 2 });
|
||||
});
|
||||
|
||||
it("uses and restores a normalized authored arrival facing", () => {
|
||||
const controller = walker(planWith(), { facing: { x: 3, z: 4 } });
|
||||
assert.deepEqual(controller.state().facing, { x: 0.6, z: 0.8 });
|
||||
controller.tick(0.1, { x: -1, z: 0 });
|
||||
assert.deepEqual(controller.state().facing, { x: -1, z: 0 });
|
||||
controller.reset();
|
||||
assert.deepEqual(controller.state().facing, { x: 0.6, z: 0.8 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("walker collision", () => {
|
||||
|
||||
Reference in New Issue
Block a user