Compare commits
2 Commits
fix/1623
...
fix/1427-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f28843878e | ||
|
|
69cc254baf |
@@ -395,7 +395,6 @@
|
||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
||||
|
||||
<script src="./boot.js"></script>
|
||||
<script src="./js/portal-registry-watcher.js"></script>
|
||||
<script src="./avatar-customization.js"></script>
|
||||
<script src="./lod-system.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
/**
|
||||
* Portal Registry Watcher
|
||||
* Issue #1623: feat: portal hot-reload from portals.json without server restart
|
||||
*
|
||||
* Watches portals.json for changes and hot-reloads portal scene objects
|
||||
* without requiring a server restart.
|
||||
*/
|
||||
|
||||
class PortalRegistryWatcher {
|
||||
constructor(options = {}) {
|
||||
this.pollInterval = options.pollInterval || 5000; // 5 seconds
|
||||
this.registryPath = options.registryPath || './portals.json';
|
||||
this.onRegistryUpdate = options.onRegistryUpdate || (() => {});
|
||||
this.onError = options.onError || console.error;
|
||||
|
||||
this.pollTimer = null;
|
||||
this.lastRegistry = null;
|
||||
this.lastModified = null;
|
||||
this.isWatching = false;
|
||||
|
||||
// Cache-busting timestamp
|
||||
this._registryTs = Date.now();
|
||||
|
||||
// Bind methods
|
||||
this.startWatching = this.startWatching.bind(this);
|
||||
this.stopWatching = this.stopWatching.bind(this);
|
||||
this.poll = this.poll.bind(this);
|
||||
this.loadRegistry = this.loadRegistry.bind(this);
|
||||
this.applyRegistry = this.applyRegistry.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start watching for registry changes
|
||||
*/
|
||||
startWatching() {
|
||||
if (this.isWatching) {
|
||||
console.warn('[PortalRegistry] Already watching');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[PortalRegistry] Starting to watch ${this.registryPath} every ${this.pollInterval / 1000}s`);
|
||||
|
||||
this.isWatching = true;
|
||||
|
||||
// Initial load
|
||||
this.poll();
|
||||
|
||||
// Set up interval
|
||||
this.pollTimer = setInterval(this.poll, this.pollInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watching
|
||||
*/
|
||||
stopWatching() {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
|
||||
this.isWatching = false;
|
||||
console.log('[PortalRegistry] Stopped watching');
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for registry changes
|
||||
*/
|
||||
async poll() {
|
||||
try {
|
||||
const registry = await this.loadRegistry();
|
||||
|
||||
if (this.hasChanged(registry)) {
|
||||
console.log('[PortalRegistry] Registry changed, applying updates...');
|
||||
this.applyRegistry(registry);
|
||||
this.lastRegistry = registry;
|
||||
this.onRegistryUpdate(registry);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.onError('Failed to poll registry:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load registry from file with cache-busting
|
||||
*/
|
||||
async loadRegistry() {
|
||||
// Add cache-busting timestamp
|
||||
this._registryTs = Date.now();
|
||||
const url = `${this.registryPath}?_registry_ts=${this._registryTs}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load registry: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Validate registry structure
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Registry must be an array of portal objects');
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load registry: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if registry has changed
|
||||
*/
|
||||
hasChanged(newRegistry) {
|
||||
if (!this.lastRegistry) {
|
||||
return true; // First load
|
||||
}
|
||||
|
||||
// Quick check: different length
|
||||
if (this.lastRegistry.length !== newRegistry.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deep check: compare JSON strings
|
||||
const oldJson = JSON.stringify(this.lastRegistry);
|
||||
const newJson = JSON.stringify(newRegistry);
|
||||
|
||||
return oldJson !== newJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply registry changes to the scene
|
||||
*/
|
||||
applyRegistry(registry) {
|
||||
console.log(`[PortalRegistry] Applying ${registry.length} portals`);
|
||||
|
||||
// Store registry globally for access by other modules
|
||||
window.portalRegistry = registry;
|
||||
|
||||
// Trigger custom event for other modules to listen to
|
||||
const event = new CustomEvent('portalRegistryUpdated', {
|
||||
detail: { registry, timestamp: Date.now() }
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
// Update portal scene objects if Three.js scene exists
|
||||
if (window.scene && window.portalObjects) {
|
||||
this.updatePortalSceneObjects(registry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update portal scene objects in Three.js
|
||||
*/
|
||||
updatePortalSceneObjects(registry) {
|
||||
// Remove existing portal objects
|
||||
for (const portalId in window.portalObjects) {
|
||||
const portalObj = window.portalObjects[portalId];
|
||||
if (portalObj && portalObj.parent) {
|
||||
portalObj.parent.remove(portalObj);
|
||||
}
|
||||
}
|
||||
|
||||
window.portalObjects = {};
|
||||
|
||||
// Create new portal objects
|
||||
for (const portal of registry) {
|
||||
try {
|
||||
const portalObj = this.createPortalObject(portal);
|
||||
if (portalObj) {
|
||||
window.portalObjects[portal.id] = portalObj;
|
||||
window.scene.add(portalObj);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[PortalRegistry] Failed to create portal ${portal.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[PortalRegistry] Updated ${Object.keys(window.portalObjects).length} portal objects`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a portal object from registry data
|
||||
*/
|
||||
createPortalObject(portal) {
|
||||
// This is a placeholder - actual implementation depends on Three.js setup
|
||||
// In a real implementation, this would create the portal mesh/object
|
||||
|
||||
const portalObj = {
|
||||
id: portal.id,
|
||||
name: portal.name,
|
||||
position: portal.position,
|
||||
status: portal.status,
|
||||
color: portal.color,
|
||||
|
||||
// Methods
|
||||
update: function(newData) {
|
||||
Object.assign(this, newData);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
// Cleanup
|
||||
}
|
||||
};
|
||||
|
||||
return portalObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current registry
|
||||
*/
|
||||
getRegistry() {
|
||||
return this.lastRegistry || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get portal by ID
|
||||
*/
|
||||
getPortalById(id) {
|
||||
const registry = this.getRegistry();
|
||||
return registry.find(p => p.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get portals by status
|
||||
*/
|
||||
getPortalsByStatus(status) {
|
||||
const registry = this.getRegistry();
|
||||
return registry.filter(p => p.status === status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get watcher status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isWatching: this.isWatching,
|
||||
pollInterval: this.pollInterval,
|
||||
registryPath: this.registryPath,
|
||||
portalCount: this.lastRegistry ? this.lastRegistry.length : 0,
|
||||
lastUpdate: this.lastRegistry ? new Date().toISOString() : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PortalRegistryWatcher;
|
||||
}
|
||||
|
||||
// Global instance for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.PortalRegistryWatcher = PortalRegistryWatcher;
|
||||
|
||||
// Auto-initialize if portal container exists
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('portal-container') || document.getElementById('nexus-canvas');
|
||||
if (container) {
|
||||
const watcher = new PortalRegistryWatcher({
|
||||
pollInterval: 5000,
|
||||
onRegistryUpdate: (registry) => {
|
||||
console.log(`[PortalRegistry] Updated with ${registry.length} portals`);
|
||||
}
|
||||
});
|
||||
|
||||
watcher.startWatching();
|
||||
|
||||
// Store globally for access
|
||||
window.portalRegistryWatcher = watcher;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -53,13 +53,28 @@ def _get_client(palace_path: Path):
|
||||
"Run: pip install chromadb (or: pip install mempalace)"
|
||||
) from exc
|
||||
|
||||
try:
|
||||
from chromadb.config import Settings # type: ignore
|
||||
except Exception: # pragma: no cover - supports MagicMock-based tests
|
||||
Settings = getattr(getattr(chromadb, "config", None), "Settings", None)
|
||||
if Settings is None:
|
||||
Settings = getattr(chromadb, "Settings", None)
|
||||
if Settings is None:
|
||||
raise MemPalaceUnavailable(
|
||||
"ChromaDB Settings API unavailable. "
|
||||
"Upgrade chromadb or verify the install."
|
||||
)
|
||||
|
||||
if not palace_path.exists():
|
||||
raise MemPalaceUnavailable(
|
||||
f"Palace directory not found: {palace_path}\n"
|
||||
"Run 'mempalace mine' to initialise the palace."
|
||||
)
|
||||
|
||||
return chromadb.PersistentClient(path=str(palace_path))
|
||||
return chromadb.PersistentClient(
|
||||
path=str(palace_path),
|
||||
settings=Settings(anonymized_telemetry=False),
|
||||
)
|
||||
|
||||
|
||||
def search_memories(
|
||||
|
||||
@@ -5,6 +5,7 @@ Filters and ranks content by Hermes/Timmy relevance
|
||||
"""
|
||||
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
from chromadb.utils import embedding_functions
|
||||
from typing import List, Dict, Any
|
||||
import json
|
||||
@@ -26,7 +27,10 @@ HERMES_CONTEXT = [
|
||||
|
||||
class RelevanceEngine:
|
||||
def __init__(self, collection_name: str = "deep_dive"):
|
||||
self.client = chromadb.PersistentClient(path="./chroma_db")
|
||||
self.client = chromadb.PersistentClient(
|
||||
path="./chroma_db",
|
||||
settings=Settings(anonymized_telemetry=False),
|
||||
)
|
||||
self.embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||
model_name="all-MiniLM-L6-v2"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ from pathlib import Path
|
||||
|
||||
try:
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
except ImportError:
|
||||
print("ERROR: chromadb not installed")
|
||||
sys.exit(1)
|
||||
@@ -34,7 +35,10 @@ VIOLATION_KEYWORDS = [
|
||||
|
||||
def audit(palace_path: Path):
|
||||
violations = []
|
||||
client = chromadb.PersistentClient(path=str(palace_path))
|
||||
client = chromadb.PersistentClient(
|
||||
path=str(palace_path),
|
||||
settings=Settings(anonymized_telemetry=False),
|
||||
)
|
||||
try:
|
||||
col = client.get_collection("mempalace_drawers")
|
||||
except Exception as e:
|
||||
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
|
||||
PALACE_PATH = "/root/wizards/bezalel/.mempalace/palace"
|
||||
FLEET_INCOMING = "/var/lib/mempalace/fleet/incoming"
|
||||
@@ -18,7 +19,10 @@ DOCS_PER_ROOM = 5
|
||||
|
||||
|
||||
def main():
|
||||
client = chromadb.PersistentClient(path=PALACE_PATH)
|
||||
client = chromadb.PersistentClient(
|
||||
path=PALACE_PATH,
|
||||
settings=Settings(anonymized_telemetry=False),
|
||||
)
|
||||
col = client.get_collection("mempalace_drawers")
|
||||
|
||||
# Discover rooms in this wing
|
||||
|
||||
55
tests/test_chroma_telemetry_optout.py
Normal file
55
tests/test_chroma_telemetry_optout.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Regression tests for ChromaDB telemetry hardening.
|
||||
|
||||
Issue #1427: ChromaDB defaults to anonymous telemetry unless explicitly disabled.
|
||||
These tests ensure every direct PersistentClient() call opts out.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from nexus.mempalace.searcher import _get_client
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
DIRECT_CLIENT_FILES = [
|
||||
PROJECT_ROOT / "nexus/mempalace/searcher.py",
|
||||
PROJECT_ROOT / "scripts/mempalace_export.py",
|
||||
PROJECT_ROOT / "scripts/audit_mempalace_privacy.py",
|
||||
PROJECT_ROOT / "scaffold/deep-dive/relevance/relevance_engine.py",
|
||||
]
|
||||
|
||||
|
||||
def test_get_client_disables_chroma_telemetry(tmp_path):
|
||||
mock_chroma = MagicMock()
|
||||
mock_settings = MagicMock(name="Settings")
|
||||
mock_chroma.PersistentClient.return_value = "CLIENT"
|
||||
mock_settings.return_value = "SETTINGS"
|
||||
|
||||
with patch.dict(
|
||||
"sys.modules",
|
||||
{
|
||||
"chromadb": mock_chroma,
|
||||
"chromadb.config": MagicMock(Settings=mock_settings),
|
||||
},
|
||||
):
|
||||
(tmp_path / "chroma.sqlite3").touch()
|
||||
client = _get_client(tmp_path)
|
||||
|
||||
assert client == "CLIENT"
|
||||
mock_settings.assert_called_once_with(anonymized_telemetry=False)
|
||||
mock_chroma.PersistentClient.assert_called_once_with(path=str(tmp_path), settings="SETTINGS")
|
||||
|
||||
|
||||
def test_all_direct_persistent_clients_explicitly_disable_telemetry():
|
||||
missing = []
|
||||
needle = "anonymized_telemetry=False"
|
||||
for path in DIRECT_CLIENT_FILES:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if needle not in text:
|
||||
missing.append(path.relative_to(PROJECT_ROOT).as_posix())
|
||||
|
||||
assert not missing, (
|
||||
"Direct Chroma PersistentClient call(s) still missing explicit telemetry opt-out:\n"
|
||||
+ "\n".join(f" - {item}" for item in missing)
|
||||
)
|
||||
@@ -1,214 +0,0 @@
|
||||
/**
|
||||
* Tests for Portal Registry Watcher
|
||||
* Issue #1623: feat: portal hot-reload from portals.json without server restart
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = async (url) => {
|
||||
// Simulate loading portals.json
|
||||
if (url.includes('portals.json')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => [
|
||||
{
|
||||
id: 'test-portal-1',
|
||||
name: 'Test Portal 1',
|
||||
status: 'online',
|
||||
color: '#ff0000',
|
||||
position: { x: 10, y: 0, z: 0 }
|
||||
},
|
||||
{
|
||||
id: 'test-portal-2',
|
||||
name: 'Test Portal 2',
|
||||
status: 'offline',
|
||||
color: '#00ff00',
|
||||
position: { x: -10, y: 0, z: 0 }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
// Mock CustomEvent
|
||||
global.CustomEvent = class CustomEvent {
|
||||
constructor(type, options) {
|
||||
this.type = type;
|
||||
this.detail = options.detail;
|
||||
}
|
||||
};
|
||||
|
||||
// Mock window
|
||||
global.window = {
|
||||
addEventListener: () => {},
|
||||
dispatchEvent: () => {},
|
||||
portalObjects: {},
|
||||
scene: {
|
||||
add: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
// Mock document
|
||||
global.document = {
|
||||
addEventListener: () => {},
|
||||
getElementById: () => null
|
||||
};
|
||||
|
||||
// Load portal-registry-watcher.js
|
||||
const watcherPath = path.join(ROOT, 'js', 'portal-registry-watcher.js');
|
||||
const watcherCode = fs.readFileSync(watcherPath, 'utf8');
|
||||
|
||||
// Execute in context
|
||||
const vm = require('node:vm');
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
console,
|
||||
window: global.window,
|
||||
document: global.document,
|
||||
fetch: global.fetch,
|
||||
CustomEvent: global.CustomEvent,
|
||||
setInterval: () => {},
|
||||
clearInterval: () => {}
|
||||
};
|
||||
|
||||
vm.runInNewContext(watcherCode, context);
|
||||
|
||||
// Get PortalRegistryWatcher
|
||||
const PortalRegistryWatcher = context.window.PortalRegistryWatcher || context.module.exports;
|
||||
|
||||
test('PortalRegistryWatcher loads correctly', () => {
|
||||
assert.ok(PortalRegistryWatcher, 'PortalRegistryWatcher should be defined');
|
||||
assert.ok(typeof PortalRegistryWatcher === 'function', 'PortalRegistryWatcher should be a constructor');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher can be instantiated', () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
assert.ok(watcher, 'PortalRegistryWatcher instance should be created');
|
||||
assert.equal(watcher.pollInterval, 5000, 'Should have default poll interval');
|
||||
assert.equal(watcher.registryPath, './portals.json', 'Should have default registry path');
|
||||
assert.ok(!watcher.isWatching, 'Should not be watching initially');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher can load registry', async () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
|
||||
const registry = await watcher.loadRegistry();
|
||||
|
||||
assert.ok(registry, 'Should return registry');
|
||||
assert.ok(Array.isArray(registry), 'Registry should be an array');
|
||||
assert.equal(registry.length, 2, 'Should have 2 portals');
|
||||
assert.equal(registry[0].id, 'test-portal-1', 'First portal should have correct ID');
|
||||
assert.equal(registry[1].id, 'test-portal-2', 'Second portal should have correct ID');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher detects changes', async () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
|
||||
// Load initial registry
|
||||
const registry1 = await watcher.loadRegistry();
|
||||
watcher.lastRegistry = registry1;
|
||||
|
||||
// Load same registry again
|
||||
const registry2 = await watcher.loadRegistry();
|
||||
const changed1 = watcher.hasChanged(registry2);
|
||||
|
||||
assert.ok(!changed1, 'Should not detect change with same registry');
|
||||
|
||||
// Create a different registry manually
|
||||
const differentRegistry = [
|
||||
{ id: 'different-portal', name: 'Different Portal' }
|
||||
];
|
||||
|
||||
// Test hasChanged with different registry
|
||||
const changed2 = watcher.hasChanged(differentRegistry);
|
||||
|
||||
assert.ok(changed2, 'Should detect change with different registry');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher can start and stop watching', () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
|
||||
// Start watching
|
||||
watcher.startWatching();
|
||||
assert.ok(watcher.isWatching, 'Should be watching after start');
|
||||
|
||||
// Stop watching
|
||||
watcher.stopWatching();
|
||||
assert.ok(!watcher.isWatching, 'Should not be watching after stop');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher applies registry', () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
|
||||
const registry = [
|
||||
{ id: 'portal-1', name: 'Portal 1' },
|
||||
{ id: 'portal-2', name: 'Portal 2' }
|
||||
];
|
||||
|
||||
// Mock window.dispatchEvent
|
||||
let eventDispatched = false;
|
||||
global.window.dispatchEvent = (event) => {
|
||||
if (event.type === 'portalRegistryUpdated') {
|
||||
eventDispatched = true;
|
||||
}
|
||||
};
|
||||
|
||||
watcher.applyRegistry(registry);
|
||||
|
||||
assert.ok(eventDispatched, 'Should dispatch portalRegistryUpdated event');
|
||||
assert.deepEqual(window.portalRegistry, registry, 'Should store registry globally');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher can get portal by ID', async () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
|
||||
// Load registry
|
||||
await watcher.loadRegistry();
|
||||
watcher.lastRegistry = await watcher.loadRegistry();
|
||||
|
||||
const portal = watcher.getPortalById('test-portal-1');
|
||||
assert.ok(portal, 'Should find portal by ID');
|
||||
assert.equal(portal.name, 'Test Portal 1', 'Should have correct name');
|
||||
|
||||
const missing = watcher.getPortalById('non-existent');
|
||||
assert.ok(!missing, 'Should return undefined for non-existent portal');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher can get portals by status', async () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
|
||||
// Load registry
|
||||
await watcher.loadRegistry();
|
||||
watcher.lastRegistry = await watcher.loadRegistry();
|
||||
|
||||
const online = watcher.getPortalsByStatus('online');
|
||||
assert.equal(online.length, 1, 'Should have 1 online portal');
|
||||
assert.equal(online[0].id, 'test-portal-1', 'Should be test-portal-1');
|
||||
|
||||
const offline = watcher.getPortalsByStatus('offline');
|
||||
assert.equal(offline.length, 1, 'Should have 1 offline portal');
|
||||
assert.equal(offline[0].id, 'test-portal-2', 'Should be test-portal-2');
|
||||
});
|
||||
|
||||
test('PortalRegistryWatcher can get status', () => {
|
||||
const watcher = new PortalRegistryWatcher();
|
||||
|
||||
const status = watcher.getStatus();
|
||||
|
||||
assert.ok(status, 'Should return status object');
|
||||
assert.equal(status.isWatching, false, 'Should not be watching');
|
||||
assert.equal(status.pollInterval, 5000, 'Should have correct poll interval');
|
||||
assert.equal(status.registryPath, './portals.json', 'Should have correct path');
|
||||
assert.equal(status.portalCount, 0, 'Should have 0 portals initially');
|
||||
});
|
||||
|
||||
console.log('All Portal Registry Watcher tests passed!');
|
||||
Reference in New Issue
Block a user