larp.test.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. const { eq, ok, notOk, deepEq } = require('../../helpers/assert');
  2. const { makeNetwork, makePeer } = require('../../helpers/setup');
  3. const HOUSES = ['academia','solaris','arrakis','terraverde','unsystem','dogma','helix','quark','hermandad'];
  4. describe('larp: house membership', (t) => {
  5. t('a new user is outside the LARP (no house) by default', async () => {
  6. const net = makeNetwork();
  7. const A = makePeer(net); A.setActor();
  8. const house = await A.use('larp').getUserHouse(A.keypair.id);
  9. eq(house, null);
  10. });
  11. t('publishLeaveLarp returns a previously-in-LARP user to the no-house state', async () => {
  12. const net = makeNetwork();
  13. const A = makePeer(net); A.setActor();
  14. const lm = A.use('larp');
  15. await lm.publishJoin('academia');
  16. eq(await lm.getUserHouse(A.keypair.id), 'academia');
  17. await lm.publishLeaveLarp();
  18. eq(await lm.getUserHouse(A.keypair.id), null);
  19. });
  20. t('after publishLeaveLarp the user is not counted in any house', async () => {
  21. const net = makeNetwork();
  22. const A = makePeer(net); A.setActor();
  23. const lm = A.use('larp');
  24. await lm.publishJoin('solaris');
  25. await lm.publishLeaveLarp();
  26. const members = await lm.getMembersOfHouse('solaris');
  27. notOk(members.includes(A.keypair.id));
  28. const acaMembers = await lm.getMembersOfHouse('academia');
  29. notOk(acaMembers.includes(A.keypair.id));
  30. });
  31. t('publishJoin moves user to chosen house', async () => {
  32. const net = makeNetwork();
  33. const A = makePeer(net); A.setActor();
  34. await A.use('larp').publishJoin('dogma');
  35. const house = await A.use('larp').getUserHouse(A.keypair.id);
  36. eq(house, 'dogma');
  37. });
  38. t('latest publishJoin wins over earlier ones', async () => {
  39. const net = makeNetwork();
  40. const A = makePeer(net); A.setActor();
  41. await A.use('larp').publishJoin('solaris');
  42. await A.use('larp').publishJoin('quark');
  43. eq(await A.use('larp').getUserHouse(A.keypair.id), 'quark');
  44. });
  45. t('publishJoin rejects invalid house key', async () => {
  46. const net = makeNetwork();
  47. const A = makePeer(net); A.setActor();
  48. let threw = false;
  49. try { await A.use('larp').publishJoin('not-a-house'); } catch (_) { threw = true; }
  50. ok(threw);
  51. });
  52. t('listHousesWithCounts returns all 9 houses with member counts', async () => {
  53. const net = makeNetwork();
  54. const A = makePeer(net); const B = makePeer(net);
  55. A.setActor(); await A.use('larp').publishJoin('helix');
  56. B.setActor(); await B.use('larp').publishJoin('helix');
  57. const houses = await A.use('larp').listHousesWithCounts();
  58. eq(houses.length, 9);
  59. deepEq(houses.map(h => h.key), HOUSES);
  60. const helix = houses.find(h => h.key === 'helix');
  61. eq(helix.memberCount, 2);
  62. });
  63. t('getMembersOfHouse returns only that house members', async () => {
  64. const net = makeNetwork();
  65. const A = makePeer(net); const B = makePeer(net); const C = makePeer(net);
  66. A.setActor(); await A.use('larp').publishJoin('arrakis');
  67. B.setActor(); await B.use('larp').publishJoin('arrakis');
  68. C.setActor(); await C.use('larp').publishJoin('solaris');
  69. A.setActor();
  70. const arrakisMembers = await A.use('larp').getMembersOfHouse('arrakis');
  71. eq(arrakisMembers.length, 2);
  72. ok(arrakisMembers.includes(A.keypair.id));
  73. ok(arrakisMembers.includes(B.keypair.id));
  74. notOk(arrakisMembers.includes(C.keypair.id));
  75. });
  76. });
  77. describe('larp: governance cycle', (t) => {
  78. t('computeCycle returns formatted string', async () => {
  79. const net = makeNetwork();
  80. const A = makePeer(net); A.setActor();
  81. const cycle = A.use('larp').computeCycle();
  82. ok(cycle);
  83. eq(typeof cycle.formatted, 'string');
  84. ok(cycle.formatted.length > 0);
  85. ok(HOUSES.includes(cycle.houseKey));
  86. });
  87. t('getGoverningHouseKey maps month index to house', async () => {
  88. const net = makeNetwork();
  89. const A = makePeer(net); A.setActor();
  90. const lm = A.use('larp');
  91. for (let i = 0; i < 12; i += 1) {
  92. const d = new Date(2026, i, 15);
  93. eq(lm.getGoverningHouseKey(d), HOUSES[i % 9]);
  94. }
  95. });
  96. });
  97. describe('larp: house wall posts', (t) => {
  98. t('member can publish a post visible to other members', async () => {
  99. const net = makeNetwork();
  100. const A = makePeer(net); const B = makePeer(net);
  101. A.setActor(); await A.use('larp').publishJoin('terraverde');
  102. await A.use('larp').publishHousePost({ house: 'terraverde', text: 'inside the gardens' });
  103. B.setActor(); await B.use('larp').publishJoin('terraverde');
  104. const posts = await B.use('larp').listHousePosts('terraverde', { viewerHouse: 'terraverde' });
  105. eq(posts.length, 1);
  106. eq(posts[0].text, 'inside the gardens');
  107. eq(posts[0].author, A.keypair.id);
  108. });
  109. t('non-member sees nothing when the house is not governing', async () => {
  110. const net = makeNetwork();
  111. const A = makePeer(net); const B = makePeer(net);
  112. A.setActor(); await A.use('larp').publishJoin('quark');
  113. await A.use('larp').publishHousePost({ house: 'quark', text: 'reserved' });
  114. B.setActor();
  115. const posts = await B.use('larp').listHousePosts('quark', { viewerHouse: 'academia', isGoverning: false });
  116. eq(posts.length, 0);
  117. });
  118. t('non-member sees the wall when the house is currently governing', async () => {
  119. const net = makeNetwork();
  120. const A = makePeer(net); const B = makePeer(net);
  121. A.setActor(); await A.use('larp').publishJoin('quark');
  122. await A.use('larp').publishHousePost({ house: 'quark', text: 'public address' });
  123. B.setActor();
  124. const posts = await B.use('larp').listHousePosts('quark', { viewerHouse: 'academia', isGoverning: true });
  125. eq(posts.length, 1);
  126. eq(posts[0].text, 'public address');
  127. });
  128. t('post from non-member of the house is filtered out', async () => {
  129. const net = makeNetwork();
  130. const A = makePeer(net); A.setActor();
  131. await A.use('larp').publishJoin('solaris');
  132. await A.use('larp').publishHousePost({ house: 'dogma', text: 'spoof' });
  133. const posts = await A.use('larp').listHousePosts('dogma', { isGoverning: true });
  134. eq(posts.length, 0);
  135. });
  136. t('publishHousePost rejects invalid house', async () => {
  137. const net = makeNetwork();
  138. const A = makePeer(net); A.setActor();
  139. let threw = false;
  140. try { await A.use('larp').publishHousePost({ house: 'invalid', text: 'x' }); } catch (_) { threw = true; }
  141. ok(threw);
  142. });
  143. t('publishHousePost rejects empty text', async () => {
  144. const net = makeNetwork();
  145. const A = makePeer(net); A.setActor();
  146. let threw = false;
  147. try { await A.use('larp').publishHousePost({ house: 'academia', text: ' ' }); } catch (_) { threw = true; }
  148. ok(threw);
  149. });
  150. });
  151. describe('larp: entrance test attempts', (t) => {
  152. t('canTakeTest is true for a fresh user', async () => {
  153. const net = makeNetwork();
  154. const A = makePeer(net); A.setActor();
  155. const can = await A.use('larp').canTakeTest(A.keypair.id);
  156. eq(can.allowed, true);
  157. eq(can.last, null);
  158. });
  159. t('getProfileTest returns 10 questions with multiple options each', async () => {
  160. const net = makeNetwork();
  161. const A = makePeer(net); A.setActor();
  162. const questions = A.use('larp').getProfileTest();
  163. eq(questions.length, 10);
  164. for (const q of questions) {
  165. ok(typeof q.question === 'string' && q.question.length > 0);
  166. ok(Array.isArray(q.options) && q.options.length >= 2);
  167. for (const opt of q.options) ok(typeof opt === 'string' && opt.length > 0);
  168. }
  169. });
  170. t('scoreProfileAnswers always picks a non-academia house', async () => {
  171. const net = makeNetwork();
  172. const A = makePeer(net); A.setActor();
  173. const lm = A.use('larp');
  174. const allZero = lm.getProfileTest().map(() => 0);
  175. const flat = lm.scoreProfileAnswers(allZero);
  176. ok(typeof flat.bestHouse === 'string');
  177. ok(HOUSES.includes(flat.bestHouse));
  178. notOk(flat.bestHouse === 'academia');
  179. });
  180. t('scoreProfileAnswers tie-breaks by fewer members then alphabetical', async () => {
  181. const net = makeNetwork();
  182. const A = makePeer(net); A.setActor();
  183. const lm = A.use('larp');
  184. const empty = lm.getProfileTest().map(() => -1);
  185. const noCounts = lm.scoreProfileAnswers(empty);
  186. eq(noCounts.bestHouse, 'arrakis', 'with empty answers and no counts, alpha order wins (arrakis < dogma < ...)');
  187. const withCounts = lm.scoreProfileAnswers(empty, { arrakis: 5, dogma: 0 });
  188. eq(withCounts.bestHouse, 'dogma', 'with counts, the less-populated house should win the tie');
  189. });
  190. t('submitProfileTest fills WILL form, assigns a house and records the attempt', async () => {
  191. const net = makeNetwork();
  192. const A = makePeer(net); A.setActor();
  193. const lm = A.use('larp');
  194. const questions = lm.getProfileTest();
  195. const answers = questions.map(() => 0);
  196. const result = await lm.submitProfileTest({ answers });
  197. eq(result.ok, true);
  198. eq(result.passed, true);
  199. ok(typeof result.house === 'string' && result.house !== 'academia');
  200. eq(await lm.getUserHouse(A.keypair.id), result.house);
  201. const last = await lm.getLastTestAttempt(A.keypair.id);
  202. ok(last);
  203. eq(last.house, result.house);
  204. });
  205. t('cooldown blocks a second profile test within 30 cycles', async () => {
  206. const net = makeNetwork();
  207. const A = makePeer(net); A.setActor();
  208. const lm = A.use('larp');
  209. const answers = lm.getProfileTest().map(() => 0);
  210. await lm.submitProfileTest({ answers });
  211. const can = await lm.canTakeTest(A.keypair.id);
  212. eq(can.allowed, false);
  213. ok(Number.isFinite(can.nextAt));
  214. ok(can.last);
  215. const second = await lm.submitProfileTest({ answers });
  216. eq(second.ok, false);
  217. eq(second.reason, 'cooldown');
  218. });
  219. });
  220. describe('larp: tribe integration', (t) => {
  221. t('joining a non-academia house auto-creates the matching tribe', async () => {
  222. const net = makeNetwork();
  223. const A = makePeer(net); A.setActor();
  224. const lm = A.use('larp');
  225. await lm.publishJoin('helix');
  226. const tribe = await lm.findMyHouseTribe('helix');
  227. ok(tribe, 'house tribe should exist after joining');
  228. eq(tribe.author, A.keypair.id);
  229. ok(Array.isArray(tribe.members) && tribe.members.includes(A.keypair.id));
  230. ok((tribe.tags || []).includes('larp-HeliX'));
  231. eq(tribe.status, 'PRIVATE');
  232. eq(tribe.inviteMode, 'open');
  233. eq(tribe.isAnonymous, true);
  234. eq(tribe.image, '/assets/larp/images/helix.jpg', 'tribe image should match house image');
  235. });
  236. t('ensureHouseTribe is idempotent — does not create duplicates', async () => {
  237. const net = makeNetwork();
  238. const A = makePeer(net); A.setActor();
  239. const lm = A.use('larp');
  240. await lm.publishJoin('arrakis');
  241. const first = await lm.ensureHouseTribe('arrakis');
  242. const second = await lm.ensureHouseTribe('arrakis');
  243. ok(first && second);
  244. eq(first.id, second.id);
  245. });
  246. t('academia tribe is public and not E2E-encrypted', async () => {
  247. const net = makeNetwork();
  248. const A = makePeer(net); A.setActor();
  249. const lm = A.use('larp');
  250. await lm.publishJoin('academia');
  251. const tribe = await lm.findMyHouseTribe('academia');
  252. ok(tribe);
  253. eq(tribe.status, 'PUBLIC');
  254. eq(tribe.isAnonymous, false);
  255. eq(tribe.inviteMode, 'open');
  256. ok((tribe.tags || []).includes('larp-ACADEMIA'));
  257. eq(tribe.image, '/assets/larp/images/academia.jpg');
  258. });
  259. t('leaving a house leaves its tribe', async () => {
  260. const net = makeNetwork();
  261. const A = makePeer(net); A.setActor();
  262. const lm = A.use('larp');
  263. await lm.publishJoin('dogma');
  264. const before = await lm.findMyHouseTribe('dogma');
  265. ok(before);
  266. await lm.publishJoin('academia');
  267. const after = await lm.findMyHouseTribe('dogma');
  268. ok(!after, 'previous house tribe should no longer list viewer as member');
  269. });
  270. t('publishJoin keeps the joiner attached to the newly entered tribe', async () => {
  271. const net = makeNetwork();
  272. const A = makePeer(net); A.setActor();
  273. const lm = A.use('larp');
  274. await lm.publishJoin('solaris');
  275. await lm.publishJoin('quark');
  276. const sol = await lm.findMyHouseTribe('solaris');
  277. const qua = await lm.findMyHouseTribe('quark');
  278. ok(!sol, 'should have left solaris tribe');
  279. ok(qua, 'should be in quark tribe');
  280. });
  281. });
  282. describe('larp: invitation codes (tribe-backed)', (t) => {
  283. t('member generates a code via tribesModel.generateInvite', async () => {
  284. const net = makeNetwork();
  285. const A = makePeer(net); A.setActor();
  286. const lm = A.use('larp');
  287. await lm.publishJoin('quark');
  288. const { code, house, tribeId } = await lm.createHouseInvite('quark');
  289. eq(house, 'quark');
  290. eq(typeof code, 'string');
  291. eq(code.length, 32);
  292. ok(typeof tribeId === 'string' && tribeId.length > 0);
  293. });
  294. t('non-member cannot create a house invite', async () => {
  295. const net = makeNetwork();
  296. const A = makePeer(net); A.setActor();
  297. let threw = false;
  298. try { await A.use('larp').createHouseInvite('quark'); } catch (_) { threw = true; }
  299. ok(threw);
  300. });
  301. t('createHouseInvite rejects academia', async () => {
  302. const net = makeNetwork();
  303. const A = makePeer(net); A.setActor();
  304. let threw = false;
  305. try { await A.use('larp').createHouseInvite('academia'); } catch (_) { threw = true; }
  306. ok(threw);
  307. });
  308. t('redeeming a malformed code fails silently', async () => {
  309. const net = makeNetwork();
  310. const A = makePeer(net); A.setActor();
  311. const result = await A.use('larp').redeemHouseInvite('not-a-real-code');
  312. eq(result.ok, false);
  313. });
  314. t('A invites B (outsider) to her house; B ends up in the same house AND tribe', async () => {
  315. const net = makeNetwork();
  316. const A = makePeer(net); const B = makePeer(net);
  317. A.setActor();
  318. await A.use('larp').publishJoin('solaris');
  319. const { code, tribeId } = await A.use('larp').createHouseInvite('solaris');
  320. eq(typeof code, 'string');
  321. eq(code.length, 32);
  322. B.setActor();
  323. eq(await B.use('larp').getUserHouse(B.keypair.id), null);
  324. const result = await B.use('larp').redeemHouseInvite(code);
  325. eq(result.ok, true);
  326. eq(result.house, 'solaris');
  327. eq(result.tribeId, tribeId);
  328. eq(await B.use('larp').getUserHouse(B.keypair.id), 'solaris');
  329. const tribe = await B.use('tribes').getTribeById(tribeId);
  330. ok(tribe.members.includes(B.keypair.id), 'B should be member of the tribe');
  331. ok(tribe.members.includes(A.keypair.id), 'A should still be member of the tribe');
  332. });
  333. t('A invites B (already in academia); B switches from academia tribe to solaris tribe', async () => {
  334. const net = makeNetwork();
  335. const A = makePeer(net); const B = makePeer(net);
  336. A.setActor();
  337. await A.use('larp').publishJoin('solaris');
  338. const { code } = await A.use('larp').createHouseInvite('solaris');
  339. B.setActor();
  340. await B.use('larp').publishJoin('academia');
  341. const academiaTribeBefore = await B.use('larp').findMyHouseTribe('academia');
  342. ok(academiaTribeBefore, 'B is in academia tribe before redeeming');
  343. const result = await B.use('larp').redeemHouseInvite(code);
  344. eq(result.ok, true);
  345. eq(result.house, 'solaris');
  346. eq(await B.use('larp').getUserHouse(B.keypair.id), 'solaris');
  347. const academiaTribeAfter = await B.use('larp').findMyHouseTribe('academia');
  348. ok(!academiaTribeAfter, 'B should have left the academia tribe');
  349. const solarisTribe = await B.use('larp').findMyHouseTribe('solaris');
  350. ok(solarisTribe, 'B is in solaris tribe');
  351. });
  352. t('a tribe invite for a house can be used only once', async () => {
  353. const net = makeNetwork();
  354. const A = makePeer(net); const B = makePeer(net); const C = makePeer(net);
  355. A.setActor();
  356. await A.use('larp').publishJoin('helix');
  357. const { code } = await A.use('larp').createHouseInvite('helix');
  358. B.setActor();
  359. const first = await B.use('larp').redeemHouseInvite(code);
  360. eq(first.ok, true);
  361. C.setActor();
  362. const second = await C.use('larp').redeemHouseInvite(code);
  363. eq(second.ok, false, 'a consumed invite cannot be reused');
  364. });
  365. t('open inviteMode allows new members to generate further invites', async () => {
  366. const net = makeNetwork();
  367. const A = makePeer(net); const B = makePeer(net); const C = makePeer(net);
  368. A.setActor();
  369. await A.use('larp').publishJoin('arrakis');
  370. const inviteAB = await A.use('larp').createHouseInvite('arrakis');
  371. B.setActor();
  372. await B.use('larp').redeemHouseInvite(inviteAB.code);
  373. eq(await B.use('larp').getUserHouse(B.keypair.id), 'arrakis');
  374. const inviteBC = await B.use('larp').createHouseInvite('arrakis');
  375. eq(typeof inviteBC.code, 'string');
  376. eq(inviteBC.code.length, 32);
  377. C.setActor();
  378. const result = await C.use('larp').redeemHouseInvite(inviteBC.code);
  379. eq(result.ok, true);
  380. eq(result.house, 'arrakis');
  381. });
  382. t('redeeming an invite while already in a non-academia house is rejected', async () => {
  383. const net = makeNetwork();
  384. const A = makePeer(net); const B = makePeer(net);
  385. A.setActor();
  386. await A.use('larp').publishJoin('quark');
  387. const { code } = await A.use('larp').createHouseInvite('quark');
  388. B.setActor();
  389. await B.use('larp').publishJoin('dogma');
  390. const result = await B.use('larp').redeemHouseInvite(code);
  391. eq(result.ok, false, 'must leave current house before redeeming a different invite');
  392. eq(await B.use('larp').getUserHouse(B.keypair.id), 'dogma');
  393. });
  394. });