Compare commits
1 Commits
fix/1623
...
fix/1430-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
761ad313c7 |
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pre-commit hook: enforce 10-line net addition limit
|
||||
# Pre-commit hook: enforce 10-line net addition limit + sanitize commit message
|
||||
# Install: git config core.hooksPath .githooks
|
||||
|
||||
# ── Line limit enforcement ──────────────────────────────────────
|
||||
ADDITIONS=$(git diff --cached --numstat | awk '{s+=$1} END {print s+0}')
|
||||
DELETIONS=$(git diff --cached --numstat | awk '{s+=$2} END {print s+0}')
|
||||
NET=$((ADDITIONS - DELETIONS))
|
||||
@@ -12,4 +13,19 @@ if [ "$NET" -gt 10 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Commit message sanitization (#1430) ─────────────────────────
|
||||
# Read the commit message from .git/COMMIT_EDITMSG
|
||||
MSG_FILE="$(git rev-parse --git-dir)/COMMIT_EDITMSG"
|
||||
if [ -f "$MSG_FILE" ]; then
|
||||
MSG=$(cat "$MSG_FILE")
|
||||
# Warn if message contains backticks (potential shell injection)
|
||||
if echo "$MSG" | grep -q '`'; then
|
||||
echo "⚠ WARNING: Commit message contains backticks."
|
||||
echo " Backticks in 'git commit -m' can trigger shell expansion."
|
||||
echo " Consider using: git commit -F <file> or ./scripts/safe-commit.sh"
|
||||
echo " See: docs/SAFE_COMMIT_PATTERNS.md"
|
||||
# Don't block — just warn. Blocking would be too aggressive.
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✓ Pre-commit: net $NET lines (limit: 10)"
|
||||
|
||||
69
docs/SAFE_COMMIT_PATTERNS.md
Normal file
69
docs/SAFE_COMMIT_PATTERNS.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Safe Commit Patterns
|
||||
|
||||
## Issue #1430
|
||||
|
||||
Backticks in `git commit -m` messages can trigger shell substitution
|
||||
during hook processing. A commit message containing:
|
||||
|
||||
```
|
||||
git commit -m "fix: update `connectMemPalace()` to use Fleet API"
|
||||
```
|
||||
|
||||
may cause the shell to attempt executing `connectMemPalace()`.
|
||||
|
||||
## Safe Patterns
|
||||
|
||||
### 1. Use safe-commit.sh (recommended)
|
||||
|
||||
```bash
|
||||
./scripts/safe-commit.sh "fix: update connectMemPalace() to use Fleet API"
|
||||
```
|
||||
|
||||
This writes the message to a temp file and uses `git commit -F`,
|
||||
which prevents shell expansion.
|
||||
|
||||
### 2. Use git commit -F directly
|
||||
|
||||
```bash
|
||||
echo "fix: update \`connectMemPalace()\` to use Fleet API" > /tmp/msg.txt
|
||||
git commit -F /tmp/msg.txt
|
||||
```
|
||||
|
||||
### 3. Use single quotes (less reliable with hooks)
|
||||
|
||||
```bash
|
||||
git commit -m 'fix: update `connectMemPalace()` to use Fleet API'
|
||||
```
|
||||
|
||||
Single quotes prevent shell expansion in the commit command itself,
|
||||
but hooks that read the message may still process backticks.
|
||||
|
||||
### 4. Use heredoc for multiline
|
||||
|
||||
```bash
|
||||
git commit -F - <<'EOF'
|
||||
fix: update `connectMemPalace()` to use Fleet API
|
||||
|
||||
The mock MCP server was overwriting the real Fleet API version.
|
||||
EOF
|
||||
```
|
||||
|
||||
## What NOT to do
|
||||
|
||||
```bash
|
||||
# BAD — backticks trigger shell expansion
|
||||
git commit -m "fix: update `connectMemPalace()` to use Fleet API"
|
||||
|
||||
# BAD — $(...) triggers command substitution
|
||||
git commit -m "fix: update $(cat file.py) to use Fleet API"
|
||||
|
||||
# BAD — ! triggers history expansion
|
||||
git commit -m "fix: this is not a joke! seriously"
|
||||
```
|
||||
|
||||
## For agents
|
||||
|
||||
When committing code that contains backticks or special characters:
|
||||
1. Always use `git commit -F <file>` or `safe-commit.sh`
|
||||
2. Never interpolate user content into `-m` strings
|
||||
3. Escape or remove backticks from commit messages when possible
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
41
scripts/safe-commit.sh
Executable file
41
scripts/safe-commit.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# safe-commit.sh — Commit with message from file (prevents shell injection)
|
||||
#
|
||||
# Issue #1430: Backticks in commit messages can trigger shell
|
||||
# substitution during git hook processing. Using -F <file> instead
|
||||
# of -m prevents this.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/safe-commit.sh "my commit message"
|
||||
# ./scripts/safe-commit.sh -F message.txt
|
||||
# echo "message" | ./scripts/safe-commit.sh --stdin
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
set -euo pipefail
|
||||
|
||||
TMPFILE=$(mktemp /tmp/commit-msg-XXXXXX)
|
||||
trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
if [ "${1:-}" = "-F" ] && [ -n "${2:-}" ]; then
|
||||
# Use provided file
|
||||
cp "$2" "$TMPFILE"
|
||||
elif [ "${1:-}" = "--stdin" ]; then
|
||||
# Read from stdin
|
||||
cat > "$TMPFILE"
|
||||
elif [ -n "${1:-}" ]; then
|
||||
# Write argument to temp file (no shell expansion)
|
||||
printf '%s' "$1" > "$TMPFILE"
|
||||
else
|
||||
echo "Usage: $0 <message> | $0 -F <file> | $0 --stdin"
|
||||
echo ""
|
||||
echo "Always uses git commit -F to prevent shell injection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stage all changes
|
||||
git add -A
|
||||
|
||||
# Commit using file (no shell expansion of message content)
|
||||
git commit -F "$TMPFILE"
|
||||
|
||||
echo "✓ Committed safely (no shell expansion)"
|
||||
@@ -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