Pi deployment mega-session: 136+ containers deployed
Massive deployment session deploying entire BlackRoad/Lucidia infrastructure to Raspberry Pi 4B: - Cleaned /tmp space: 595MB → 5.2GB free - Total containers: 136+ running simultaneously - Ports: 3067-3200+ - Disk: 25G/29G (92% usage) - Memory: 3.6Gi/7.9Gi Deployment scripts created: - /tmp/continue-deploy.sh (v2-* deployments) - /tmp/absolute-final-deploy.sh (final-* deployments) - /tmp/deployment-status.sh (monitoring) Infrastructure maximized on single Pi 4B (8GB RAM, 32GB SD). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
19
.claude/settings.local.json
Normal file
19
.claude/settings.local.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(wrangler pages deploy:*)",
|
||||||
|
"Bash(wrangler pages project create:*)",
|
||||||
|
"Bash(gh repo create:*)",
|
||||||
|
"Bash(wrangler pages project:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(wrangler whoami:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(bash:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
13
package-lock.json
generated
Normal file
13
package-lock.json
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "blackroad-metaverse",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "blackroad-metaverse",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "PROPRIETARY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
"name": "blackroad-metaverse",
|
"name": "blackroad-metaverse",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "BlackRoad 3D Metaverse - Login and explore the universe where AI agents live",
|
"description": "BlackRoad 3D Metaverse - Login and explore the universe where AI agents live",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "wrangler pages deploy . --project-name=blackroad-metaverse",
|
"deploy": "wrangler pages deploy . --project-name=blackroad-metaverse",
|
||||||
"dev": "python3 -m http.server 8000"
|
"dev": "python3 -m http.server 8000",
|
||||||
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"metaverse",
|
"metaverse",
|
||||||
|
|||||||
91
tests/dialogue-story.test.js
Normal file
91
tests/dialogue-story.test.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NODE_TYPES,
|
||||||
|
ALICE_DIALOGUE,
|
||||||
|
ARIA_DIALOGUE,
|
||||||
|
LUCIDIA_DIALOGUE,
|
||||||
|
STORY_EVENTS,
|
||||||
|
DialogueManager,
|
||||||
|
StoryManager
|
||||||
|
} from '../dialogue-story.js';
|
||||||
|
|
||||||
|
const NODE_TYPE_VALUES = new Set(Object.values(NODE_TYPES));
|
||||||
|
|
||||||
|
function validateDialogue(dialogue) {
|
||||||
|
const keys = new Set(Object.keys(dialogue));
|
||||||
|
assert.ok(keys.has('greeting_first'));
|
||||||
|
assert.ok(keys.has('end'));
|
||||||
|
assert.equal(dialogue.end.type, NODE_TYPES.END);
|
||||||
|
|
||||||
|
for (const [nodeId, node] of Object.entries(dialogue)) {
|
||||||
|
assert.ok(NODE_TYPE_VALUES.has(node.type), `Unknown type for ${nodeId}`);
|
||||||
|
|
||||||
|
if (node.type === NODE_TYPES.SAY) {
|
||||||
|
assert.equal(typeof node.text, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.next) {
|
||||||
|
assert.ok(node.next === 'end' || keys.has(node.next));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === NODE_TYPES.CHOICE) {
|
||||||
|
assert.ok(Array.isArray(node.choices));
|
||||||
|
assert.ok(node.choices.length > 0);
|
||||||
|
for (const choice of node.choices) {
|
||||||
|
assert.ok(choice.next === 'end' || keys.has(choice.next));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('dialogue graphs reference valid nodes and types', () => {
|
||||||
|
validateDialogue(ALICE_DIALOGUE);
|
||||||
|
validateDialogue(ARIA_DIALOGUE);
|
||||||
|
validateDialogue(LUCIDIA_DIALOGUE);
|
||||||
|
|
||||||
|
for (const [eventId, event] of Object.entries(STORY_EVENTS)) {
|
||||||
|
assert.equal(typeof event.trigger, 'string', `Missing trigger for ${eventId}`);
|
||||||
|
assert.ok(Array.isArray(event.effects));
|
||||||
|
assert.ok(event.effects.length > 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DialogueManager advances and records choices', () => {
|
||||||
|
const manager = new DialogueManager();
|
||||||
|
|
||||||
|
const first = manager.startConversation('alice');
|
||||||
|
assert.equal(first.type, NODE_TYPES.SAY);
|
||||||
|
|
||||||
|
const choiceNode = manager.advance();
|
||||||
|
assert.equal(choiceNode.type, NODE_TYPES.CHOICE);
|
||||||
|
|
||||||
|
const nextNode = manager.selectChoice(0);
|
||||||
|
assert.equal(nextNode.type, NODE_TYPES.SAY);
|
||||||
|
|
||||||
|
assert.equal(manager.getHistory().length, 1);
|
||||||
|
|
||||||
|
manager.endConversation();
|
||||||
|
assert.equal(manager.getRelationship('alice'), 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('StoryManager triggers events and builds narrative', () => {
|
||||||
|
const story = new StoryManager();
|
||||||
|
|
||||||
|
const result = story.triggerEvent('first_love', { source: 'test' });
|
||||||
|
assert.ok(result);
|
||||||
|
|
||||||
|
const stateAfterEvent = story.getNarrativeState();
|
||||||
|
assert.equal(stateAfterEvent.love, 10);
|
||||||
|
|
||||||
|
assert.equal(story.triggerEvent('first_love'), null);
|
||||||
|
|
||||||
|
story.recordChoice('test', { love: 150, creation: 5 });
|
||||||
|
const narrative = story.generateNarrative();
|
||||||
|
assert.notEqual(narrative, 'Your story is just beginning...');
|
||||||
|
assert.ok(narrative.includes('compassion'));
|
||||||
|
|
||||||
|
const archetype = story.getPlayerArchetype();
|
||||||
|
assert.equal(archetype.type, 'Nurturer');
|
||||||
|
});
|
||||||
7
tests/fixtures/test-module.js
vendored
Normal file
7
tests/fixtures/test-module.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export let initialized = false;
|
||||||
|
|
||||||
|
export async function init() {
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const name = 'fixture';
|
||||||
75
tests/module-loader.test.js
Normal file
75
tests/module-loader.test.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { ModuleLoader } from '../module-loader.js';
|
||||||
|
|
||||||
|
test('buildDependencyGraph maps dependencies and dependents', () => {
|
||||||
|
const loader = new ModuleLoader();
|
||||||
|
const configs = [
|
||||||
|
{ name: 'a', deps: [] },
|
||||||
|
{ name: 'b', deps: ['a'] },
|
||||||
|
{ name: 'c', deps: ['b'] },
|
||||||
|
{ name: 'wild', deps: ['*'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
const graph = loader.buildDependencyGraph(configs);
|
||||||
|
|
||||||
|
assert.deepEqual(graph.get('b').deps, ['a']);
|
||||||
|
assert.deepEqual(graph.get('a').dependents, ['b']);
|
||||||
|
assert.deepEqual(graph.get('wild').deps, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('topologicalSort orders dependencies before dependents', () => {
|
||||||
|
const loader = new ModuleLoader();
|
||||||
|
const configs = [
|
||||||
|
{ name: 'a', deps: [] },
|
||||||
|
{ name: 'b', deps: ['a'] },
|
||||||
|
{ name: 'c', deps: ['b'] }
|
||||||
|
];
|
||||||
|
const graph = loader.buildDependencyGraph(configs);
|
||||||
|
const order = loader.topologicalSort(graph);
|
||||||
|
|
||||||
|
assert.ok(order.includes('a'));
|
||||||
|
assert.ok(order.includes('b'));
|
||||||
|
assert.ok(order.includes('c'));
|
||||||
|
assert.ok(order.indexOf('a') < order.indexOf('b'));
|
||||||
|
assert.ok(order.indexOf('b') < order.indexOf('c'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('topologicalSort throws on cycles', () => {
|
||||||
|
const loader = new ModuleLoader();
|
||||||
|
const graph = new Map([
|
||||||
|
['a', { deps: ['b'], dependents: [] }],
|
||||||
|
['b', { deps: ['a'], dependents: [] }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.throws(() => loader.topologicalSort(graph), /Circular dependency/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadModule returns module and runs init', async () => {
|
||||||
|
const loader = new ModuleLoader();
|
||||||
|
const fixtureUrl = new URL('./fixtures/test-module.js', import.meta.url);
|
||||||
|
|
||||||
|
const loadedModule = await loader.loadModule({
|
||||||
|
name: 'fixture',
|
||||||
|
path: fixtureUrl.href,
|
||||||
|
deps: []
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(loadedModule.name, 'fixture');
|
||||||
|
assert.equal(loadedModule.initialized, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadModule returns placeholder for missing module', async () => {
|
||||||
|
const loader = new ModuleLoader();
|
||||||
|
const missingUrl = new URL('./fixtures/does-not-exist.js', import.meta.url);
|
||||||
|
|
||||||
|
const loadedModule = await loader.loadModule({
|
||||||
|
name: 'missing',
|
||||||
|
path: missingUrl.href,
|
||||||
|
deps: []
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(loadedModule.placeholder, true);
|
||||||
|
assert.equal(loadedModule.name, 'missing');
|
||||||
|
});
|
||||||
68
tests/quest-system.test.js
Normal file
68
tests/quest-system.test.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
QUESTS,
|
||||||
|
QUEST_TYPES,
|
||||||
|
ACHIEVEMENTS,
|
||||||
|
QuestManager
|
||||||
|
} from '../quest-system.js';
|
||||||
|
|
||||||
|
test('quest definitions are consistent', () => {
|
||||||
|
const questTypes = new Set(Object.values(QUEST_TYPES));
|
||||||
|
|
||||||
|
for (const [questId, quest] of Object.entries(QUESTS)) {
|
||||||
|
assert.equal(quest.id, questId);
|
||||||
|
assert.ok(questTypes.has(quest.type));
|
||||||
|
assert.ok(Array.isArray(quest.objectives));
|
||||||
|
assert.ok(quest.objectives.length > 0);
|
||||||
|
assert.equal(quest.completed, false);
|
||||||
|
assert.ok(quest.rewards && Object.keys(quest.rewards).length > 0);
|
||||||
|
|
||||||
|
quest.objectives.forEach(objective => {
|
||||||
|
assert.equal(typeof objective.target, 'number');
|
||||||
|
assert.equal(typeof objective.current, 'number');
|
||||||
|
assert.ok(objective.current <= objective.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('QuestManager completes quests and grants rewards', () => {
|
||||||
|
const manager = new QuestManager();
|
||||||
|
|
||||||
|
const quest = manager.startQuest('first_steps');
|
||||||
|
assert.ok(quest);
|
||||||
|
assert.equal(manager.activeQuests.length, 1);
|
||||||
|
|
||||||
|
manager.updateQuestProgress('first_steps', 'move', 1);
|
||||||
|
assert.ok(manager.completedQuests.includes('first_steps'));
|
||||||
|
assert.equal(manager.activeQuests.length, 0);
|
||||||
|
assert.equal(manager.experience, 10);
|
||||||
|
|
||||||
|
assert.equal(QUESTS.first_steps.objectives[0].current, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('QuestManager unlocks achievements and reports progress', () => {
|
||||||
|
const manager = new QuestManager();
|
||||||
|
|
||||||
|
assert.equal(manager.unlockAchievement('first_flight'), true);
|
||||||
|
assert.equal(manager.unlockAchievement('first_flight'), false);
|
||||||
|
|
||||||
|
const progress = manager.getAchievementProgress();
|
||||||
|
assert.equal(progress.unlocked, 1);
|
||||||
|
assert.equal(progress.total, Object.keys(ACHIEVEMENTS).length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('QuestManager save and load preserves state', () => {
|
||||||
|
const manager = new QuestManager();
|
||||||
|
manager.startQuest('first_steps');
|
||||||
|
manager.updateQuestProgress('first_steps', 'move', 1);
|
||||||
|
|
||||||
|
const saved = manager.save();
|
||||||
|
const restored = new QuestManager();
|
||||||
|
restored.load(saved);
|
||||||
|
|
||||||
|
assert.deepEqual(restored.completedQuests, manager.completedQuests);
|
||||||
|
assert.equal(restored.experience, manager.experience);
|
||||||
|
assert.equal(restored.level, manager.level);
|
||||||
|
});
|
||||||
91
tests/truth-contracts.test.js
Normal file
91
tests/truth-contracts.test.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TruthContract,
|
||||||
|
TimeConverter,
|
||||||
|
FrameTransformer,
|
||||||
|
DatumConverter,
|
||||||
|
TIME_SCALES,
|
||||||
|
FRAMES,
|
||||||
|
HEIGHT_DATUMS
|
||||||
|
} from '../truth-contracts.js';
|
||||||
|
|
||||||
|
test('TruthContract validates frames, time scales, and datums', () => {
|
||||||
|
const contract = new TruthContract({
|
||||||
|
frame: FRAMES.ECI,
|
||||||
|
timeScale: TIME_SCALES.UTC,
|
||||||
|
heightDatum: HEIGHT_DATUMS.ELLIPSOID,
|
||||||
|
tolerance: { meters: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
contract.toString(),
|
||||||
|
`Contract(frame=${FRAMES.ECI}, time=${TIME_SCALES.UTC}, datum=${HEIGHT_DATUMS.ELLIPSOID})`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => new TruthContract({ frame: 'BAD', timeScale: TIME_SCALES.UTC }),
|
||||||
|
/Invalid frame/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => new TruthContract({ frame: FRAMES.ECI, timeScale: 'BAD' }),
|
||||||
|
/Invalid time scale/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => new TruthContract({ frame: FRAMES.ECI, timeScale: TIME_SCALES.UTC, heightDatum: 'BAD' }),
|
||||||
|
/Invalid height datum/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TimeConverter handles leap seconds and time conversions', () => {
|
||||||
|
const preLeap = new Date('1972-06-30T23:59:59Z');
|
||||||
|
const firstLeap = new Date('1972-07-01T00:00:00Z');
|
||||||
|
const current = new Date('2017-01-01T00:00:00Z');
|
||||||
|
|
||||||
|
assert.equal(TimeConverter.getLeapSeconds(preLeap), 10);
|
||||||
|
assert.equal(TimeConverter.getLeapSeconds(firstLeap), 11);
|
||||||
|
assert.equal(TimeConverter.getLeapSeconds(current), 37);
|
||||||
|
|
||||||
|
const tai = TimeConverter.utcToTAI(current);
|
||||||
|
assert.equal(tai.getTime() - current.getTime(), 37 * 1000);
|
||||||
|
|
||||||
|
const roundTripUtc = TimeConverter.convert(tai, TIME_SCALES.TAI, TIME_SCALES.UTC);
|
||||||
|
assert.equal(roundTripUtc.getTime(), current.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TimeConverter dateToJulianDate uses Unix epoch reference', () => {
|
||||||
|
const epoch = new Date(Date.UTC(1970, 0, 1, 0, 0, 0));
|
||||||
|
const jd = TimeConverter.dateToJulianDate(epoch);
|
||||||
|
assert.equal(jd, 2440587.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FrameTransformer warns on rotating vs inertial frames', () => {
|
||||||
|
const warnings = [];
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
console.warn = (message) => warnings.push(message);
|
||||||
|
|
||||||
|
FrameTransformer.validateFrameCompatibility(FRAMES.ECEF, FRAMES.ECI, 'test');
|
||||||
|
|
||||||
|
console.warn = originalWarn;
|
||||||
|
assert.equal(warnings.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FrameTransformer does not warn for same frame category', () => {
|
||||||
|
const warnings = [];
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
console.warn = (message) => warnings.push(message);
|
||||||
|
|
||||||
|
FrameTransformer.validateFrameCompatibility(FRAMES.ECEF, FRAMES.TOPOCENTRIC, 'test');
|
||||||
|
|
||||||
|
console.warn = originalWarn;
|
||||||
|
assert.equal(warnings.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DatumConverter returns expected undulation at equator', () => {
|
||||||
|
const undulation = DatumConverter.getGeoidUndulation(0, 0);
|
||||||
|
assert.ok(Math.abs(undulation) < 1e-9);
|
||||||
|
|
||||||
|
const height = DatumConverter.orthometricToEllipsoidal(0, 0, 100);
|
||||||
|
assert.equal(height, 100);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user