SOURCE / PINNED RELEASE
Made of little things.
Black Site
- Release
- 666bc7c74ebd…
- Author-recorded commit
- 05e95af79676…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391
Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import assert from 'node:assert/strict';
import { readFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { runInNewContext } from 'node:vm';
import test from 'node:test';
import ts from 'typescript';
// data.js expects MAPS in scope (vite.config.ts injects the JSON maps from src/game/maps)
const maps = JSON.parse(readFileSync('src/game/maps/index.json', 'utf8')).campaign.map((id) => JSON.parse(readFileSync(`src/game/maps/${id}.json`, 'utf8')));
const catalog = JSON.parse(JSON.stringify(runInNewContext(`const MAPS = ${JSON.stringify(maps)};\n` + readFileSync('src/game/data.js', 'utf8') +'\n({weapons:Object.keys(WEAPONS),throwables:THROWABLE_IDS,armors:ARMOUR_IDS,maxWeaponLevel:MAX_WEAPON_LEVEL,maxArmorLevel:MAX_ARMOUR_LEVEL,primary:PRIMARY_IDS,secondary:SECONDARY_IDS,characters:Object.keys(CHARACTERS),difficulties:Object.keys(DIFFICULTIES),maps:MAPS.map(m=>m.id)})')));
const alice = 'a'.repeat(64), bob = 'b'.repeat(64);
const defaults = () => ({ character:'assaulter', loadout:{primary:'m4a1',secondary:'m1911',throwable:'frag'},equippedArmor:null,difficulty:'normal',map:'town' });
function modules(storage = new Map()) {
const cache = new Map();
const sdk = { storage: { getItem: async key => storage.get(key) ?? null, setItem: async (key,value) => {storage.set(key,value);} } };
function load(path) {
if (cache.has(path)) return cache.get(path).exports;
const module = {exports:{}}; cache.set(path,module);
const code = ts.transpileModule(readFileSync(path,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
new Function('require','module','exports',code)(name => {
if (name === '@napplet/sdk') return sdk;
if (name === 'virtual:black-site-catalog') return {armoryCatalog:catalog};
if (name.endsWith('domain-availability.js')) return {runtimeHasDomain:()=>true};
if (name.endsWith('armory-host.js')) return {openArmoryCollection:()=>{throw Error('no fixture')},openCustomizationCollection:()=>{throw Error('no fixture')}};
const file = resolve(dirname(path),name.replace(/\.js$/,'.ts'));
if (!existsSync(file)) throw Error(name);
return load(file);
},module,module.exports);
return module.exports;
}
return {load:name=>load(resolve('src',name+'.ts')),sdk,storage};
}
const model = modules().load('armory');
test('armory validates the live catalog and merges permanent progression without funds/equipment',()=>{
const a = model.validateArmory({unlockedThrowables:['frag'],ownedArmors:['soft'],weaponLevels:{m4a1:2},armorLevels:{soft:1}});
const b = model.validateArmory({unlockedThrowables:['smoke'],ownedArmors:['l1'],weaponLevels:{m4a1:1,ak47:4},armorLevels:{l1:3}});
const merged = model.mergeArmory(a,b);
assert.deepEqual(merged.unlockedThrowables,['frag','smoke']); assert.equal(merged.weaponLevels.m4a1,2);
assert.equal(merged.armorLevels.l1,3);
const profile = model.withArmory({bank:2300,armour:'soft',unrelated:1},merged);
assert.equal(profile.bank,2300); assert.equal(profile.armour,'soft'); assert.equal(profile.unrelated,1);
assert.equal(model.armoryDigest(model.mergeArmory(a,b)),model.armoryDigest(model.mergeArmory(b,a)));
assert.throws(()=>model.validateArmory({...a,weaponLevels:{m4a1:21}}));
assert.throws(()=>model.validateArmory({...a,armorLevels:{l4:1}}));
assert.throws(()=>model.validateArmory({...a,unlockedThrowables:['frag','frag']}));
assert.throws(()=>model.validateArmory({...a,ownedArmors:['future-item']}));
});
async function fixture(kind='armory', provider={record:null}) {
const m = modules();
const store = m.load('save-store').createSaveStore();
store.activate(await store.load(alice));
let profile = {bank:3000,armour:null,unlocked:[],armours:[],levels:{},armourLevels:{}};
let choices = defaults(), safe = true;
const game = {isSafe:()=>safe,profile:()=>structuredClone(profile),customization:()=>structuredClone(choices),
applyArmory:value=>{profile=model.withArmory(profile,value);store.set('blacksite.profile',profile)},
applyCustomization:value=>{choices=structuredClone(value)}};
const collection = {
get:async author=> provider.get ? provider.get(author) : {record: provider.record?.author===author ? structuredClone(provider.record):null,incomplete:!!provider.incomplete},
prepare:async ({data,base})=>{
const snapshot=structuredClone(data); const template={content:JSON.stringify({previous:base?.revision??null})};
return {template,publish:async()=>{
provider.calls=(provider.calls??0)+1;
if(provider.fail) throw Error(provider.fail);
if(provider.delay) await new Promise(resolve=>{provider.finish=resolve});
provider.record={author:store.owner(),revision:String(provider.calls).padStart(64,'0'),data:snapshot,deleted:false};
return structuredClone(provider.record);
}};
}
};
const states=[];
const sync = kind==='armory' ? m.load('armory-sync').createArmorySync(store,()=>game,s=>states.push(s),async()=>collection)
: m.load('customization-sync').createCustomizationSync(store,()=>game,s=>states.push(s),async()=>collection);
return {m,store,sync,states,provider,game,profile:()=>profile,choices:()=>choices,
purchase:armory=>{profile=model.withArmory(profile,armory);game.applyArmory(armory);sync.changed()},
choose:value=>{choices=value;sync.changed()},safe:value=>{safe=value}};
}
const record = data=>({author:alice,revision:'1'.repeat(64),data,deleted:false});
test('two devices restore and merge independent unlocks; complete reads and approval gate writes',async()=>{
const provider={record:record(model.emptyArmory())};
const first=await fixture('armory',provider); await first.sync.select(alice);first.sync.enable();
first.purchase({...model.emptyArmory(),unlockedThrowables:['frag'],weaponLevels:{m4a1:3}});
await first.sync.save();assert.equal(first.states.at(-1).message,'Saved to account');
const second=await fixture('armory',provider); await second.sync.select(alice);
assert.equal(second.profile().levels.m4a1,3); assert.equal(second.profile().bank,3000);
second.sync.enable();second.purchase({...model.profileArmory(second.profile()),unlockedThrowables:['frag','smoke']});
await second.sync.save();await first.sync.restore();
assert.deepEqual(first.profile().unlocked,['frag','smoke']);
const calls=provider.calls;await first.sync.save();assert.equal(provider.calls,calls,'unchanged saves do not publish');
provider.incomplete=true;first.purchase({...model.profileArmory(first.profile()),weaponLevels:{m4a1:4}});
await first.sync.save();assert.equal(provider.calls,calls,'partial reads block writes');
provider.incomplete=false;provider.fail='consent-denied';await first.sync.save();
assert.match(first.states.at(-1).message,/not approved/);assert.equal(first.profile().levels.m4a1,4);
await Promise.all([first.store.flush(),second.store.flush()]);
});
test('remote data waits until menu; newer purchases stay pending during a publication',async()=>{
const provider={record:record({...model.emptyArmory(),unlockedThrowables:['frag']})};
const h=await fixture('armory',provider);h.safe(false);await h.sync.select(alice);
assert.deepEqual(h.profile().unlocked,[]);h.safe(true);h.sync.menu();assert.deepEqual(h.profile().unlocked,['frag']);
h.sync.enable();h.purchase({...model.profileArmory(h.profile()),weaponLevels:{m4a1:1}});
provider.delay=true;const save=h.sync.save();while(!provider.finish)await new Promise(r=>setImmediate(r));
h.purchase({...model.profileArmory(h.profile()),weaponLevels:{m4a1:2}});provider.finish();await save;
assert.equal(provider.record.data.weaponLevels.m4a1,1);assert.equal(h.profile().levels.m4a1,2);
assert.match(h.states.at(-1).message,/Newer changes/);await h.store.flush();
});
test('account changes ignore late reads and never publish old progress to a different player',async()=>{
const h=await fixture();let finish;
h.provider.get=()=>new Promise(resolve=>{finish=resolve});const selecting=h.sync.select(alice);
while(!finish)await new Promise(r=>setImmediate(r));h.sync.suspend();
h.store.activate(await h.store.load(bob));finish({record:record({...model.emptyArmory(),unlockedThrowables:['frag']}),incomplete:false});await selecting;
assert.deepEqual(h.profile().unlocked,[]);await h.sync.save();assert.equal(h.provider.calls,undefined);
});
test('local saves are account-separated and unreadable values cannot be overwritten',async()=>{
const m=modules();const store=m.load('save-store').createSaveStore();
store.activate(await store.load(alice));store.set('blacksite.profile',{bank:123});await store.flush();
store.activate(await store.load(bob));assert.equal(store.get('blacksite.profile',null),null);
store.set('blacksite.profile',{bank:456});await store.flush();
store.activate(await store.load(alice));assert.equal(store.get('blacksite.profile',{}).bank,123);
const bad=modules(new Map([['blacksite.account.'+alice+'.profile','invalid json']]));
const corrupt=bad.load('save-store').createSaveStore();corrupt.activate(await corrupt.load(alice));corrupt.set('blacksite.profile',{bank:0});await corrupt.flush();
assert.equal(bad.storage.get('blacksite.account.'+alice+'.profile'),'invalid json');assert.equal(corrupt.bucket().saveFailed,true);
});
test('customization restores character/loadout/armor/map/difficulty and preserves unsaved local choices',async()=>{
const desired={...defaults(),loadout:{primary:'ak47',secondary:'deagle',throwable:'smoke'},equippedArmor:'soft',difficulty:'hard',map:'docks'};
const validator=modules().load('customization');assert.deepEqual(validator.validateCustomization(desired),desired);
assert.throws(()=>validator.validateCustomization({...desired,character:'future-character'}));
const provider={record:record(desired)};const first=await fixture('customization',provider);
await first.sync.select(alice);assert.deepEqual(first.choices(),desired);first.sync.enable();
first.choose({...desired,difficulty:'easy'});provider.record=record({...desired,difficulty:'normal'});
await first.sync.restore();assert.equal(first.choices().difficulty,'easy','pending local choices survive another device read');
await first.sync.save();assert.equal(provider.record.data.difficulty,'easy');
const second=await fixture('customization',provider);await second.sync.select(alice);assert.equal(second.choices().difficulty,'easy');
assert.equal(second.profile().bank,3000);await first.store.flush();
});
test('stale publication preserves progression and retry remerges the latest revision',async()=>{
const provider={record:record(model.emptyArmory())};const h=await fixture('armory',provider);
await h.sync.select(alice);h.sync.enable();h.purchase({...model.emptyArmory(),unlockedThrowables:['frag']});
provider.record=record({...model.emptyArmory(),unlockedThrowables:['smoke']});provider.fail='app-data-stale-edit';
await h.sync.save();assert.match(h.states.at(-1).message,/changed on another device/);
assert.deepEqual(h.profile().unlocked,['frag','smoke']);provider.fail=undefined;
await h.sync.save();assert.deepEqual(provider.record.data.unlockedThrowables,['frag','smoke']);
assert.equal(h.states.at(-1).message,'Saved to account');await h.store.flush();
});
