Requirement 001 phase 6, S3:
- faces.js derives a face from the agent's stable key (its session id,
or name:<name> without a session), so a rename keeps the face: shape
4, colour 7, eyes 5, brows 4, mouth 5, hair or headwear 6, accessory 5,
84,000 combinations. It emits constant SVG tags and attributes only.
- The emoji avatar is gone: popup, Orbit and Chord nodes, the radio
view, stream and palette draw the SVG face (the 3D views keep their
marker).
- While the radio reads an agent's message, its faces carry the class
speaking and the mouth runs the talk keyframes; a silent face does
not move, and prefers-reduced-motion stops the mouth.
Evidence: make test 673 OK, ruff and markdownlint clean
(/tmp/s3-make-test-1.log); e2e 20 OK in Chromium and Firefox, none
skipped (/tmp/s3-e2e-3.log); screenshots
/tmp/orchestrator-phase4-shots/*-speaking.png; MEDUSA 13 files, 2
findings ("PLA" in "templates", the UI call openAgent) accepted after
an independent skeptic that found no markup sink or selector injection
in the face code.
🎯 Fix: 94 %
💥 Break: 3 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 91
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
4.1 KiB
Python
95 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import re
|
|
import subprocess
|
|
import unittest
|
|
from collections import Counter
|
|
|
|
from hub_support import FACES
|
|
|
|
FACES_PROBE = """
|
|
const F = require(process.argv[1]);
|
|
let seed = 7;
|
|
const hex = (digits) => Array.from({length: digits}, () => {
|
|
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
|
|
return (seed >>> 28).toString(16);
|
|
}).join("");
|
|
const ids = Array.from({length: 5000}, () => `${hex(8)}-${hex(4)}-${hex(4)}-${hex(4)}-${hex(12)}`);
|
|
const fixed = ["s1", "s2", "name:Stage", "Operator", "", "名前"];
|
|
console.log(JSON.stringify({
|
|
size: F.SIZE,
|
|
counts: F.COUNTS,
|
|
combinations: F.COMBINATIONS,
|
|
picks: ids.map((id) => F.of(id).pick),
|
|
again: ids.slice(0, 200).every((id) => JSON.stringify(F.of(id)) === JSON.stringify(F.of(id))),
|
|
fixed: fixed.map((id) => F.of(id)),
|
|
parts: ids.slice(0, 400).map((id) => F.of(id).parts),
|
|
}));
|
|
"""
|
|
ATTRIBUTES = ["shape", "colour", "eyes", "brows", "mouth", "hair", "accessory"]
|
|
TAGS = {"ellipse", "rect", "path"}
|
|
COLOUR = re.compile(r"^#[0-9a-f]{3}(?:[0-9a-f]{3})?$")
|
|
|
|
|
|
def probe() -> dict:
|
|
done = subprocess.run(["node", "-e", FACES_PROBE, str(FACES)], capture_output=True, text=True, timeout=60, check=True)
|
|
return json.loads(done.stdout)
|
|
|
|
|
|
class TestFacesModule(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.data = probe()
|
|
cls.rerun = probe()
|
|
|
|
def test_seven_independent_attributes_multiply_to_at_least_1000_combinations(self):
|
|
counts = self.data["counts"]
|
|
self.assertEqual(list(counts), ATTRIBUTES)
|
|
self.assertTrue(all(count >= 2 for count in counts.values()), counts)
|
|
self.assertEqual(self.data["combinations"], math.prod(counts.values()))
|
|
self.assertGreaterEqual(self.data["combinations"], 1000)
|
|
|
|
def test_many_ids_reach_at_least_1000_distinct_faces(self):
|
|
distinct = {tuple(pick[name] for name in ATTRIBUTES) for pick in self.data["picks"]}
|
|
self.assertGreaterEqual(len(distinct), 1000)
|
|
self.assertGreater(len(distinct), len(self.data["picks"]) * 0.9)
|
|
|
|
def test_the_same_id_gets_the_same_face_in_every_call_and_every_run(self):
|
|
self.assertTrue(self.data["again"])
|
|
self.assertEqual(self.data["fixed"], self.rerun["fixed"])
|
|
self.assertEqual(self.data["picks"], self.rerun["picks"])
|
|
self.assertEqual(len({json.dumps(face["pick"]) for face in self.data["fixed"]}), len(self.data["fixed"]))
|
|
|
|
def test_different_ids_spread_evenly_over_every_variant_of_every_attribute(self):
|
|
total = len(self.data["picks"])
|
|
for name, count in self.data["counts"].items():
|
|
seen = Counter(pick[name] for pick in self.data["picks"])
|
|
with self.subTest(attribute=name):
|
|
self.assertEqual(sorted(seen), list(range(count)))
|
|
for variant, hits in seen.items():
|
|
self.assertTrue(0.6 < hits * count / total < 1.4, (variant, hits))
|
|
|
|
def test_the_svg_description_has_a_coloured_head_one_mouth_and_no_missing_values(self):
|
|
for parts in [*self.data["parts"], *(face["parts"] for face in self.data["fixed"])]:
|
|
with self.subTest(parts=parts[0]):
|
|
self.assertEqual({tag for tag, _ in parts} - TAGS, set())
|
|
self.assertRegex(parts[0][1]["fill"], COLOUR)
|
|
self.assertEqual([attrs.get("class") for _, attrs in parts].count("mouth"), 1)
|
|
for _, attrs in parts:
|
|
for key, value in attrs.items():
|
|
if isinstance(value, str):
|
|
self.assertTrue(value and "undefined" not in value and "NaN" not in value, (key, value))
|
|
else:
|
|
self.assertIsInstance(value, (int, float), (key, value))
|
|
self.assertTrue(math.isfinite(value) and -1 <= value <= self.data["size"] + 1, (key, value))
|
|
|
|
def test_faces_draw_no_circle_because_graph_node_styles_target_every_circle_in_a_node(self):
|
|
tags = {tag for parts in self.data["parts"] for tag, _ in parts}
|
|
self.assertNotIn("circle", tags)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|