gridstamp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +74 -0
- package/CLAUDE.md +61 -0
- package/LICENSE +190 -0
- package/README.md +107 -0
- package/dist/index.js +194 -0
- package/package.json +84 -0
- package/src/antispoofing/detector.ts +509 -0
- package/src/antispoofing/index.ts +7 -0
- package/src/gamification/badges.ts +429 -0
- package/src/gamification/fleet-leaderboard.ts +293 -0
- package/src/gamification/index.ts +44 -0
- package/src/gamification/streaks.ts +243 -0
- package/src/gamification/trust-tiers.ts +393 -0
- package/src/gamification/zone-mastery.ts +256 -0
- package/src/index.ts +341 -0
- package/src/memory/index.ts +9 -0
- package/src/memory/place-cells.ts +279 -0
- package/src/memory/spatial-memory.ts +375 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/pathfinding.ts +403 -0
- package/src/perception/camera.ts +249 -0
- package/src/perception/index.ts +2 -0
- package/src/types/index.ts +416 -0
- package/src/utils/crypto.ts +94 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/math.ts +204 -0
- package/src/verification/index.ts +9 -0
- package/src/verification/spatial-proof.ts +442 -0
- package/tests/antispoofing/detector.test.ts +196 -0
- package/tests/gamification/badges.test.ts +163 -0
- package/tests/gamification/fleet-leaderboard.test.ts +181 -0
- package/tests/gamification/streaks.test.ts +158 -0
- package/tests/gamification/trust-tiers.test.ts +165 -0
- package/tests/gamification/zone-mastery.test.ts +143 -0
- package/tests/memory/place-cells.test.ts +128 -0
- package/tests/stress/load.test.ts +499 -0
- package/tests/stress/security.test.ts +378 -0
- package/tests/stress/simulation.test.ts +361 -0
- package/tests/utils/crypto.test.ts +115 -0
- package/tests/utils/math.test.ts +195 -0
- package/tests/verification/spatial-proof.test.ts +299 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Simulation — Full robot lifecycle from registration to elite status
|
|
3
|
+
*
|
|
4
|
+
* Simulates a realistic fleet scenario:
|
|
5
|
+
* - Fleet of 20 delivery robots
|
|
6
|
+
* - 90-day operation period
|
|
7
|
+
* - Varying success rates, zone visits, incidents
|
|
8
|
+
* - One attacker bot attempting spoofing
|
|
9
|
+
* - Tracks all metrics for competitive analysis output
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
TrustTierSystem,
|
|
15
|
+
TrustTier,
|
|
16
|
+
BadgeSystem,
|
|
17
|
+
StreakSystem,
|
|
18
|
+
ZoneMasterySystem,
|
|
19
|
+
FleetLeaderboard,
|
|
20
|
+
LeaderboardMetric,
|
|
21
|
+
type RobotMetrics,
|
|
22
|
+
} from '../../src/gamification/index.js';
|
|
23
|
+
import {
|
|
24
|
+
generateSpatialProof,
|
|
25
|
+
verifySpatialProofIntegrity,
|
|
26
|
+
createSettlement,
|
|
27
|
+
} from '../../src/verification/spatial-proof.js';
|
|
28
|
+
import { signFrame, generateNonce } from '../../src/utils/crypto.js';
|
|
29
|
+
import { PlaceCellPopulation, GridCellSystem, computeSpatialCode } from '../../src/memory/place-cells.js';
|
|
30
|
+
import type { CameraFrame, Pose, RenderedView } from '../../src/types/index.js';
|
|
31
|
+
|
|
32
|
+
const SECRET = 'z'.repeat(32);
|
|
33
|
+
const FLEET_ID = 'dallas-delivery-fleet';
|
|
34
|
+
const NUM_ROBOTS = 20;
|
|
35
|
+
const SIMULATION_DAYS = 90;
|
|
36
|
+
const OPS_PER_DAY = 8; // 8 deliveries per robot per day
|
|
37
|
+
|
|
38
|
+
function makePose(x: number, y: number): Pose {
|
|
39
|
+
return { position: { x, y, z: 0 }, orientation: { w: 1, x: 0, y: 0, z: 0 }, timestamp: Date.now() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeFrame(seed: number, seq: number): CameraFrame {
|
|
43
|
+
const rgb = new Uint8Array(32 * 32 * 3);
|
|
44
|
+
for (let i = 0; i < rgb.length; i++) rgb[i] = (i * 17 + seed * 31) % 256;
|
|
45
|
+
const ts = Date.now();
|
|
46
|
+
return {
|
|
47
|
+
id: generateNonce(8), timestamp: ts, rgb, width: 32, height: 32,
|
|
48
|
+
depth: new Float32Array(32 * 32).fill(2.5),
|
|
49
|
+
pose: makePose(seed, seed),
|
|
50
|
+
hmac: signFrame(rgb, ts, seq, SECRET),
|
|
51
|
+
sequenceNumber: seq,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('Fleet Simulation: 20 robots × 90 days', () => {
|
|
56
|
+
// Shared state across the simulation
|
|
57
|
+
const trustSystem = new TrustTierSystem(SECRET);
|
|
58
|
+
const badgeSystem = new BadgeSystem(SECRET);
|
|
59
|
+
const streakSystem = new StreakSystem(SECRET);
|
|
60
|
+
const zoneMastery = new ZoneMasterySystem(SECRET, 5.0);
|
|
61
|
+
const leaderboard = new FleetLeaderboard(SECRET);
|
|
62
|
+
|
|
63
|
+
// Define realistic delivery zones
|
|
64
|
+
const zones = [
|
|
65
|
+
{ name: 'Downtown Dallas', min: { x: 0, y: 0, z: 0 }, max: { x: 50, y: 50, z: 3 } },
|
|
66
|
+
{ name: 'Deep Ellum', min: { x: 50, y: 0, z: 0 }, max: { x: 100, y: 30, z: 3 } },
|
|
67
|
+
{ name: 'Uptown', min: { x: 0, y: 50, z: 0 }, max: { x: 40, y: 100, z: 3 } },
|
|
68
|
+
{ name: 'Bishop Arts', min: { x: -30, y: -30, z: 0 }, max: { x: 0, y: 0, z: 3 } },
|
|
69
|
+
{ name: 'Oak Lawn', min: { x: -30, y: 50, z: 0 }, max: { x: 0, y: 100, z: 3 } },
|
|
70
|
+
{ name: 'Fair Park', min: { x: 60, y: 30, z: 0 }, max: { x: 100, y: 60, z: 3 } },
|
|
71
|
+
{ name: 'Greenville Ave', min: { x: 30, y: 70, z: 0 }, max: { x: 60, y: 100, z: 3 } },
|
|
72
|
+
{ name: 'Design District', min: { x: -50, y: 20, z: 0 }, max: { x: -20, y: 50, z: 3 } },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Robot personality profiles (different success rates simulate real fleet variance)
|
|
76
|
+
const robotProfiles = Array.from({ length: NUM_ROBOTS }, (_, i) => ({
|
|
77
|
+
id: `DLV-${String(i + 1).padStart(3, '0')}`,
|
|
78
|
+
successRate: i === 19 ? 0.3 : 0.85 + Math.random() * 0.14, // Robot 20 is an attacker (30%)
|
|
79
|
+
isAttacker: i === 19,
|
|
80
|
+
preferredZones: [i % zones.length, (i + 3) % zones.length, (i + 5) % zones.length],
|
|
81
|
+
nightOperations: Math.floor(Math.random() * 40),
|
|
82
|
+
fastOps: Math.floor(Math.random() * 15),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
it('Phase 1: Registration', () => {
|
|
86
|
+
for (const robot of robotProfiles) {
|
|
87
|
+
trustSystem.register(robot.id);
|
|
88
|
+
streakSystem.register(robot.id);
|
|
89
|
+
}
|
|
90
|
+
for (const zone of zones) {
|
|
91
|
+
zoneMastery.defineZone(zone.name, { min: zone.min, max: zone.max });
|
|
92
|
+
}
|
|
93
|
+
expect(trustSystem.robotCount).toBe(NUM_ROBOTS);
|
|
94
|
+
expect(streakSystem.robotCount).toBe(NUM_ROBOTS);
|
|
95
|
+
expect(zoneMastery.totalZones).toBe(8);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('Phase 2: 90-day operation simulation', () => {
|
|
99
|
+
const baseDate = new Date('2025-06-01T08:00:00Z').getTime();
|
|
100
|
+
const simStats: Record<string, {
|
|
101
|
+
totalOps: number;
|
|
102
|
+
successes: number;
|
|
103
|
+
failures: number;
|
|
104
|
+
spoofAttempts: number;
|
|
105
|
+
pointsEarned: number;
|
|
106
|
+
}> = {};
|
|
107
|
+
|
|
108
|
+
for (const robot of robotProfiles) {
|
|
109
|
+
simStats[robot.id] = { totalOps: 0, successes: 0, failures: 0, spoofAttempts: 0, pointsEarned: 0 };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (let day = 0; day < SIMULATION_DAYS; day++) {
|
|
113
|
+
const dayTimestamp = baseDate + day * 86400000;
|
|
114
|
+
|
|
115
|
+
for (const robot of robotProfiles) {
|
|
116
|
+
const stats = simStats[robot.id]!;
|
|
117
|
+
let daySuccess = true;
|
|
118
|
+
|
|
119
|
+
for (let op = 0; op < OPS_PER_DAY; op++) {
|
|
120
|
+
stats.totalOps++;
|
|
121
|
+
const isSuccess = Math.random() < robot.successRate;
|
|
122
|
+
const isSpoofAttempt = robot.isAttacker && Math.random() < 0.2;
|
|
123
|
+
|
|
124
|
+
if (isSpoofAttempt) {
|
|
125
|
+
stats.spoofAttempts++;
|
|
126
|
+
trustSystem.recordFailure(robot.id, true);
|
|
127
|
+
streakSystem.breakStreak(robot.id);
|
|
128
|
+
daySuccess = false;
|
|
129
|
+
} else if (isSuccess) {
|
|
130
|
+
stats.successes++;
|
|
131
|
+
const basePoints = 50;
|
|
132
|
+
const streakResult = streakSystem.recordActivity(robot.id, basePoints, dayTimestamp);
|
|
133
|
+
const totalEarned = streakResult.totalPoints;
|
|
134
|
+
stats.pointsEarned += totalEarned;
|
|
135
|
+
trustSystem.recordSuccess(robot.id, totalEarned);
|
|
136
|
+
|
|
137
|
+
// Visit a zone
|
|
138
|
+
const zoneIdx = robot.preferredZones[op % robot.preferredZones.length]!;
|
|
139
|
+
const zone = zones[zoneIdx]!;
|
|
140
|
+
const pos = {
|
|
141
|
+
x: zone.min.x + Math.random() * (zone.max.x - zone.min.x),
|
|
142
|
+
y: zone.min.y + Math.random() * (zone.max.y - zone.min.y),
|
|
143
|
+
z: 1,
|
|
144
|
+
};
|
|
145
|
+
zoneMastery.recordVisit(robot.id, pos, true, totalEarned);
|
|
146
|
+
} else {
|
|
147
|
+
stats.failures++;
|
|
148
|
+
trustSystem.recordFailure(robot.id, false);
|
|
149
|
+
daySuccess = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Verify simulation completed
|
|
156
|
+
const totalOps = Object.values(simStats).reduce((s, v) => s + v.totalOps, 0);
|
|
157
|
+
expect(totalOps).toBe(NUM_ROBOTS * SIMULATION_DAYS * OPS_PER_DAY); // 14,400 operations
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('Phase 3: Badge evaluation', () => {
|
|
161
|
+
for (const robot of robotProfiles) {
|
|
162
|
+
const profile = trustSystem.getProfile(robot.id)!;
|
|
163
|
+
const streakRecord = streakSystem.getRecord(robot.id)!;
|
|
164
|
+
const metrics: RobotMetrics = {
|
|
165
|
+
successful_verifications: profile.successfulVerifications,
|
|
166
|
+
spoofing_incidents: profile.spoofingIncidents,
|
|
167
|
+
threats_detected: 0,
|
|
168
|
+
zones_mapped: zoneMastery.getZoneCount(robot.id),
|
|
169
|
+
max_zone_mastery: zoneMastery.getMaxMastery(robot.id),
|
|
170
|
+
unique_routes: Math.floor(profile.successfulVerifications * 0.3),
|
|
171
|
+
consecutive_days: streakRecord.longestStreak,
|
|
172
|
+
night_operations: robot.nightOperations,
|
|
173
|
+
total_verifications: profile.totalVerifications,
|
|
174
|
+
success_rate: profile.totalVerifications > 0
|
|
175
|
+
? (profile.successfulVerifications / profile.totalVerifications) * 100
|
|
176
|
+
: 0,
|
|
177
|
+
max_ssim: 0.85 + Math.random() * 0.14,
|
|
178
|
+
max_zones_per_trip: robot.preferredZones.length,
|
|
179
|
+
fast_operations: robot.fastOps,
|
|
180
|
+
};
|
|
181
|
+
badgeSystem.evaluate(robot.id, metrics);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Top performers should have many badges
|
|
185
|
+
const topRobot = robotProfiles[0]!;
|
|
186
|
+
const topBadges = badgeSystem.getBadgeCount(topRobot.id);
|
|
187
|
+
expect(topBadges).toBeGreaterThan(5);
|
|
188
|
+
|
|
189
|
+
// Attacker should have fewer badges
|
|
190
|
+
const attacker = robotProfiles.find(r => r.isAttacker)!;
|
|
191
|
+
const attackerBadges = badgeSystem.getBadgeCount(attacker.id);
|
|
192
|
+
expect(attackerBadges).toBeLessThan(topBadges);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('Phase 4: Leaderboard generation', () => {
|
|
196
|
+
for (const robot of robotProfiles) {
|
|
197
|
+
const profile = trustSystem.getProfile(robot.id)!;
|
|
198
|
+
const streakRecord = streakSystem.getRecord(robot.id)!;
|
|
199
|
+
leaderboard.updateStats({
|
|
200
|
+
robotId: robot.id,
|
|
201
|
+
fleetId: FLEET_ID,
|
|
202
|
+
trustTier: profile.currentTier,
|
|
203
|
+
points: profile.points,
|
|
204
|
+
totalVerifications: profile.totalVerifications,
|
|
205
|
+
successfulVerifications: profile.successfulVerifications,
|
|
206
|
+
zonesExplored: zoneMastery.getZoneCount(robot.id),
|
|
207
|
+
badgeCount: badgeSystem.getBadgeCount(robot.id),
|
|
208
|
+
streakDays: streakRecord.currentStreak,
|
|
209
|
+
maxZoneMastery: zoneMastery.getMaxMastery(robot.id),
|
|
210
|
+
spoofingIncidents: profile.spoofingIncidents,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const board = leaderboard.getFleetLeaderboard(FLEET_ID, LeaderboardMetric.COMPOSITE);
|
|
215
|
+
expect(board).toHaveLength(NUM_ROBOTS);
|
|
216
|
+
|
|
217
|
+
// Attacker should be near bottom
|
|
218
|
+
const attacker = robotProfiles.find(r => r.isAttacker)!;
|
|
219
|
+
const attackerEntry = board.find(e => e.robotId === attacker.id)!;
|
|
220
|
+
expect(attackerEntry.rank).toBeGreaterThan(NUM_ROBOTS / 2);
|
|
221
|
+
|
|
222
|
+
// Top robot should be a high-performer
|
|
223
|
+
const topEntry = board[0]!;
|
|
224
|
+
expect(topEntry.score).toBeGreaterThan(0);
|
|
225
|
+
expect(topEntry.spoofingIncidents).toBe(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('Phase 5: Trust tier distribution analysis', () => {
|
|
229
|
+
const tierCounts: Record<TrustTier, number> = {
|
|
230
|
+
[TrustTier.UNTRUSTED]: 0,
|
|
231
|
+
[TrustTier.PROBATION]: 0,
|
|
232
|
+
[TrustTier.VERIFIED]: 0,
|
|
233
|
+
[TrustTier.TRUSTED]: 0,
|
|
234
|
+
[TrustTier.ELITE]: 0,
|
|
235
|
+
[TrustTier.AUTONOMOUS]: 0,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
for (const robot of robotProfiles) {
|
|
239
|
+
const profile = trustSystem.getProfile(robot.id)!;
|
|
240
|
+
tierCounts[profile.currentTier]++;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// After 90 days of mostly-successful operations:
|
|
244
|
+
// Most robots should be at Verified or above
|
|
245
|
+
const advancedCount = tierCounts[TrustTier.VERIFIED]
|
|
246
|
+
+ tierCounts[TrustTier.TRUSTED]
|
|
247
|
+
+ tierCounts[TrustTier.ELITE]
|
|
248
|
+
+ tierCounts[TrustTier.AUTONOMOUS];
|
|
249
|
+
expect(advancedCount).toBeGreaterThan(NUM_ROBOTS / 2);
|
|
250
|
+
|
|
251
|
+
// Attacker should be at low tier
|
|
252
|
+
const attacker = robotProfiles.find(r => r.isAttacker)!;
|
|
253
|
+
const attackerProfile = trustSystem.getProfile(attacker.id)!;
|
|
254
|
+
expect(attackerProfile.currentTier).toBeLessThanOrEqual(TrustTier.PROBATION);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('Phase 6: Fleet summary health check', () => {
|
|
258
|
+
const summary = leaderboard.getFleetSummary(FLEET_ID);
|
|
259
|
+
expect(summary.robotCount).toBe(NUM_ROBOTS);
|
|
260
|
+
expect(summary.totalDeliveries).toBeGreaterThan(0);
|
|
261
|
+
// Average success rate should reflect mostly-good fleet
|
|
262
|
+
expect(summary.averageSuccessRate).toBeGreaterThan(0.7);
|
|
263
|
+
expect(summary.topRobotId).toBeTruthy();
|
|
264
|
+
expect(summary.totalZonesExplored).toBeGreaterThan(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('Phase 7: Integrity verification', () => {
|
|
268
|
+
// All tier histories should be valid
|
|
269
|
+
for (const robot of robotProfiles) {
|
|
270
|
+
const result = trustSystem.verifyHistory(robot.id);
|
|
271
|
+
expect(result.valid).toBe(true);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// All badges should be authentic
|
|
275
|
+
for (const robot of robotProfiles) {
|
|
276
|
+
const result = badgeSystem.verifyAllBadges(robot.id);
|
|
277
|
+
expect(result.valid).toBe(true);
|
|
278
|
+
expect(result.forged).toHaveLength(0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// All streak records should verify
|
|
282
|
+
for (const robot of robotProfiles) {
|
|
283
|
+
expect(streakSystem.verifyRecord(robot.id)).toBe(true);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Leaderboard entries should be signed
|
|
287
|
+
const board = leaderboard.getFleetLeaderboard(FLEET_ID);
|
|
288
|
+
for (const entry of board) {
|
|
289
|
+
expect(entry.signature).toHaveLength(64);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('Phase 8: Spatial verification integration', () => {
|
|
294
|
+
// Run 10 real spatial verifications to ensure the pipeline works
|
|
295
|
+
let passed = 0;
|
|
296
|
+
let failed = 0;
|
|
297
|
+
|
|
298
|
+
for (let i = 0; i < 10; i++) {
|
|
299
|
+
const frame = makeFrame(i, i + 1);
|
|
300
|
+
const identicalRender: RenderedView = {
|
|
301
|
+
rgb: frame.rgb, depth: frame.depth!, width: 32, height: 32,
|
|
302
|
+
pose: makePose(i, i), renderTimeMs: 1,
|
|
303
|
+
};
|
|
304
|
+
const proof = generateSpatialProof(
|
|
305
|
+
'DLV-001', makePose(i, i), frame, identicalRender,
|
|
306
|
+
'merkle-root', [], SECRET,
|
|
307
|
+
);
|
|
308
|
+
const integrity = verifySpatialProofIntegrity(proof, SECRET);
|
|
309
|
+
expect(integrity.valid).toBe(true);
|
|
310
|
+
|
|
311
|
+
if (proof.passed) {
|
|
312
|
+
const settlement = createSettlement(proof, 15.00, 'USD', 'merchant-1', SECRET);
|
|
313
|
+
expect(settlement.status).toBe('verified');
|
|
314
|
+
passed++;
|
|
315
|
+
} else {
|
|
316
|
+
failed++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// With identical render/frame, all should pass
|
|
320
|
+
expect(passed).toBe(10);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('Phase 9: Performance benchmarks', () => {
|
|
324
|
+
// Benchmark: badge evaluation for 20 robots
|
|
325
|
+
const start1 = performance.now();
|
|
326
|
+
for (let round = 0; round < 100; round++) {
|
|
327
|
+
for (const robot of robotProfiles) {
|
|
328
|
+
const profile = trustSystem.getProfile(robot.id)!;
|
|
329
|
+
badgeSystem.evaluate(robot.id, {
|
|
330
|
+
successful_verifications: profile.successfulVerifications,
|
|
331
|
+
spoofing_incidents: profile.spoofingIncidents,
|
|
332
|
+
threats_detected: 0, zones_mapped: 3, max_zone_mastery: 0.5,
|
|
333
|
+
unique_routes: 10, consecutive_days: 30, night_operations: 5,
|
|
334
|
+
total_verifications: profile.totalVerifications, success_rate: 90,
|
|
335
|
+
max_ssim: 0.9, max_zones_per_trip: 2, fast_operations: 3,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const badgeEvalMs = performance.now() - start1;
|
|
340
|
+
expect(badgeEvalMs).toBeLessThan(3000); // 2000 evaluations under 3s
|
|
341
|
+
|
|
342
|
+
// Benchmark: leaderboard generation
|
|
343
|
+
const start2 = performance.now();
|
|
344
|
+
for (let round = 0; round < 100; round++) {
|
|
345
|
+
leaderboard.getFleetLeaderboard(FLEET_ID, LeaderboardMetric.COMPOSITE);
|
|
346
|
+
}
|
|
347
|
+
const leaderboardMs = performance.now() - start2;
|
|
348
|
+
expect(leaderboardMs).toBeLessThan(2000); // 100 leaderboard generations under 2s
|
|
349
|
+
|
|
350
|
+
// Benchmark: spatial code computation
|
|
351
|
+
const placeCells = new PlaceCellPopulation(2.0);
|
|
352
|
+
placeCells.coverRegion({ x: 0, y: 0, z: 0 }, { x: 50, y: 50, z: 0 }, 3.0);
|
|
353
|
+
const gridCells = new GridCellSystem(0.5, Math.SQRT2, 4, 16);
|
|
354
|
+
const start3 = performance.now();
|
|
355
|
+
for (let i = 0; i < 1000; i++) {
|
|
356
|
+
computeSpatialCode({ x: Math.random() * 50, y: Math.random() * 50, z: 0 }, placeCells, gridCells);
|
|
357
|
+
}
|
|
358
|
+
const spatialCodeMs = performance.now() - start3;
|
|
359
|
+
expect(spatialCodeMs).toBeLessThan(5000); // 1000 spatial codes under 5s
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
hmacSign,
|
|
4
|
+
hmacVerify,
|
|
5
|
+
signFrame,
|
|
6
|
+
verifyFrame,
|
|
7
|
+
generateNonce,
|
|
8
|
+
sha256,
|
|
9
|
+
deriveKey,
|
|
10
|
+
} from '../../src/utils/crypto.js';
|
|
11
|
+
|
|
12
|
+
const TEST_SECRET = 'a'.repeat(32); // minimum length secret for testing
|
|
13
|
+
|
|
14
|
+
describe('Crypto Utilities', () => {
|
|
15
|
+
describe('hmacSign / hmacVerify', () => {
|
|
16
|
+
it('signs and verifies data correctly', () => {
|
|
17
|
+
const data = Buffer.from('hello world');
|
|
18
|
+
const sig = hmacSign(data, TEST_SECRET);
|
|
19
|
+
expect(sig).toHaveLength(64); // SHA-256 hex = 64 chars
|
|
20
|
+
expect(hmacVerify(data, sig, TEST_SECRET)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects tampered data', () => {
|
|
24
|
+
const data = Buffer.from('hello world');
|
|
25
|
+
const sig = hmacSign(data, TEST_SECRET);
|
|
26
|
+
const tampered = Buffer.from('hello World'); // capital W
|
|
27
|
+
expect(hmacVerify(tampered, sig, TEST_SECRET)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects wrong secret', () => {
|
|
31
|
+
const data = Buffer.from('hello world');
|
|
32
|
+
const sig = hmacSign(data, TEST_SECRET);
|
|
33
|
+
const wrongSecret = 'b'.repeat(32);
|
|
34
|
+
expect(hmacVerify(data, sig, wrongSecret)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('throws on short secret', () => {
|
|
38
|
+
expect(() => hmacSign(Buffer.from('test'), 'short')).toThrow('at least 32');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects empty secret', () => {
|
|
42
|
+
expect(() => hmacSign(Buffer.from('test'), '')).toThrow('at least 32');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('signFrame / verifyFrame', () => {
|
|
47
|
+
it('signs and verifies frame data', () => {
|
|
48
|
+
const rgb = new Uint8Array([255, 0, 0, 0, 255, 0]);
|
|
49
|
+
const timestamp = 1712600000000;
|
|
50
|
+
const seq = 42;
|
|
51
|
+
const sig = signFrame(rgb, timestamp, seq, TEST_SECRET);
|
|
52
|
+
expect(verifyFrame(rgb, timestamp, seq, sig, TEST_SECRET)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('rejects altered pixel data', () => {
|
|
56
|
+
const rgb = new Uint8Array([255, 0, 0, 0, 255, 0]);
|
|
57
|
+
const sig = signFrame(rgb, 1000, 1, TEST_SECRET);
|
|
58
|
+
const altered = new Uint8Array([255, 0, 0, 0, 255, 1]); // 1 pixel changed
|
|
59
|
+
expect(verifyFrame(altered, 1000, 1, sig, TEST_SECRET)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects altered timestamp', () => {
|
|
63
|
+
const rgb = new Uint8Array([255, 0, 0]);
|
|
64
|
+
const sig = signFrame(rgb, 1000, 1, TEST_SECRET);
|
|
65
|
+
expect(verifyFrame(rgb, 1001, 1, sig, TEST_SECRET)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rejects altered sequence number', () => {
|
|
69
|
+
const rgb = new Uint8Array([255, 0, 0]);
|
|
70
|
+
const sig = signFrame(rgb, 1000, 1, TEST_SECRET);
|
|
71
|
+
expect(verifyFrame(rgb, 1000, 2, sig, TEST_SECRET)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('generateNonce', () => {
|
|
76
|
+
it('generates hex string of correct length', () => {
|
|
77
|
+
const nonce = generateNonce(16);
|
|
78
|
+
expect(nonce).toHaveLength(32); // 16 bytes = 32 hex chars
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('generates unique values', () => {
|
|
82
|
+
const a = generateNonce();
|
|
83
|
+
const b = generateNonce();
|
|
84
|
+
expect(a).not.toBe(b);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('sha256', () => {
|
|
89
|
+
it('hashes strings correctly', () => {
|
|
90
|
+
const hash = sha256('hello');
|
|
91
|
+
expect(hash).toHaveLength(64);
|
|
92
|
+
expect(hash).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('hashes buffers correctly', () => {
|
|
96
|
+
const hash = sha256(Buffer.from('hello'));
|
|
97
|
+
expect(hash).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('deriveKey', () => {
|
|
102
|
+
it('derives different keys for different contexts', () => {
|
|
103
|
+
const key1 = deriveKey(TEST_SECRET, 'frame-signing');
|
|
104
|
+
const key2 = deriveKey(TEST_SECRET, 'memory-signing');
|
|
105
|
+
expect(key1).not.toBe(key2);
|
|
106
|
+
expect(key1).toHaveLength(64);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('derives same key for same inputs', () => {
|
|
110
|
+
const key1 = deriveKey(TEST_SECRET, 'test');
|
|
111
|
+
const key2 = deriveKey(TEST_SECRET, 'test');
|
|
112
|
+
expect(key1).toBe(key2);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
vec3Add, vec3Sub, vec3Scale, vec3Dot, vec3Length, vec3Normalize,
|
|
4
|
+
vec3Distance, vec3Cross, vec3Lerp,
|
|
5
|
+
quatMultiply, quatConjugate, quatNormalize, quatRotateVec3,
|
|
6
|
+
quatFromAxisAngle, quatSlerp, QUAT_IDENTITY,
|
|
7
|
+
poseToMat4, egoToAllo, alloToEgo, stereoDepth,
|
|
8
|
+
gaussian, gaussian3D, clamp, meanAbsoluteError,
|
|
9
|
+
} from '../../src/utils/math.js';
|
|
10
|
+
|
|
11
|
+
describe('Vector Operations', () => {
|
|
12
|
+
it('adds vectors', () => {
|
|
13
|
+
const result = vec3Add({ x: 1, y: 2, z: 3 }, { x: 4, y: 5, z: 6 });
|
|
14
|
+
expect(result).toEqual({ x: 5, y: 7, z: 9 });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('subtracts vectors', () => {
|
|
18
|
+
const result = vec3Sub({ x: 5, y: 7, z: 9 }, { x: 4, y: 5, z: 6 });
|
|
19
|
+
expect(result).toEqual({ x: 1, y: 2, z: 3 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('scales vectors', () => {
|
|
23
|
+
expect(vec3Scale({ x: 1, y: 2, z: 3 }, 2)).toEqual({ x: 2, y: 4, z: 6 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('computes dot product', () => {
|
|
27
|
+
expect(vec3Dot({ x: 1, y: 0, z: 0 }, { x: 0, y: 1, z: 0 })).toBe(0);
|
|
28
|
+
expect(vec3Dot({ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 3 })).toBe(14);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('computes length', () => {
|
|
32
|
+
expect(vec3Length({ x: 3, y: 4, z: 0 })).toBe(5);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('normalizes vectors', () => {
|
|
36
|
+
const n = vec3Normalize({ x: 3, y: 4, z: 0 });
|
|
37
|
+
expect(n.x).toBeCloseTo(0.6);
|
|
38
|
+
expect(n.y).toBeCloseTo(0.8);
|
|
39
|
+
expect(vec3Length(n)).toBeCloseTo(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('handles zero vector normalization', () => {
|
|
43
|
+
const n = vec3Normalize({ x: 0, y: 0, z: 0 });
|
|
44
|
+
expect(n).toEqual({ x: 0, y: 0, z: 0 });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('computes distance', () => {
|
|
48
|
+
expect(vec3Distance({ x: 0, y: 0, z: 0 }, { x: 3, y: 4, z: 0 })).toBe(5);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('computes cross product', () => {
|
|
52
|
+
const result = vec3Cross({ x: 1, y: 0, z: 0 }, { x: 0, y: 1, z: 0 });
|
|
53
|
+
expect(result).toEqual({ x: 0, y: 0, z: 1 });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('lerps between vectors', () => {
|
|
57
|
+
const a = { x: 0, y: 0, z: 0 };
|
|
58
|
+
const b = { x: 10, y: 10, z: 10 };
|
|
59
|
+
const mid = vec3Lerp(a, b, 0.5);
|
|
60
|
+
expect(mid).toEqual({ x: 5, y: 5, z: 5 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('clamps lerp parameter', () => {
|
|
64
|
+
const a = { x: 0, y: 0, z: 0 };
|
|
65
|
+
const b = { x: 10, y: 10, z: 10 };
|
|
66
|
+
expect(vec3Lerp(a, b, -1)).toEqual(a);
|
|
67
|
+
expect(vec3Lerp(a, b, 2)).toEqual(b);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Quaternion Operations', () => {
|
|
72
|
+
it('identity quaternion preserves rotation', () => {
|
|
73
|
+
const v = { x: 1, y: 2, z: 3 };
|
|
74
|
+
const rotated = quatRotateVec3(QUAT_IDENTITY, v);
|
|
75
|
+
expect(rotated.x).toBeCloseTo(1);
|
|
76
|
+
expect(rotated.y).toBeCloseTo(2);
|
|
77
|
+
expect(rotated.z).toBeCloseTo(3);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rotates 90 degrees around Z axis', () => {
|
|
81
|
+
const q = quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI / 2);
|
|
82
|
+
const v = { x: 1, y: 0, z: 0 };
|
|
83
|
+
const rotated = quatRotateVec3(q, v);
|
|
84
|
+
expect(rotated.x).toBeCloseTo(0);
|
|
85
|
+
expect(rotated.y).toBeCloseTo(1);
|
|
86
|
+
expect(rotated.z).toBeCloseTo(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('conjugate reverses rotation', () => {
|
|
90
|
+
const q = quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI / 4);
|
|
91
|
+
const v = { x: 1, y: 0, z: 0 };
|
|
92
|
+
const rotated = quatRotateVec3(q, v);
|
|
93
|
+
const unrotated = quatRotateVec3(quatConjugate(q), rotated);
|
|
94
|
+
expect(unrotated.x).toBeCloseTo(1);
|
|
95
|
+
expect(unrotated.y).toBeCloseTo(0);
|
|
96
|
+
expect(unrotated.z).toBeCloseTo(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('slerp interpolates between quaternions', () => {
|
|
100
|
+
const a = QUAT_IDENTITY;
|
|
101
|
+
const b = quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI);
|
|
102
|
+
const mid = quatSlerp(a, b, 0.5);
|
|
103
|
+
// At t=0.5, should be 90 degrees rotation
|
|
104
|
+
const v = quatRotateVec3(mid, { x: 1, y: 0, z: 0 });
|
|
105
|
+
expect(v.x).toBeCloseTo(0, 1);
|
|
106
|
+
expect(Math.abs(v.y)).toBeCloseTo(1, 1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Coordinate Transforms', () => {
|
|
111
|
+
it('ego to allo with identity pose is identity', () => {
|
|
112
|
+
const pose = {
|
|
113
|
+
position: { x: 0, y: 0, z: 0 },
|
|
114
|
+
orientation: QUAT_IDENTITY,
|
|
115
|
+
timestamp: 0,
|
|
116
|
+
};
|
|
117
|
+
const result = egoToAllo({ x: 1, y: 2, z: 3 }, pose);
|
|
118
|
+
expect(result.x).toBeCloseTo(1);
|
|
119
|
+
expect(result.y).toBeCloseTo(2);
|
|
120
|
+
expect(result.z).toBeCloseTo(3);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('ego to allo applies translation', () => {
|
|
124
|
+
const pose = {
|
|
125
|
+
position: { x: 10, y: 20, z: 30 },
|
|
126
|
+
orientation: QUAT_IDENTITY,
|
|
127
|
+
timestamp: 0,
|
|
128
|
+
};
|
|
129
|
+
const result = egoToAllo({ x: 1, y: 2, z: 3 }, pose);
|
|
130
|
+
expect(result.x).toBeCloseTo(11);
|
|
131
|
+
expect(result.y).toBeCloseTo(22);
|
|
132
|
+
expect(result.z).toBeCloseTo(33);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('allo to ego reverses ego to allo', () => {
|
|
136
|
+
const pose = {
|
|
137
|
+
position: { x: 5, y: 10, z: 15 },
|
|
138
|
+
orientation: quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI / 4),
|
|
139
|
+
timestamp: 0,
|
|
140
|
+
};
|
|
141
|
+
const worldPoint = { x: 7, y: 12, z: 18 };
|
|
142
|
+
const ego = alloToEgo(worldPoint, pose);
|
|
143
|
+
const back = egoToAllo(ego, pose);
|
|
144
|
+
expect(back.x).toBeCloseTo(worldPoint.x);
|
|
145
|
+
expect(back.y).toBeCloseTo(worldPoint.y);
|
|
146
|
+
expect(back.z).toBeCloseTo(worldPoint.z);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('computes stereo depth correctly', () => {
|
|
150
|
+
// Z = f*B/d
|
|
151
|
+
expect(stereoDepth(500, 0.1, 10)).toBeCloseTo(5); // 500*0.1/10 = 5m
|
|
152
|
+
expect(stereoDepth(500, 0.1, 0)).toBe(Infinity); // zero disparity = infinity
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('Statistical Functions', () => {
|
|
157
|
+
it('gaussian peaks at center', () => {
|
|
158
|
+
expect(gaussian(0, 0, 1)).toBeCloseTo(1);
|
|
159
|
+
expect(gaussian(0, 0, 1, 2)).toBeCloseTo(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('gaussian falls off with distance', () => {
|
|
163
|
+
const atCenter = gaussian(0, 0, 1);
|
|
164
|
+
const at1Sigma = gaussian(1, 0, 1);
|
|
165
|
+
const at2Sigma = gaussian(2, 0, 1);
|
|
166
|
+
expect(atCenter).toBeGreaterThan(at1Sigma);
|
|
167
|
+
expect(at1Sigma).toBeGreaterThan(at2Sigma);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('3D gaussian works in 3D', () => {
|
|
171
|
+
const center = { x: 0, y: 0, z: 0 };
|
|
172
|
+
expect(gaussian3D(center, center, 1)).toBeCloseTo(1);
|
|
173
|
+
expect(gaussian3D({ x: 1, y: 0, z: 0 }, center, 1)).toBeLessThan(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('clamp works', () => {
|
|
177
|
+
expect(clamp(5, 0, 10)).toBe(5);
|
|
178
|
+
expect(clamp(-5, 0, 10)).toBe(0);
|
|
179
|
+
expect(clamp(15, 0, 10)).toBe(10);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('meanAbsoluteError computes correctly', () => {
|
|
183
|
+
const a = new Float32Array([1, 2, 3]);
|
|
184
|
+
const b = new Float32Array([1, 2, 3]);
|
|
185
|
+
expect(meanAbsoluteError(a, b)).toBe(0);
|
|
186
|
+
|
|
187
|
+
const c = new Float32Array([1, 2, 3]);
|
|
188
|
+
const d = new Float32Array([2, 3, 4]);
|
|
189
|
+
expect(meanAbsoluteError(c, d)).toBe(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('meanAbsoluteError throws on mismatch', () => {
|
|
193
|
+
expect(() => meanAbsoluteError(new Float32Array(3), new Float32Array(4))).toThrow('mismatch');
|
|
194
|
+
});
|
|
195
|
+
});
|