| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- const { eq, ok, notOk, deepEq } = require('../../helpers/assert');
- const { makeNetwork, makePeer } = require('../../helpers/setup');
- const HOUSES = ['academia','solaris','arrakis','terraverde','unsystem','dogma','helix','quark','hermandad'];
- describe('larp: house membership', (t) => {
- t('a new user is outside the LARP (no house) by default', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const house = await A.use('larp').getUserHouse(A.keypair.id);
- eq(house, null);
- });
- t('publishLeaveLarp returns a previously-in-LARP user to the no-house state', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('academia');
- eq(await lm.getUserHouse(A.keypair.id), 'academia');
- await lm.publishLeaveLarp();
- eq(await lm.getUserHouse(A.keypair.id), null);
- });
- t('after publishLeaveLarp the user is not counted in any house', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('solaris');
- await lm.publishLeaveLarp();
- const members = await lm.getMembersOfHouse('solaris');
- notOk(members.includes(A.keypair.id));
- const acaMembers = await lm.getMembersOfHouse('academia');
- notOk(acaMembers.includes(A.keypair.id));
- });
- t('publishJoin moves user to chosen house', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- await A.use('larp').publishJoin('dogma');
- const house = await A.use('larp').getUserHouse(A.keypair.id);
- eq(house, 'dogma');
- });
- t('latest publishJoin wins over earlier ones', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- await A.use('larp').publishJoin('solaris');
- await A.use('larp').publishJoin('quark');
- eq(await A.use('larp').getUserHouse(A.keypair.id), 'quark');
- });
- t('publishJoin rejects invalid house key', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- let threw = false;
- try { await A.use('larp').publishJoin('not-a-house'); } catch (_) { threw = true; }
- ok(threw);
- });
- t('listHousesWithCounts returns all 9 houses with member counts', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net);
- A.setActor(); await A.use('larp').publishJoin('helix');
- B.setActor(); await B.use('larp').publishJoin('helix');
- const houses = await A.use('larp').listHousesWithCounts();
- eq(houses.length, 9);
- deepEq(houses.map(h => h.key), HOUSES);
- const helix = houses.find(h => h.key === 'helix');
- eq(helix.memberCount, 2);
- });
- t('getMembersOfHouse returns only that house members', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net); const C = makePeer(net);
- A.setActor(); await A.use('larp').publishJoin('arrakis');
- B.setActor(); await B.use('larp').publishJoin('arrakis');
- C.setActor(); await C.use('larp').publishJoin('solaris');
- A.setActor();
- const arrakisMembers = await A.use('larp').getMembersOfHouse('arrakis');
- eq(arrakisMembers.length, 2);
- ok(arrakisMembers.includes(A.keypair.id));
- ok(arrakisMembers.includes(B.keypair.id));
- notOk(arrakisMembers.includes(C.keypair.id));
- });
- });
- describe('larp: governance cycle', (t) => {
- t('computeCycle returns formatted string', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const cycle = A.use('larp').computeCycle();
- ok(cycle);
- eq(typeof cycle.formatted, 'string');
- ok(cycle.formatted.length > 0);
- ok(HOUSES.includes(cycle.houseKey));
- });
- t('getGoverningHouseKey maps month index to house', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- for (let i = 0; i < 12; i += 1) {
- const d = new Date(2026, i, 15);
- eq(lm.getGoverningHouseKey(d), HOUSES[i % 9]);
- }
- });
- });
- describe('larp: house wall posts', (t) => {
- t('member can publish a post visible to other members', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net);
- A.setActor(); await A.use('larp').publishJoin('terraverde');
- await A.use('larp').publishHousePost({ house: 'terraverde', text: 'inside the gardens' });
- B.setActor(); await B.use('larp').publishJoin('terraverde');
- const posts = await B.use('larp').listHousePosts('terraverde', { viewerHouse: 'terraverde' });
- eq(posts.length, 1);
- eq(posts[0].text, 'inside the gardens');
- eq(posts[0].author, A.keypair.id);
- });
- t('non-member sees nothing when the house is not governing', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net);
- A.setActor(); await A.use('larp').publishJoin('quark');
- await A.use('larp').publishHousePost({ house: 'quark', text: 'reserved' });
- B.setActor();
- const posts = await B.use('larp').listHousePosts('quark', { viewerHouse: 'academia', isGoverning: false });
- eq(posts.length, 0);
- });
- t('non-member sees the wall when the house is currently governing', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net);
- A.setActor(); await A.use('larp').publishJoin('quark');
- await A.use('larp').publishHousePost({ house: 'quark', text: 'public address' });
- B.setActor();
- const posts = await B.use('larp').listHousePosts('quark', { viewerHouse: 'academia', isGoverning: true });
- eq(posts.length, 1);
- eq(posts[0].text, 'public address');
- });
- t('post from non-member of the house is filtered out', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- await A.use('larp').publishJoin('solaris');
- await A.use('larp').publishHousePost({ house: 'dogma', text: 'spoof' });
- const posts = await A.use('larp').listHousePosts('dogma', { isGoverning: true });
- eq(posts.length, 0);
- });
- t('publishHousePost rejects invalid house', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- let threw = false;
- try { await A.use('larp').publishHousePost({ house: 'invalid', text: 'x' }); } catch (_) { threw = true; }
- ok(threw);
- });
- t('publishHousePost rejects empty text', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- let threw = false;
- try { await A.use('larp').publishHousePost({ house: 'academia', text: ' ' }); } catch (_) { threw = true; }
- ok(threw);
- });
- });
- describe('larp: entrance test attempts', (t) => {
- t('canTakeTest is true for a fresh user', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const can = await A.use('larp').canTakeTest(A.keypair.id);
- eq(can.allowed, true);
- eq(can.last, null);
- });
- t('getProfileTest returns 10 questions with multiple options each', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const questions = A.use('larp').getProfileTest();
- eq(questions.length, 10);
- for (const q of questions) {
- ok(typeof q.question === 'string' && q.question.length > 0);
- ok(Array.isArray(q.options) && q.options.length >= 2);
- for (const opt of q.options) ok(typeof opt === 'string' && opt.length > 0);
- }
- });
- t('scoreProfileAnswers always picks a non-academia house', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- const allZero = lm.getProfileTest().map(() => 0);
- const flat = lm.scoreProfileAnswers(allZero);
- ok(typeof flat.bestHouse === 'string');
- ok(HOUSES.includes(flat.bestHouse));
- notOk(flat.bestHouse === 'academia');
- });
- t('scoreProfileAnswers tie-breaks by fewer members then alphabetical', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- const empty = lm.getProfileTest().map(() => -1);
- const noCounts = lm.scoreProfileAnswers(empty);
- eq(noCounts.bestHouse, 'arrakis', 'with empty answers and no counts, alpha order wins (arrakis < dogma < ...)');
- const withCounts = lm.scoreProfileAnswers(empty, { arrakis: 5, dogma: 0 });
- eq(withCounts.bestHouse, 'dogma', 'with counts, the less-populated house should win the tie');
- });
- t('submitProfileTest fills WILL form, assigns a house and records the attempt', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- const questions = lm.getProfileTest();
- const answers = questions.map(() => 0);
- const result = await lm.submitProfileTest({ answers });
- eq(result.ok, true);
- eq(result.passed, true);
- ok(typeof result.house === 'string' && result.house !== 'academia');
- eq(await lm.getUserHouse(A.keypair.id), result.house);
- const last = await lm.getLastTestAttempt(A.keypair.id);
- ok(last);
- eq(last.house, result.house);
- });
- t('cooldown blocks a second profile test within 30 cycles', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- const answers = lm.getProfileTest().map(() => 0);
- await lm.submitProfileTest({ answers });
- const can = await lm.canTakeTest(A.keypair.id);
- eq(can.allowed, false);
- ok(Number.isFinite(can.nextAt));
- ok(can.last);
- const second = await lm.submitProfileTest({ answers });
- eq(second.ok, false);
- eq(second.reason, 'cooldown');
- });
- });
- describe('larp: tribe integration', (t) => {
- t('joining a non-academia house auto-creates the matching tribe', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('helix');
- const tribe = await lm.findMyHouseTribe('helix');
- ok(tribe, 'house tribe should exist after joining');
- eq(tribe.author, A.keypair.id);
- ok(Array.isArray(tribe.members) && tribe.members.includes(A.keypair.id));
- ok((tribe.tags || []).includes('larp-HeliX'));
- eq(tribe.status, 'PRIVATE');
- eq(tribe.inviteMode, 'open');
- eq(tribe.isAnonymous, true);
- eq(tribe.image, '/assets/larp/images/helix.jpg', 'tribe image should match house image');
- });
- t('ensureHouseTribe is idempotent — does not create duplicates', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('arrakis');
- const first = await lm.ensureHouseTribe('arrakis');
- const second = await lm.ensureHouseTribe('arrakis');
- ok(first && second);
- eq(first.id, second.id);
- });
- t('academia tribe is public and not E2E-encrypted', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('academia');
- const tribe = await lm.findMyHouseTribe('academia');
- ok(tribe);
- eq(tribe.status, 'PUBLIC');
- eq(tribe.isAnonymous, false);
- eq(tribe.inviteMode, 'open');
- ok((tribe.tags || []).includes('larp-ACADEMIA'));
- eq(tribe.image, '/assets/larp/images/academia.jpg');
- });
- t('leaving a house leaves its tribe', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('dogma');
- const before = await lm.findMyHouseTribe('dogma');
- ok(before);
- await lm.publishJoin('academia');
- const after = await lm.findMyHouseTribe('dogma');
- ok(!after, 'previous house tribe should no longer list viewer as member');
- });
- t('publishJoin keeps the joiner attached to the newly entered tribe', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('solaris');
- await lm.publishJoin('quark');
- const sol = await lm.findMyHouseTribe('solaris');
- const qua = await lm.findMyHouseTribe('quark');
- ok(!sol, 'should have left solaris tribe');
- ok(qua, 'should be in quark tribe');
- });
- });
- describe('larp: invitation codes (tribe-backed)', (t) => {
- t('member generates a code via tribesModel.generateInvite', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const lm = A.use('larp');
- await lm.publishJoin('quark');
- const { code, house, tribeId } = await lm.createHouseInvite('quark');
- eq(house, 'quark');
- eq(typeof code, 'string');
- eq(code.length, 32);
- ok(typeof tribeId === 'string' && tribeId.length > 0);
- });
- t('non-member cannot create a house invite', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- let threw = false;
- try { await A.use('larp').createHouseInvite('quark'); } catch (_) { threw = true; }
- ok(threw);
- });
- t('createHouseInvite rejects academia', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- let threw = false;
- try { await A.use('larp').createHouseInvite('academia'); } catch (_) { threw = true; }
- ok(threw);
- });
- t('redeeming a malformed code fails silently', async () => {
- const net = makeNetwork();
- const A = makePeer(net); A.setActor();
- const result = await A.use('larp').redeemHouseInvite('not-a-real-code');
- eq(result.ok, false);
- });
- t('A invites B (outsider) to her house; B ends up in the same house AND tribe', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net);
- A.setActor();
- await A.use('larp').publishJoin('solaris');
- const { code, tribeId } = await A.use('larp').createHouseInvite('solaris');
- eq(typeof code, 'string');
- eq(code.length, 32);
- B.setActor();
- eq(await B.use('larp').getUserHouse(B.keypair.id), null);
- const result = await B.use('larp').redeemHouseInvite(code);
- eq(result.ok, true);
- eq(result.house, 'solaris');
- eq(result.tribeId, tribeId);
- eq(await B.use('larp').getUserHouse(B.keypair.id), 'solaris');
- const tribe = await B.use('tribes').getTribeById(tribeId);
- ok(tribe.members.includes(B.keypair.id), 'B should be member of the tribe');
- ok(tribe.members.includes(A.keypair.id), 'A should still be member of the tribe');
- });
- t('A invites B (already in academia); B switches from academia tribe to solaris tribe', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net);
- A.setActor();
- await A.use('larp').publishJoin('solaris');
- const { code } = await A.use('larp').createHouseInvite('solaris');
- B.setActor();
- await B.use('larp').publishJoin('academia');
- const academiaTribeBefore = await B.use('larp').findMyHouseTribe('academia');
- ok(academiaTribeBefore, 'B is in academia tribe before redeeming');
- const result = await B.use('larp').redeemHouseInvite(code);
- eq(result.ok, true);
- eq(result.house, 'solaris');
- eq(await B.use('larp').getUserHouse(B.keypair.id), 'solaris');
- const academiaTribeAfter = await B.use('larp').findMyHouseTribe('academia');
- ok(!academiaTribeAfter, 'B should have left the academia tribe');
- const solarisTribe = await B.use('larp').findMyHouseTribe('solaris');
- ok(solarisTribe, 'B is in solaris tribe');
- });
- t('a tribe invite for a house can be used only once', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net); const C = makePeer(net);
- A.setActor();
- await A.use('larp').publishJoin('helix');
- const { code } = await A.use('larp').createHouseInvite('helix');
- B.setActor();
- const first = await B.use('larp').redeemHouseInvite(code);
- eq(first.ok, true);
- C.setActor();
- const second = await C.use('larp').redeemHouseInvite(code);
- eq(second.ok, false, 'a consumed invite cannot be reused');
- });
- t('open inviteMode allows new members to generate further invites', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net); const C = makePeer(net);
- A.setActor();
- await A.use('larp').publishJoin('arrakis');
- const inviteAB = await A.use('larp').createHouseInvite('arrakis');
- B.setActor();
- await B.use('larp').redeemHouseInvite(inviteAB.code);
- eq(await B.use('larp').getUserHouse(B.keypair.id), 'arrakis');
- const inviteBC = await B.use('larp').createHouseInvite('arrakis');
- eq(typeof inviteBC.code, 'string');
- eq(inviteBC.code.length, 32);
- C.setActor();
- const result = await C.use('larp').redeemHouseInvite(inviteBC.code);
- eq(result.ok, true);
- eq(result.house, 'arrakis');
- });
- t('redeeming an invite while already in a non-academia house is rejected', async () => {
- const net = makeNetwork();
- const A = makePeer(net); const B = makePeer(net);
- A.setActor();
- await A.use('larp').publishJoin('quark');
- const { code } = await A.use('larp').createHouseInvite('quark');
- B.setActor();
- await B.use('larp').publishJoin('dogma');
- const result = await B.use('larp').redeemHouseInvite(code);
- eq(result.ok, false, 'must leave current house before redeeming a different invite');
- eq(await B.use('larp').getUserHouse(B.keypair.id), 'dogma');
- });
- });
|