feat(adr-124/pseudocode): Zod schema barrel for all 20 ADR-124 §4.1+§4.1a tools
Advances SPARC Phase 2 (Pseudocode) — typed schemas are the language-level
design artifact that defines the complete tool surface before any HTTP/WS
plumbing is written. The schema map + TOOL_NAMES catalog are the pseudocode
contract that Phase 3 (Architecture) wires to the MCP Server dispatch loop.
New files under tools/ruview-mcp/src/schemas/:
common.ts — shared Zod sub-schemas
NodeIdSchema, DurationSSchema (max 3600 s), WindowSSchema (max 300 s),
SemanticPrimitiveKindSchema (10 ADR-115 primitives enum), PosePersonResultSchema
(17-keypoint COCO array + confidence + optional AETHER person_id)
tools.ts — 20 input schemas + TOOL_NAMES catalog + TOOL_INPUT_SCHEMAS dispatch map
§4.1 sensing (15): presence.now, vitals.get_{breathing,heart_rate,all},
pose.{latest,subscribe}, primitives.{get,list_active,subscribe},
bfld.{last_scan,subscribe}, node.{list,status},
vector.{search_pose,store_pose}
§4.1a policy (5): policy.{can_access_vitals, can_query_presence,
can_subscribe, redact_identity_fields, audit_log}
index.ts — barrel re-export of both modules
New test: tests/schemas.test.ts (24 assertions)
- Catalog completeness: exactly 20 tools, all §4.1 + §4.1a names present,
TOOL_INPUT_SCHEMAS one-to-one with catalog (no extras)
- Happy-path parse: 11 representative schemas accept valid inputs
- Constraint rejection: 8 schemas reject invalid inputs (empty NodeId,
DurationS=0 / >3600, unknown primitive, wrong keypoint length, k>100,
unknown vital, missing required node_id)
Fix: use Object.prototype.hasOwnProperty instead of Jest toHaveProperty for
dotted-key names (Jest interprets dots as nested path separators).
Test results: 50/50 PASS (schemas ×24 + manifest ×10 + tools ×5 + validate ×11)
Build: tsc clean, zero errors.
ACs touched: ADR-124 §4.1 complete tool surface; §4.1a policy layer surface;
Phase 2 gate: pseudocode covers all acceptance criteria from spec.
Next iter target: Phase 3 (Architecture) — wire TOOL_INPUT_SCHEMAS into the
MCP Server CallTool handler as a uniform validation gate; add Streamable HTTP
transport scaffold with Origin-validation middleware (option C).
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
d11ca9c31e
commit
7b7dbc7980
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* Shared Zod sub-schemas reused across the ADR-124 §4.1 tool catalog.
|
||||||
|
*
|
||||||
|
* All constraints are sourced from the ADR-124 decision record; comments cite
|
||||||
|
* the specific table row or section that defines the constraint.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// ── Shared primitives ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional node_id — present on almost every tool. Defaults to the single
|
||||||
|
* active node when only one is registered; required for multi-node fleets.
|
||||||
|
*/
|
||||||
|
export const NodeIdSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.optional()
|
||||||
|
.describe("Target node id. Omit to use the single active node.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription duration in seconds. ADR-124 policy layer caps this at the
|
||||||
|
* value returned by ruview.policy.can_subscribe.max_duration_s; the schema
|
||||||
|
* enforces a hard ceiling of 3600 s (1 h) as a first-line guard.
|
||||||
|
*/
|
||||||
|
export const DurationSSchema = z
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.max(3600)
|
||||||
|
.describe("Subscription duration in seconds (max 3600).");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional window in seconds for vitals averaging. Positive, max 300 s.
|
||||||
|
* ADR-124 §4.1 rows vitals.get_breathing / vitals.get_heart_rate.
|
||||||
|
*/
|
||||||
|
export const WindowSSchema = z
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.max(300)
|
||||||
|
.optional()
|
||||||
|
.describe("Averaging window in seconds (max 300).");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 10 semantic primitive kinds defined in ADR-115 and mirrored in
|
||||||
|
* python/wifi_densepose/client/primitives.py:36-45.
|
||||||
|
*/
|
||||||
|
export const SemanticPrimitiveKindSchema = z.enum([
|
||||||
|
"presence",
|
||||||
|
"n_persons",
|
||||||
|
"fall_detected",
|
||||||
|
"breathing_rate",
|
||||||
|
"heart_rate",
|
||||||
|
"gesture",
|
||||||
|
"zone_entry",
|
||||||
|
"zone_exit",
|
||||||
|
"movement_intensity",
|
||||||
|
"sleep_quality",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type SemanticPrimitiveKind = z.infer<typeof SemanticPrimitiveKindSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single 17-keypoint COCO pose result as stored and returned by the
|
||||||
|
* ruvector HNSW index (ADR-016). Used by ruview.vector.store_pose input.
|
||||||
|
*/
|
||||||
|
export const PosePersonResultSchema = z.object({
|
||||||
|
keypoints: z
|
||||||
|
.array(z.tuple([z.number(), z.number()]))
|
||||||
|
.length(17)
|
||||||
|
.describe("17 COCO keypoints as [x,y] pairs in image-normalised coords."),
|
||||||
|
confidence: z.number().min(0).max(1).describe("Pose confidence score [0,1]."),
|
||||||
|
person_id: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("AETHER re-ID token, if available."),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PosePersonResult = z.infer<typeof PosePersonResultSchema>;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Barrel re-export for @ruvnet/rvagent schema layer.
|
||||||
|
*
|
||||||
|
* Import from this module to get all Zod input schemas, shared sub-schemas,
|
||||||
|
* the TOOL_NAMES catalog, and the TOOL_INPUT_SCHEMAS dispatch map.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./common.js";
|
||||||
|
export * from "./tools.js";
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
/**
|
||||||
|
* Zod input schemas for all 20 ADR-124 MCP tools.
|
||||||
|
*
|
||||||
|
* §4.1 — 15 sensing tools (presence, vitals, pose, primitives, bfld, node, vector)
|
||||||
|
* §4.1a — 5 policy / governance tools (RUVIEW-POLICY)
|
||||||
|
*
|
||||||
|
* Each exported schema is named `<CamelCase>InputSchema` matching the tool
|
||||||
|
* name from the ADR-124 §4.1 catalog table. The parallel `TOOL_NAMES` array
|
||||||
|
* is the single source of truth asserted by the schema-coverage test.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
NodeIdSchema,
|
||||||
|
DurationSSchema,
|
||||||
|
WindowSSchema,
|
||||||
|
SemanticPrimitiveKindSchema,
|
||||||
|
PosePersonResultSchema,
|
||||||
|
} from "./common.js";
|
||||||
|
|
||||||
|
// ── §4.1 Presence ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.presence.now */
|
||||||
|
export const PresenceNowInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── §4.1 Vitals ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.vitals.get_breathing */
|
||||||
|
export const VitalsGetBreathingInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
window_s: WindowSSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.vitals.get_heart_rate */
|
||||||
|
export const VitalsGetHeartRateInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
window_s: WindowSSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.vitals.get_all */
|
||||||
|
export const VitalsGetAllInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── §4.1 Pose ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.pose.latest */
|
||||||
|
export const PoseLatestInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.pose.subscribe */
|
||||||
|
export const PoseSubscribeInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
duration_s: DurationSSchema,
|
||||||
|
callback_url: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.optional()
|
||||||
|
.describe("Webhook URL to receive PoseDataMessage events (optional)."),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── §4.1 Primitives ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.primitives.get */
|
||||||
|
export const PrimitivesGetInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
primitive: SemanticPrimitiveKindSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.primitives.list_active */
|
||||||
|
export const PrimitivesListActiveInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.primitives.subscribe */
|
||||||
|
export const PrimitivesSubscribeInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
primitive: SemanticPrimitiveKindSchema.optional().describe(
|
||||||
|
"Subscribe to a specific primitive. Omit to receive all active primitives."
|
||||||
|
),
|
||||||
|
duration_s: DurationSSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── §4.1 BFLD ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.bfld.last_scan */
|
||||||
|
export const BfldLastScanInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.bfld.subscribe */
|
||||||
|
export const BfldSubscribeInputSchema = z.object({
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
duration_s: DurationSSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── §4.1 Node ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.node.list — empty input per ADR-124 §4.1 table */
|
||||||
|
export const NodeListInputSchema = z.object({});
|
||||||
|
|
||||||
|
/** ruview.node.status */
|
||||||
|
export const NodeStatusInputSchema = z.object({
|
||||||
|
node_id: z.string().min(1).describe("Node id to query status for."),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── §4.1 Vector ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.vector.search_pose */
|
||||||
|
export const VectorSearchPoseInputSchema = z.object({
|
||||||
|
query_embedding: z
|
||||||
|
.array(z.number())
|
||||||
|
.min(1)
|
||||||
|
.describe("Dense embedding vector to query against the HNSW index."),
|
||||||
|
k: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.max(100)
|
||||||
|
.optional()
|
||||||
|
.default(10)
|
||||||
|
.describe("Number of nearest neighbours to return (default 10, max 100)."),
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.vector.store_pose */
|
||||||
|
export const VectorStorePoseInputSchema = z.object({
|
||||||
|
pose: PosePersonResultSchema,
|
||||||
|
node_id: z.string().min(1).describe("Node id that observed this pose."),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── §4.1a Policy / governance tools ──────────────────────────────────────
|
||||||
|
|
||||||
|
/** ruview.policy.can_access_vitals */
|
||||||
|
export const PolicyCanAccessVitalsInputSchema = z.object({
|
||||||
|
agent_id: z.string().min(1).describe("Calling agent identifier."),
|
||||||
|
node_id: z.string().min(1).describe("Target sensing node."),
|
||||||
|
vital: z
|
||||||
|
.enum(["breathing", "heart_rate", "all"])
|
||||||
|
.describe("Which vital the agent is requesting."),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.policy.can_query_presence */
|
||||||
|
export const PolicyCanQueryPresenceInputSchema = z.object({
|
||||||
|
agent_id: z.string().min(1),
|
||||||
|
scope: z
|
||||||
|
.enum(["node", "fleet"])
|
||||||
|
.describe("node = single node; fleet = all nodes / aggregated count."),
|
||||||
|
node_id: NodeIdSchema,
|
||||||
|
zone: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Named zone within a node (e.g. 'living_room')."),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.policy.can_subscribe */
|
||||||
|
export const PolicyCanSubscribeInputSchema = z.object({
|
||||||
|
agent_id: z.string().min(1),
|
||||||
|
topic: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.describe("MQTT topic or tool name the agent wishes to subscribe to."),
|
||||||
|
duration_s: DurationSSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.policy.redact_identity_fields */
|
||||||
|
export const PolicyRedactIdentityFieldsInputSchema = z.object({
|
||||||
|
payload: z.record(z.unknown()).describe("Tool return value to redact."),
|
||||||
|
agent_id: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ruview.policy.audit_log */
|
||||||
|
export const PolicyAuditLogInputSchema = z.object({
|
||||||
|
agent_id: z.string().optional().describe("Filter to a specific agent."),
|
||||||
|
since_ts: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Return events after this Unix timestamp (ms)."),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Catalog ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth: every tool name in the ADR-124 §4.1 + §4.1a catalog.
|
||||||
|
* The schema-coverage test asserts this list exactly matches the exported schemas.
|
||||||
|
*/
|
||||||
|
export const TOOL_NAMES = [
|
||||||
|
// §4.1 — 15 sensing tools
|
||||||
|
"ruview.presence.now",
|
||||||
|
"ruview.vitals.get_breathing",
|
||||||
|
"ruview.vitals.get_heart_rate",
|
||||||
|
"ruview.vitals.get_all",
|
||||||
|
"ruview.pose.latest",
|
||||||
|
"ruview.pose.subscribe",
|
||||||
|
"ruview.primitives.get",
|
||||||
|
"ruview.primitives.list_active",
|
||||||
|
"ruview.primitives.subscribe",
|
||||||
|
"ruview.bfld.last_scan",
|
||||||
|
"ruview.bfld.subscribe",
|
||||||
|
"ruview.node.list",
|
||||||
|
"ruview.node.status",
|
||||||
|
"ruview.vector.search_pose",
|
||||||
|
"ruview.vector.store_pose",
|
||||||
|
// §4.1a — 5 policy tools
|
||||||
|
"ruview.policy.can_access_vitals",
|
||||||
|
"ruview.policy.can_query_presence",
|
||||||
|
"ruview.policy.can_subscribe",
|
||||||
|
"ruview.policy.redact_identity_fields",
|
||||||
|
"ruview.policy.audit_log",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ToolName = (typeof TOOL_NAMES)[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from tool name → its Zod input schema. Used by the MCP server's
|
||||||
|
* CallTool handler for uniform schema-validation before dispatch.
|
||||||
|
*/
|
||||||
|
export const TOOL_INPUT_SCHEMAS: Record<ToolName, z.ZodTypeAny> = {
|
||||||
|
"ruview.presence.now": PresenceNowInputSchema,
|
||||||
|
"ruview.vitals.get_breathing": VitalsGetBreathingInputSchema,
|
||||||
|
"ruview.vitals.get_heart_rate": VitalsGetHeartRateInputSchema,
|
||||||
|
"ruview.vitals.get_all": VitalsGetAllInputSchema,
|
||||||
|
"ruview.pose.latest": PoseLatestInputSchema,
|
||||||
|
"ruview.pose.subscribe": PoseSubscribeInputSchema,
|
||||||
|
"ruview.primitives.get": PrimitivesGetInputSchema,
|
||||||
|
"ruview.primitives.list_active": PrimitivesListActiveInputSchema,
|
||||||
|
"ruview.primitives.subscribe": PrimitivesSubscribeInputSchema,
|
||||||
|
"ruview.bfld.last_scan": BfldLastScanInputSchema,
|
||||||
|
"ruview.bfld.subscribe": BfldSubscribeInputSchema,
|
||||||
|
"ruview.node.list": NodeListInputSchema,
|
||||||
|
"ruview.node.status": NodeStatusInputSchema,
|
||||||
|
"ruview.vector.search_pose": VectorSearchPoseInputSchema,
|
||||||
|
"ruview.vector.store_pose": VectorStorePoseInputSchema,
|
||||||
|
"ruview.policy.can_access_vitals": PolicyCanAccessVitalsInputSchema,
|
||||||
|
"ruview.policy.can_query_presence": PolicyCanQueryPresenceInputSchema,
|
||||||
|
"ruview.policy.can_subscribe": PolicyCanSubscribeInputSchema,
|
||||||
|
"ruview.policy.redact_identity_fields": PolicyRedactIdentityFieldsInputSchema,
|
||||||
|
"ruview.policy.audit_log": PolicyAuditLogInputSchema,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
/**
|
||||||
|
* ADR-124 §4.1 / §4.1a schema coverage tests.
|
||||||
|
*
|
||||||
|
* Guards:
|
||||||
|
* 1. Every catalogued tool name appears in TOOL_NAMES and TOOL_INPUT_SCHEMAS.
|
||||||
|
* 2. TOOL_INPUT_SCHEMAS has no extra (undocumented) keys.
|
||||||
|
* 3. Each schema accepts its documented happy-path input without throwing.
|
||||||
|
* 4. Each schema rejects structurally invalid input (Zod parse failure).
|
||||||
|
* 5. Shared sub-schemas (NodeId, DurationS, SemanticPrimitiveKind) enforce
|
||||||
|
* their documented constraints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
TOOL_NAMES,
|
||||||
|
TOOL_INPUT_SCHEMAS,
|
||||||
|
SemanticPrimitiveKindSchema,
|
||||||
|
DurationSSchema,
|
||||||
|
NodeIdSchema,
|
||||||
|
PosePersonResultSchema,
|
||||||
|
PresenceNowInputSchema,
|
||||||
|
VitalsGetBreathingInputSchema,
|
||||||
|
PrimitivesGetInputSchema,
|
||||||
|
BfldLastScanInputSchema,
|
||||||
|
NodeStatusInputSchema,
|
||||||
|
VectorSearchPoseInputSchema,
|
||||||
|
VectorStorePoseInputSchema,
|
||||||
|
PolicyCanAccessVitalsInputSchema,
|
||||||
|
PolicyCanSubscribeInputSchema,
|
||||||
|
PolicyRedactIdentityFieldsInputSchema,
|
||||||
|
} from "../src/schemas/index.js";
|
||||||
|
|
||||||
|
// ── 1. Catalog completeness ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("TOOL_NAMES catalog (ADR-124 §4.1 + §4.1a)", () => {
|
||||||
|
const EXPECTED_COUNT = 20; // 15 sensing + 5 policy
|
||||||
|
|
||||||
|
it("contains exactly 20 tools", () => {
|
||||||
|
expect(TOOL_NAMES).toHaveLength(EXPECTED_COUNT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains all 15 §4.1 sensing tool names", () => {
|
||||||
|
const sensing = [
|
||||||
|
"ruview.presence.now",
|
||||||
|
"ruview.vitals.get_breathing",
|
||||||
|
"ruview.vitals.get_heart_rate",
|
||||||
|
"ruview.vitals.get_all",
|
||||||
|
"ruview.pose.latest",
|
||||||
|
"ruview.pose.subscribe",
|
||||||
|
"ruview.primitives.get",
|
||||||
|
"ruview.primitives.list_active",
|
||||||
|
"ruview.primitives.subscribe",
|
||||||
|
"ruview.bfld.last_scan",
|
||||||
|
"ruview.bfld.subscribe",
|
||||||
|
"ruview.node.list",
|
||||||
|
"ruview.node.status",
|
||||||
|
"ruview.vector.search_pose",
|
||||||
|
"ruview.vector.store_pose",
|
||||||
|
];
|
||||||
|
for (const name of sensing) {
|
||||||
|
expect(TOOL_NAMES).toContain(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains all 5 §4.1a policy tool names", () => {
|
||||||
|
const policy = [
|
||||||
|
"ruview.policy.can_access_vitals",
|
||||||
|
"ruview.policy.can_query_presence",
|
||||||
|
"ruview.policy.can_subscribe",
|
||||||
|
"ruview.policy.redact_identity_fields",
|
||||||
|
"ruview.policy.audit_log",
|
||||||
|
];
|
||||||
|
for (const name of policy) {
|
||||||
|
expect(TOOL_NAMES).toContain(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("TOOL_INPUT_SCHEMAS has a schema for every catalogued tool name", () => {
|
||||||
|
for (const name of TOOL_NAMES) {
|
||||||
|
// Use Object.prototype.hasOwnProperty to avoid Jest's dotted-path
|
||||||
|
// interpretation of toHaveProperty (dots = nested path in Jest).
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(TOOL_INPUT_SCHEMAS, name)).toBe(true);
|
||||||
|
expect(TOOL_INPUT_SCHEMAS[name]).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("TOOL_INPUT_SCHEMAS has no extra keys beyond the catalog", () => {
|
||||||
|
const schemaKeys = Object.keys(TOOL_INPUT_SCHEMAS).sort();
|
||||||
|
const catalogKeys = [...TOOL_NAMES].sort();
|
||||||
|
expect(schemaKeys).toEqual(catalogKeys);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 2. Happy-path parse ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Schema happy-path acceptance", () => {
|
||||||
|
it("PresenceNow — accepts empty object (node_id optional)", () => {
|
||||||
|
expect(() => PresenceNowInputSchema.parse({})).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PresenceNow — accepts object with node_id", () => {
|
||||||
|
const r = PresenceNowInputSchema.parse({ node_id: "node-abc" });
|
||||||
|
expect(r.node_id).toBe("node-abc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("VitalsGetBreathing — accepts window_s and node_id", () => {
|
||||||
|
const r = VitalsGetBreathingInputSchema.parse({ window_s: 30, node_id: "n1" });
|
||||||
|
expect(r.window_s).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PrimitivesGet — accepts valid primitive kind", () => {
|
||||||
|
const r = PrimitivesGetInputSchema.parse({ primitive: "fall_detected" });
|
||||||
|
expect(r.primitive).toBe("fall_detected");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("BfldLastScan — accepts empty object", () => {
|
||||||
|
expect(() => BfldLastScanInputSchema.parse({})).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("NodeStatus — accepts node_id string", () => {
|
||||||
|
const r = NodeStatusInputSchema.parse({ node_id: "cognitum-seed-1" });
|
||||||
|
expect(r.node_id).toBe("cognitum-seed-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("VectorSearchPose — applies default k=10", () => {
|
||||||
|
const r = VectorSearchPoseInputSchema.parse({ query_embedding: [0.1, 0.2, 0.3] });
|
||||||
|
expect(r.k).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("VectorStorePose — accepts a valid 17-keypoint pose", () => {
|
||||||
|
const kpts = Array.from({ length: 17 }, (_, i) => [i * 0.05, i * 0.03] as [number, number]);
|
||||||
|
const r = VectorStorePoseInputSchema.parse({
|
||||||
|
pose: { keypoints: kpts, confidence: 0.92 },
|
||||||
|
node_id: "node-x",
|
||||||
|
});
|
||||||
|
expect(r.pose.keypoints).toHaveLength(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PolicyCanAccessVitals — accepts valid vital value", () => {
|
||||||
|
const r = PolicyCanAccessVitalsInputSchema.parse({
|
||||||
|
agent_id: "agent-007",
|
||||||
|
node_id: "node-1",
|
||||||
|
vital: "heart_rate",
|
||||||
|
});
|
||||||
|
expect(r.vital).toBe("heart_rate");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PolicyCanSubscribe — accepts valid duration_s", () => {
|
||||||
|
const r = PolicyCanSubscribeInputSchema.parse({
|
||||||
|
agent_id: "agent-007",
|
||||||
|
topic: "ruview.vitals.get_all",
|
||||||
|
duration_s: 300,
|
||||||
|
});
|
||||||
|
expect(r.duration_s).toBe(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PolicyRedactIdentityFields — accepts arbitrary payload record", () => {
|
||||||
|
const r = PolicyRedactIdentityFieldsInputSchema.parse({
|
||||||
|
payload: { sta_mac: "AA:BB:CC:DD:EE:FF", n_persons: 2 },
|
||||||
|
agent_id: "agent-007",
|
||||||
|
});
|
||||||
|
expect(r.payload).toHaveProperty("sta_mac");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 3. Constraint rejection ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Schema constraint enforcement", () => {
|
||||||
|
it("NodeIdSchema — rejects empty string", () => {
|
||||||
|
expect(() => NodeIdSchema.parse("")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DurationSSchema — rejects zero", () => {
|
||||||
|
expect(() => DurationSSchema.parse(0)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DurationSSchema — rejects value > 3600", () => {
|
||||||
|
expect(() => DurationSSchema.parse(3601)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("SemanticPrimitiveKind — rejects unknown primitive", () => {
|
||||||
|
expect(() => SemanticPrimitiveKindSchema.parse("unknown_primitive")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PosePersonResult — rejects keypoints array with wrong length", () => {
|
||||||
|
const badKpts = Array.from({ length: 5 }, () => [0, 0] as [number, number]);
|
||||||
|
expect(() => PosePersonResultSchema.parse({ keypoints: badKpts, confidence: 0.9 })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("VectorSearchPose — rejects k > 100", () => {
|
||||||
|
expect(() =>
|
||||||
|
VectorSearchPoseInputSchema.parse({ query_embedding: [0.1], k: 101 })
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PolicyCanAccessVitals — rejects unknown vital value", () => {
|
||||||
|
expect(() =>
|
||||||
|
PolicyCanAccessVitalsInputSchema.parse({
|
||||||
|
agent_id: "a",
|
||||||
|
node_id: "n",
|
||||||
|
vital: "temperature",
|
||||||
|
})
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("NodeStatus — rejects missing node_id", () => {
|
||||||
|
expect(() => NodeStatusInputSchema.parse({})).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue