@bongos/core 1.19.714 → 1.19.715
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +27 -17
- package/docs/copy-inventory.md +15 -13
- package/docs/copy-registry.json +35 -17
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/watch.css +6 -0
- package/modules/hall-ui/public/watch.js +52 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/backfill-task-rewards.js +9 -4
- package/src/module-api.js +1 -1
- package/tests/credit_log_single_writer.mjs +94 -0
- package/tests/helpers.mjs +8 -0
- package/tests/watch_roster_self_row.mjs +289 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.715",
|
|
6
|
+
"core_contract": "1.19.715",
|
|
7
|
+
"source_commit": "b62e25ab61b1f2c4994e6b53ae9958cd34596927",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-13T08:14:55.124Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 478,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2149,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2651,
|
|
20
|
+
"tree_sha256": "7ec11a13e61036eaa680d8780521289c206117336ca95013e22bddde2dc83a1f",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -1957,12 +1957,12 @@
|
|
|
1957
1957
|
{
|
|
1958
1958
|
"path": "docs/copy-inventory.md",
|
|
1959
1959
|
"mode": "0000644",
|
|
1960
|
-
"sha256": "
|
|
1960
|
+
"sha256": "8a54b0082d0eddeb63878440f6b42fb0a701e65284c54b7e4475a0685e943c88"
|
|
1961
1961
|
},
|
|
1962
1962
|
{
|
|
1963
1963
|
"path": "docs/copy-registry.json",
|
|
1964
1964
|
"mode": "0000644",
|
|
1965
|
-
"sha256": "
|
|
1965
|
+
"sha256": "fe218fd29cc8f03ceb3ff3ea8dd2bcb36df81a75a73bcdfe73e7af90fa6e3ad0"
|
|
1966
1966
|
},
|
|
1967
1967
|
{
|
|
1968
1968
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2807,7 +2807,7 @@
|
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/module-api-changelog.md",
|
|
2809
2809
|
"mode": "0000644",
|
|
2810
|
-
"sha256": "
|
|
2810
|
+
"sha256": "b057c7c856c96a08e25c433ae816ac6ff9762da2f510ee74defdefbf6151ca14"
|
|
2811
2811
|
},
|
|
2812
2812
|
{
|
|
2813
2813
|
"path": "docs/modules-contract.md",
|
|
@@ -5522,7 +5522,7 @@
|
|
|
5522
5522
|
{
|
|
5523
5523
|
"path": "modules/hall-ui/public/watch.css",
|
|
5524
5524
|
"mode": "0000644",
|
|
5525
|
-
"sha256": "
|
|
5525
|
+
"sha256": "f11f73ec3649c7547cb59b7d5fdf090a1e52ad96f6fb28bdb3b5a14691153de1"
|
|
5526
5526
|
},
|
|
5527
5527
|
{
|
|
5528
5528
|
"path": "modules/hall-ui/public/watch.html",
|
|
@@ -5532,7 +5532,7 @@
|
|
|
5532
5532
|
{
|
|
5533
5533
|
"path": "modules/hall-ui/public/watch.js",
|
|
5534
5534
|
"mode": "0000644",
|
|
5535
|
-
"sha256": "
|
|
5535
|
+
"sha256": "671cce72893d85971cc81ed7a64f7c3796bcb1f3bbdf7b28ab68e7e4e1d5b11c"
|
|
5536
5536
|
},
|
|
5537
5537
|
{
|
|
5538
5538
|
"path": "modules/hall-ui/public/watch.states.json",
|
|
@@ -7817,12 +7817,12 @@
|
|
|
7817
7817
|
{
|
|
7818
7818
|
"path": "package-lock.json",
|
|
7819
7819
|
"mode": "0000644",
|
|
7820
|
-
"sha256": "
|
|
7820
|
+
"sha256": "76f1c9e8c1f6a12a9857eff4b538e700c337252afd4ecb40536088a7c967f81b"
|
|
7821
7821
|
},
|
|
7822
7822
|
{
|
|
7823
7823
|
"path": "package.json",
|
|
7824
7824
|
"mode": "0000644",
|
|
7825
|
-
"sha256": "
|
|
7825
|
+
"sha256": "c65aae0250d7c1c324a3355a81d0bb5004a1bf7b339e0c6b69f819640e33eac4"
|
|
7826
7826
|
},
|
|
7827
7827
|
{
|
|
7828
7828
|
"path": "public-docs/index.html",
|
|
@@ -7982,7 +7982,7 @@
|
|
|
7982
7982
|
{
|
|
7983
7983
|
"path": "scripts/gds/backfill-task-rewards.js",
|
|
7984
7984
|
"mode": "0000644",
|
|
7985
|
-
"sha256": "
|
|
7985
|
+
"sha256": "6328a73f8ce38754280d8f7b6234c0eea83c5e5061534550f44b0f7d78b12224"
|
|
7986
7986
|
},
|
|
7987
7987
|
{
|
|
7988
7988
|
"path": "scripts/gds/backlog-review.js",
|
|
@@ -9602,7 +9602,7 @@
|
|
|
9602
9602
|
{
|
|
9603
9603
|
"path": "src/module-api.js",
|
|
9604
9604
|
"mode": "0000644",
|
|
9605
|
-
"sha256": "
|
|
9605
|
+
"sha256": "b0482a0410f077b4c9a41a19cf85a46dc527a20d7a897023cddff8d89787e39f"
|
|
9606
9606
|
},
|
|
9607
9607
|
{
|
|
9608
9608
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10384,6 +10384,11 @@
|
|
|
10384
10384
|
"mode": "0000644",
|
|
10385
10385
|
"sha256": "d7c96a67f1eda974a9387a47d12bf70d7f2125ea77c1ddb313e5a3feb47ae04f"
|
|
10386
10386
|
},
|
|
10387
|
+
{
|
|
10388
|
+
"path": "tests/credit_log_single_writer.mjs",
|
|
10389
|
+
"mode": "0000644",
|
|
10390
|
+
"sha256": "e1ce127d39f8f6aad3f819ab828d66de3d211484174849b1a0ce6a6ab7546b36"
|
|
10391
|
+
},
|
|
10387
10392
|
{
|
|
10388
10393
|
"path": "tests/criterion_authoring_gate.mjs",
|
|
10389
10394
|
"mode": "0000644",
|
|
@@ -11312,7 +11317,7 @@
|
|
|
11312
11317
|
{
|
|
11313
11318
|
"path": "tests/helpers.mjs",
|
|
11314
11319
|
"mode": "0000644",
|
|
11315
|
-
"sha256": "
|
|
11320
|
+
"sha256": "2c5ffa3ed447fdbbbc1b44e553f493bf05ab2848b154836defe3fe40e1909592"
|
|
11316
11321
|
},
|
|
11317
11322
|
{
|
|
11318
11323
|
"path": "tests/hierarchy-config.mjs",
|
|
@@ -13199,6 +13204,11 @@
|
|
|
13199
13204
|
"mode": "0000644",
|
|
13200
13205
|
"sha256": "bca521caccef7c1c147bfa678203369b246f786d4d8cb5a485f15a9d11f925ba"
|
|
13201
13206
|
},
|
|
13207
|
+
{
|
|
13208
|
+
"path": "tests/watch_roster_self_row.mjs",
|
|
13209
|
+
"mode": "0000644",
|
|
13210
|
+
"sha256": "a582a7dc1490038b9d5fed9551b00d6cc1ded2758f06d4c2f21c36e4aea12a9c"
|
|
13211
|
+
},
|
|
13202
13212
|
{
|
|
13203
13213
|
"path": "tests/watch_sealed_floor.mjs",
|
|
13204
13214
|
"mode": "0000644",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**1751 strings** across 3 surfaces · 1515 distinct pieces of copy · 1751 certain, 0 heuristic · 164 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 80 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 80 | 1272 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 5 | 379 |
|
|
16
16
|
|
|
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
78
78
|
| `a072071b893c` | 2x | All time | modules/status-ui/public/index.html:286, modules/status-ui/public/index.html:313 |
|
|
79
79
|
| … | | _124 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1272 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -285,8 +285,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
285
285
|
| `7bcaeffb742f` | certain | Could not approve: {…} | `modules/hall-ui/public/gate.js:108` |
|
|
286
286
|
| `c922293e668c` | certain | Could not assign: {…} | `modules/hall-ui/public/government.js:468` |
|
|
287
287
|
| `94763c4d5a94` | certain | Could not block: {…} | `modules/hall-ui/public/harbor.js:167` |
|
|
288
|
-
| `e1fc7cedf4e7` | certain | Could not change rank: {…} | `modules/hall-ui/public/watch.js:
|
|
289
|
-
| `b9e53fbd90e6` | certain | Could not change standing: {…} | `modules/hall-ui/public/watch.js:
|
|
288
|
+
| `e1fc7cedf4e7` | certain | Could not change rank: {…} | `modules/hall-ui/public/watch.js:860` |
|
|
289
|
+
| `b9e53fbd90e6` | certain | Could not change standing: {…} | `modules/hall-ui/public/watch.js:886` |
|
|
290
290
|
| `1ef1efc15b6c` | certain | Could not change that just now. | `modules/hall-ui/public/goals-render.js:1235` |
|
|
291
291
|
| `78e18ea59b9f` | certain | Could not close: {…} | `modules/hall-ui/public/harbor.js:136` |
|
|
292
292
|
| `c3f99e7c6e6b` | certain | Could not copy automatically — select and copy manually. ({…}) | `modules/hall-ui/public/settings.js:1017` |
|
|
@@ -310,7 +310,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
310
310
|
| `8b69e445592a` | certain | Could not load the question labels; showing the answers as stored. | `modules/hall-ui/public/idea.js:244` |
|
|
311
311
|
| `edd5bafbe075` | certain | Could not load the queue just now. | `modules/hall-ui/public/ideas.js:565` |
|
|
312
312
|
| `02bb0570996a` | certain | Could not load the ranks. | `modules/hall-ui/public/ranks.js:197` |
|
|
313
|
-
| `2878778cf9f3` | certain | Could not load this page. | `modules/hall-ui/public/watch.js:
|
|
313
|
+
| `2878778cf9f3` | certain | Could not load this page. | `modules/hall-ui/public/watch.js:1357` |
|
|
314
314
|
| `4b0c8d94541c` | certain | Could not load your account. Reload the page to try again. | `modules/hall-ui/public/settings.js:1396` |
|
|
315
315
|
| `2a9fb4046bc4` | certain | Could not load your rank. Return to the hall and try again. | `modules/hall-ui/public/ranks.js:196` |
|
|
316
316
|
| `9d533e811dec` | certain | Could not load your sessions: {…} | `modules/hall-ui/public/settings-sessions.js:114` |
|
|
@@ -348,7 +348,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
348
348
|
| `d4601887a058` | certain | Could not sign out everywhere: {…} | `modules/hall-ui/public/settings-sessions.js:102` |
|
|
349
349
|
| `b1602d01f4bb` | certain | Could not unblock: {…} | `modules/hall-ui/public/harbor.js:187` |
|
|
350
350
|
| `3db890b074a7` | certain | Could not update visibility just now. | `modules/hall-ui/public/goals-render.js:1219` |
|
|
351
|
-
| `112d12b24879` | certain | Could not verify your access. Go back and try again. | `modules/hall-ui/public/watch.js:
|
|
351
|
+
| `112d12b24879` | certain | Could not verify your access. Go back and try again. | `modules/hall-ui/public/watch.js:1356` |
|
|
352
352
|
| `6bd74fd20311` | certain | Could not verify your access. Try again. | `modules/hall-ui/public/sessions.js:280` |
|
|
353
353
|
| `278e8281c82f` | certain | Could not vote: {…} | `modules/hall-ui/public/board-room.js:304` |
|
|
354
354
|
| `a48e9f098392` | certain | Could not {…} just now. | `modules/hall-ui/public/goals-render.js:1205` |
|
|
@@ -691,7 +691,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
691
691
|
| `d9c802f5d85e` | certain | Opening the studio… | `modules/hall-ui/public/studio.html:45` |
|
|
692
692
|
| `569980a263ae` | certain | Optional. This is the ledger link — a copy change ships as a task like every other change. | `modules/hall-ui/public/copy-desk.html:130` |
|
|
693
693
|
| `c78d7388ffd7` | certain | Or just stay in the online terminal | `modules/hall-ui/public/hall-render.js:779` |
|
|
694
|
-
| `d89d1923ecf9` | certain | Over the last {…} days | `modules/hall-ui/public/watch.js:
|
|
694
|
+
| `d89d1923ecf9` | certain | Over the last {…} days | `modules/hall-ui/public/watch.js:1194` |
|
|
695
695
|
| `695ffff13730` | certain | Over time | `modules/hall-ui/public/profile.html:72` |
|
|
696
696
|
| `5ded44da0cc5` | certain | Override requests | `modules/hall-ui/public/watch.html:143` |
|
|
697
697
|
| `15a49bf15007` | certain | Page {…} of {…} | `modules/hall-ui/public/hall-kit.js:112` |
|
|
@@ -737,7 +737,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
737
737
|
| `3f85bdc47099` | certain | Protected module scope — join by request; an Archon admits members | `modules/hall-ui/public/goals-render.js:848` |
|
|
738
738
|
| `e5c292c8b361` | certain | Provides (seams) | `modules/hall-ui/public/atlas.js:288` |
|
|
739
739
|
| `4de232a05888` | certain | Raise after | `modules/hall-ui/public/project-settings.js:113` |
|
|
740
|
-
| `b9c6def6b120` | certain | Rank set to {…}. | `modules/hall-ui/public/watch.js:
|
|
740
|
+
| `b9c6def6b120` | certain | Rank set to {…}. | `modules/hall-ui/public/watch.js:854` |
|
|
741
741
|
| `ff50a1395cbc` | certain | Rank: {…} | `modules/hall-ui/public/profile.js:50` |
|
|
742
742
|
| `6824ec470f99` | certain | Ranks — {{worldName}} | `modules/hall-ui/public/ranks.html:6` |
|
|
743
743
|
| `6824ec470f99` | certain | Ranks — {{worldName}} | `modules/hall-ui/public/ranks.html:6` |
|
|
@@ -745,7 +745,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
745
745
|
| `101a51902f53` | certain | Re-issue failed: {…} | `modules/hall-ui/public/settings.js:1021` |
|
|
746
746
|
| `fae2f90927bd` | certain | Read an idea, then decide. An objection needs a reason and the section it faults. | `modules/hall-ui/public/board-room.html:51` |
|
|
747
747
|
| `b03e17693dcd` | certain | Read it as a table | `modules/hall-ui/public/drachmae.js:148` |
|
|
748
|
-
| `b03e17693dcd` | certain | Read it as a table | `modules/hall-ui/public/watch.js:
|
|
748
|
+
| `b03e17693dcd` | certain | Read it as a table | `modules/hall-ui/public/watch.js:1167` |
|
|
749
749
|
| `b6fcfdd652a7` | certain | Read it here | `modules/hall-ui/public/work.html:45` |
|
|
750
750
|
| `c5f19a888a15` | certain | Read the handoff notes | `modules/hall-ui/public/task.js:441` |
|
|
751
751
|
| `f9e998f4af3d` | certain | Read to the end and the button below unlocks. It is an honour system — nothing here checks your comprehension. | `modules/hall-ui/public/primer.html:100` |
|
|
@@ -798,7 +798,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
798
798
|
| `0887725744f2` | certain | Restricted. | `modules/hall-ui/public/gate.js:219` |
|
|
799
799
|
| `0887725744f2` | certain | Restricted. | `modules/hall-ui/public/government.js:879` |
|
|
800
800
|
| `0887725744f2` | certain | Restricted. | `modules/hall-ui/public/harbor.js:337` |
|
|
801
|
-
| `0887725744f2` | certain | Restricted. | `modules/hall-ui/public/watch.js:
|
|
801
|
+
| `0887725744f2` | certain | Restricted. | `modules/hall-ui/public/watch.js:1306` |
|
|
802
802
|
| `b33dde402434` | certain | Retire this goal — you will be asked what happens to any unfinished work | `modules/hall-ui/public/goals-render.js:927` |
|
|
803
803
|
| `feb48b0d462c` | certain | Return to the Dev Box app and click “Connect” again for a fresh code. | `modules/hall-ui/public/pair.js:32` |
|
|
804
804
|
| `908a0d5d0685` | certain | Review how it judged this work, and teach it what good looked like. | `modules/hall-ui/public/studio.js:181` |
|
|
@@ -884,7 +884,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
884
884
|
| `f79e1fb8ebd4` | certain | Suggested as you type; leave it on "no goal" and it waits for triage. | `modules/hall-ui/public/ideas.html:88` |
|
|
885
885
|
| `1e40f33483eb` | certain | Switch theme | `modules/hall-ui/public/shell.js:461` |
|
|
886
886
|
| `93fa2e97dd35` | certain | System default for this skill | `modules/hall-ui/public/settings.js:74` |
|
|
887
|
-
| `4894b1d9b0de` | certain | System oversight: people, operations and security. | `modules/hall-ui/public/watch.js:
|
|
887
|
+
| `4894b1d9b0de` | certain | System oversight: people, operations and security. | `modules/hall-ui/public/watch.js:1370` |
|
|
888
888
|
| `dfc967a63d57` | certain | Task lifecycle from backlog to shipped, showing states and transitions and the edge where credits are awarded. | `modules/hall-ui/public/diagrams.html:51` |
|
|
889
889
|
| `10cc12f37636` | certain | Task sections | `modules/hall-ui/public/work.html:53` |
|
|
890
890
|
| `0e636ea41b29` | certain | Task status | `modules/hall-ui/public/goals-render.js:340` |
|
|
@@ -905,6 +905,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
905
905
|
| `282c3fa2d456` | certain | That is the wording it already has. Change something, or file a flag instead if you only want to mark it. | `modules/hall-ui/public/copy-desk.js:438` |
|
|
906
906
|
| `c0e28f30524d` | certain | That project's own standing, as reported by that project. | `modules/hall-ui/public/profile.js:314` |
|
|
907
907
|
| `e99d4a811e92` | certain | That request is no longer available. | `modules/hall-ui/public/profile-connections.js:270` |
|
|
908
|
+
| `6e5a324b2972` | certain | That's you | `modules/hall-ui/public/watch.js:803` |
|
|
908
909
|
| `50198c465225` | certain | The 16-value rank enum in three bands, which ranks are live, and the permissions each rank holds. | `modules/hall-ui/public/diagrams.html:59` |
|
|
909
910
|
| `c5e6bdd32fe3` | certain | The Board Room is open to the builders this project’s constitution seats on its board. | `modules/hall-ui/public/board-room.js:436` |
|
|
910
911
|
| `1569b8838862` | certain | The Dev Box app now acts as | `modules/hall-ui/public/pair.js:40` |
|
|
@@ -1087,6 +1088,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1087
1088
|
| `6dadadffa36e` | certain | Your name cannot be blank. | `modules/hall-ui/public/settings.js:832` |
|
|
1088
1089
|
| `bd77210a8c90` | certain | Your name must be {…} characters or fewer. | `modules/hall-ui/public/settings.js:837` |
|
|
1089
1090
|
| `c0a01a95e97e` | certain | Your own idea. You may vote, but it can’t be decided by your vote alone, and a vote on your own idea earns no | `modules/hall-ui/public/board-room.js:274` |
|
|
1091
|
+
| `5e0ec4cd1cfe` | certain | Your own row carries no controls. Nobody can change their own rank or standing — that guard is what stops the | `modules/hall-ui/public/watch.js:835` |
|
|
1090
1092
|
| `8fc605026acf` | certain | Your pending join {…}: {…} — awaiting approval. | `modules/hall-ui/public/goal-inbox.js:88` |
|
|
1091
1093
|
| `e91418db3962` | certain | Your preferences, {…}. | `modules/hall-ui/public/settings.js:856` |
|
|
1092
1094
|
| `e91418db3962` | certain | Your preferences, {…}. | `modules/hall-ui/public/settings.js:1413` |
|
|
@@ -1231,7 +1233,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1231
1233
|
| `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/hall-render.js:623` |
|
|
1232
1234
|
| `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/idea.js:207` |
|
|
1233
1235
|
| `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/ideas.js:476` |
|
|
1234
|
-
| `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/watch.js:
|
|
1236
|
+
| `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/watch.js:1277` |
|
|
1235
1237
|
| `a41659dc2bee` | certain | tasks remaining | `modules/hall-ui/public/roadmap.js:305` |
|
|
1236
1238
|
| `bd3402efa59e` | certain | task{…} shipped | `modules/hall-ui/public/goals-render.js:793` |
|
|
1237
1239
|
| `95854f8315ca` | certain | the page everyone else reads | `modules/hall-ui/public/profile.html:32` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1751,
|
|
7
|
+
"unique_ids": 1515,
|
|
8
|
+
"certain": 1751,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 164
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 80,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1272
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"modules/hall-ui/public/gate.js:219",
|
|
148
148
|
"modules/hall-ui/public/government.js:879",
|
|
149
149
|
"modules/hall-ui/public/harbor.js:337",
|
|
150
|
-
"modules/hall-ui/public/watch.js:
|
|
150
|
+
"modules/hall-ui/public/watch.js:1306"
|
|
151
151
|
]
|
|
152
152
|
},
|
|
153
153
|
{
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"modules/hall-ui/public/hall-render.js:623",
|
|
174
174
|
"modules/hall-ui/public/idea.js:207",
|
|
175
175
|
"modules/hall-ui/public/ideas.js:476",
|
|
176
|
-
"modules/hall-ui/public/watch.js:
|
|
176
|
+
"modules/hall-ui/public/watch.js:1277"
|
|
177
177
|
]
|
|
178
178
|
},
|
|
179
179
|
{
|
|
@@ -1167,7 +1167,7 @@
|
|
|
1167
1167
|
"occurrences": 2,
|
|
1168
1168
|
"at": [
|
|
1169
1169
|
"modules/hall-ui/public/drachmae.js:148",
|
|
1170
|
-
"modules/hall-ui/public/watch.js:
|
|
1170
|
+
"modules/hall-ui/public/watch.js:1167"
|
|
1171
1171
|
]
|
|
1172
1172
|
},
|
|
1173
1173
|
{
|
|
@@ -3584,7 +3584,7 @@
|
|
|
3584
3584
|
"surface": "builders-hall",
|
|
3585
3585
|
"text": "Could not change rank: {…}",
|
|
3586
3586
|
"file": "modules/hall-ui/public/watch.js",
|
|
3587
|
-
"line":
|
|
3587
|
+
"line": 860,
|
|
3588
3588
|
"origin": "js-notice",
|
|
3589
3589
|
"confidence": "certain"
|
|
3590
3590
|
},
|
|
@@ -3593,7 +3593,7 @@
|
|
|
3593
3593
|
"surface": "builders-hall",
|
|
3594
3594
|
"text": "Could not change standing: {…}",
|
|
3595
3595
|
"file": "modules/hall-ui/public/watch.js",
|
|
3596
|
-
"line":
|
|
3596
|
+
"line": 886,
|
|
3597
3597
|
"origin": "js-notice",
|
|
3598
3598
|
"confidence": "certain"
|
|
3599
3599
|
},
|
|
@@ -3809,7 +3809,7 @@
|
|
|
3809
3809
|
"surface": "builders-hall",
|
|
3810
3810
|
"text": "Could not load this page.",
|
|
3811
3811
|
"file": "modules/hall-ui/public/watch.js",
|
|
3812
|
-
"line":
|
|
3812
|
+
"line": 1357,
|
|
3813
3813
|
"origin": "js-notice",
|
|
3814
3814
|
"confidence": "certain"
|
|
3815
3815
|
},
|
|
@@ -4151,7 +4151,7 @@
|
|
|
4151
4151
|
"surface": "builders-hall",
|
|
4152
4152
|
"text": "Could not verify your access. Go back and try again.",
|
|
4153
4153
|
"file": "modules/hall-ui/public/watch.js",
|
|
4154
|
-
"line":
|
|
4154
|
+
"line": 1356,
|
|
4155
4155
|
"origin": "js-text-assign",
|
|
4156
4156
|
"confidence": "certain"
|
|
4157
4157
|
},
|
|
@@ -7238,7 +7238,7 @@
|
|
|
7238
7238
|
"surface": "builders-hall",
|
|
7239
7239
|
"text": "Over the last {…} days",
|
|
7240
7240
|
"file": "modules/hall-ui/public/watch.js",
|
|
7241
|
-
"line":
|
|
7241
|
+
"line": 1194,
|
|
7242
7242
|
"origin": "js-markup",
|
|
7243
7243
|
"confidence": "certain"
|
|
7244
7244
|
},
|
|
@@ -7652,7 +7652,7 @@
|
|
|
7652
7652
|
"surface": "builders-hall",
|
|
7653
7653
|
"text": "Rank set to {…}.",
|
|
7654
7654
|
"file": "modules/hall-ui/public/watch.js",
|
|
7655
|
-
"line":
|
|
7655
|
+
"line": 854,
|
|
7656
7656
|
"origin": "js-notice",
|
|
7657
7657
|
"confidence": "certain"
|
|
7658
7658
|
},
|
|
@@ -7724,7 +7724,7 @@
|
|
|
7724
7724
|
"surface": "builders-hall",
|
|
7725
7725
|
"text": "Read it as a table",
|
|
7726
7726
|
"file": "modules/hall-ui/public/watch.js",
|
|
7727
|
-
"line":
|
|
7727
|
+
"line": 1167,
|
|
7728
7728
|
"origin": "js-markup",
|
|
7729
7729
|
"confidence": "certain"
|
|
7730
7730
|
},
|
|
@@ -8201,7 +8201,7 @@
|
|
|
8201
8201
|
"surface": "builders-hall",
|
|
8202
8202
|
"text": "Restricted.",
|
|
8203
8203
|
"file": "modules/hall-ui/public/watch.js",
|
|
8204
|
-
"line":
|
|
8204
|
+
"line": 1306,
|
|
8205
8205
|
"origin": "js-text-assign",
|
|
8206
8206
|
"confidence": "certain"
|
|
8207
8207
|
},
|
|
@@ -8975,7 +8975,7 @@
|
|
|
8975
8975
|
"surface": "builders-hall",
|
|
8976
8976
|
"text": "System oversight: people, operations and security.",
|
|
8977
8977
|
"file": "modules/hall-ui/public/watch.js",
|
|
8978
|
-
"line":
|
|
8978
|
+
"line": 1370,
|
|
8979
8979
|
"origin": "js-text-assign",
|
|
8980
8980
|
"confidence": "certain"
|
|
8981
8981
|
},
|
|
@@ -9159,6 +9159,15 @@
|
|
|
9159
9159
|
"origin": "js-notice",
|
|
9160
9160
|
"confidence": "certain"
|
|
9161
9161
|
},
|
|
9162
|
+
{
|
|
9163
|
+
"id": "6e5a324b2972",
|
|
9164
|
+
"surface": "builders-hall",
|
|
9165
|
+
"text": "That's you",
|
|
9166
|
+
"file": "modules/hall-ui/public/watch.js",
|
|
9167
|
+
"line": 803,
|
|
9168
|
+
"origin": "js-markup",
|
|
9169
|
+
"confidence": "certain"
|
|
9170
|
+
},
|
|
9162
9171
|
{
|
|
9163
9172
|
"id": "50198c465225",
|
|
9164
9173
|
"surface": "builders-hall",
|
|
@@ -10797,6 +10806,15 @@
|
|
|
10797
10806
|
"origin": "js-markup",
|
|
10798
10807
|
"confidence": "certain"
|
|
10799
10808
|
},
|
|
10809
|
+
{
|
|
10810
|
+
"id": "5e0ec4cd1cfe",
|
|
10811
|
+
"surface": "builders-hall",
|
|
10812
|
+
"text": "Your own row carries no controls. Nobody can change their own rank or standing — that guard is what stops the last {…} locking everyone out — so another {…} does it.",
|
|
10813
|
+
"file": "modules/hall-ui/public/watch.js",
|
|
10814
|
+
"line": 835,
|
|
10815
|
+
"origin": "js-text-assign",
|
|
10816
|
+
"confidence": "certain"
|
|
10817
|
+
},
|
|
10800
10818
|
{
|
|
10801
10819
|
"id": "8fc605026acf",
|
|
10802
10820
|
"surface": "builders-hall",
|
|
@@ -12098,7 +12116,7 @@
|
|
|
12098
12116
|
"surface": "builders-hall",
|
|
12099
12117
|
"text": "task {…}",
|
|
12100
12118
|
"file": "modules/hall-ui/public/watch.js",
|
|
12101
|
-
"line":
|
|
12119
|
+
"line": 1277,
|
|
12102
12120
|
"origin": "js-markup",
|
|
12103
12121
|
"confidence": "certain"
|
|
12104
12122
|
},
|
|
@@ -1887,5 +1887,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1887
1887
|
landed since 1.19.712 with no explicit bump. run 34745667268. (task 1002620)
|
|
1888
1888
|
1.19.714 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1889
1889
|
landed since 1.19.713 with no explicit bump. run 34746102135. (task 1002620)
|
|
1890
|
+
1.19.715 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1891
|
+
landed since 1.19.714 with no explicit bump. run 34747181910. (task 1002620)
|
|
1890
1892
|
---------------------------------------------------------------------------
|
|
1891
1893
|
```
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
/* Row actions inside a ledger cell: ghost pills in a row. */
|
|
22
22
|
.watch-actions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
23
23
|
|
|
24
|
+
/* The reader's own roster row (task 1003720). Nobody may change their own rank
|
|
25
|
+
or standing — the server refuses both outright — so that row states a fact
|
|
26
|
+
where the others carry a control, and a fact is written in the faint ink, not
|
|
27
|
+
painted as a disabled pill. The foot line under the table says why. */
|
|
28
|
+
.watch-self { color: var(--ink-faint); }
|
|
29
|
+
|
|
24
30
|
/* A sealed or failed section says so in place. Three states, three sentences:
|
|
25
31
|
"nothing here" is the kit's empty state, this is "you may not see this" and
|
|
26
32
|
"could not load". */
|
|
@@ -745,6 +745,13 @@
|
|
|
745
745
|
// sits at a rank outside this set, we inject it so the select still shows it.
|
|
746
746
|
const RANK_OPTIONS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
747
747
|
|
|
748
|
+
// Who is reading this roster (task 1003720). `load()` already fetches /me to
|
|
749
|
+
// resolve the page's floor and then threw the id away, so the roster — which
|
|
750
|
+
// returns EVERY builder, the viewer included — could not tell which row was
|
|
751
|
+
// the reader's own. See renderRoster for why that mattered.
|
|
752
|
+
let viewerId = null;
|
|
753
|
+
const isViewer = (b) => viewerId != null && b && b.id != null && String(b.id) === String(viewerId);
|
|
754
|
+
|
|
748
755
|
function renderRoster(data, target) {
|
|
749
756
|
const builders = Array.isArray(data.builders) ? data.builders : [];
|
|
750
757
|
setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
|
|
@@ -762,16 +769,39 @@
|
|
|
762
769
|
],
|
|
763
770
|
pager: makePager('watch-roster', 'otb.watch.rosterPerPage'),
|
|
764
771
|
rowHtml: (b) => {
|
|
772
|
+
// THE VIEWER'S OWN ROW OFFERS NOTHING (task 1003720, re-filed from 1002951
|
|
773
|
+
// — "I cannot press offboard", reported by the owner from Discord).
|
|
774
|
+
//
|
|
775
|
+
// The roster returns every builder, so the reader's own row is in it. Both
|
|
776
|
+
// routes behind that row refuse a self-change OUTRIGHT and always will:
|
|
777
|
+
// PATCH /builders/:id/rank answers cannot_change_own_rank and
|
|
778
|
+
// /builders/:id/status answers cannot_change_own_status — the guard that
|
|
779
|
+
// stops the last Archon demoting or offboarding themselves and locking
|
|
780
|
+
// everyone out. The row painted a live rank <select> and a live Offboard
|
|
781
|
+
// pill anyway, so from the owner's seat the page carried a control that
|
|
782
|
+
// could only ever answer with a raw refusal slug in a toast. That is what
|
|
783
|
+
// "I cannot press offboard" describes.
|
|
784
|
+
//
|
|
785
|
+
// This is not a SOFT gate (the primer's acknowledge control, which opens
|
|
786
|
+
// once you have read enough and so stays in the tab order as
|
|
787
|
+
// aria-disabled) — it is an action that can never open for this reader.
|
|
788
|
+
// So the row renders no control at all and the section's foot line says
|
|
789
|
+
// why, rather than painting a dead one.
|
|
790
|
+
const self = isViewer(b);
|
|
765
791
|
const opts = RANK_OPTIONS.slice();
|
|
766
792
|
if (b.rank && !opts.includes(b.rank)) opts.unshift(b.rank);
|
|
767
|
-
const
|
|
768
|
-
`<
|
|
769
|
-
|
|
770
|
-
|
|
793
|
+
const rankCell = self
|
|
794
|
+
? `<span class="watch-self">${escapeHtml(rankLabel(b.rank))}</span>`
|
|
795
|
+
: `<select data-roster-rank data-id="${escapeHtml(b.id)}" data-current="${escapeHtml(b.rank || '')}" aria-label="Rank">` +
|
|
796
|
+
opts.map((rk) => `<option value="${escapeHtml(rk)}"${rk === b.rank ? ' selected' : ''}>${escapeHtml(rankLabel(rk))}</option>`).join('') +
|
|
797
|
+
'</select>';
|
|
771
798
|
const isInactive = String(b.status) === 'inactive';
|
|
772
799
|
const statusBtn = isInactive
|
|
773
800
|
? ghost(`data-roster-status="active" data-id="${escapeHtml(b.id)}"`, 'Reactivate')
|
|
774
801
|
: ghost(`data-roster-status="inactive" data-id="${escapeHtml(b.id)}"`, 'Offboard', true);
|
|
802
|
+
const actionCell = self
|
|
803
|
+
? '<span class="watch-self">That\'s you</span>'
|
|
804
|
+
: `<div class="watch-actions">${statusBtn}</div>`;
|
|
775
805
|
const claims = num(b.active_claim_count);
|
|
776
806
|
const staleFlag = b.stale_claim ? ' <span class="badge badge--sev-medium">stale</span>' : '';
|
|
777
807
|
// task 1467: the roster name links to the builder's public profile page.
|
|
@@ -784,17 +814,28 @@
|
|
|
784
814
|
`<td>${rosterNameCell}` +
|
|
785
815
|
(b.github_login ? ` <span class="ov-mono">@${escapeHtml(b.github_login)}</span>` : '') +
|
|
786
816
|
'</td>' +
|
|
787
|
-
`<td>${
|
|
817
|
+
`<td>${rankCell}</td>` +
|
|
788
818
|
`<td class="num">${fmtInt(b.total_credits)}</td>` +
|
|
789
819
|
`<td class="num">${fmtInt(b.karma)}</td>` +
|
|
790
820
|
`<td class="num${b.stale_claim ? ' warn' : ''}">${fmtInt(claims)}${staleFlag}</td>` +
|
|
791
821
|
`<td><span class="badge badge--status">${escapeHtml(b.status || '—')}</span></td>` +
|
|
792
|
-
`<td
|
|
822
|
+
`<td>${actionCell}</td>` +
|
|
793
823
|
'</tr>'
|
|
794
824
|
);
|
|
795
825
|
},
|
|
796
826
|
}).render(builders);
|
|
797
827
|
|
|
828
|
+
// The one asymmetry in the table, stated once under it rather than repeated
|
|
829
|
+
// on the row. Appended as an ELEMENT, not with `innerHTML +=`: re-parsing the
|
|
830
|
+
// body would drop the pager's own wiring. The rank is written from
|
|
831
|
+
// rankLabel(), never a hardcoded "Archon", so the floor cannot drift.
|
|
832
|
+
if (builders.some(isViewer)) {
|
|
833
|
+
const note = document.createElement('p');
|
|
834
|
+
note.className = 'ov-foot';
|
|
835
|
+
note.textContent = `Your own row carries no controls. Nobody can change their own rank or standing — that guard is what stops the last ${rankLabel('archon')} locking everyone out — so another ${rankLabel('archon')} does it.`;
|
|
836
|
+
target.appendChild(note);
|
|
837
|
+
}
|
|
838
|
+
|
|
798
839
|
// Rank change via the <select>. Bespoke: a 'change' listener that rolls the
|
|
799
840
|
// select back to its previous value on failure, delegated so a page flip
|
|
800
841
|
// cannot strand a row without one.
|
|
@@ -1305,6 +1346,11 @@
|
|
|
1305
1346
|
if (!res.ok) throw new Error(`me → ${res.status}`);
|
|
1306
1347
|
const payload = res.data;
|
|
1307
1348
|
builder = (payload && payload.builder) ? payload.builder : payload;
|
|
1349
|
+
// Kept for the roster, which must know which row is the reader's own
|
|
1350
|
+
// (task 1003720). Null when /me answered without one — isViewer then
|
|
1351
|
+
// matches nothing, so the roster degrades to the old every-row-acts shape
|
|
1352
|
+
// rather than marking the wrong row as yours.
|
|
1353
|
+
viewerId = (builder && builder.id != null) ? builder.id : null;
|
|
1308
1354
|
} catch (err) {
|
|
1309
1355
|
console.error('[watch] failed to read viewer', err);
|
|
1310
1356
|
$('#watch-sub').textContent = 'Could not verify your access. Go back and try again.';
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.715",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.715",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.715",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -295,10 +295,15 @@ async function applyPlan(pool, plans) {
|
|
|
295
295
|
[p.builderId, r.taskId, delta, BACKFILL_REASON,
|
|
296
296
|
`${r.description} (net-capped ${delta} of ${r.effective}; builder cost-plus already paid ${p.costPlusAlreadyPaid})`]
|
|
297
297
|
);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
// NO `UPDATE builders SET total_credits = ...` HERE, DELIBERATELY.
|
|
299
|
+
// trg_credit_log_apply applies the delta (migrations/003_pms.sql:169 —
|
|
300
|
+
// pms_apply_credit, AFTER INSERT ON credit_log, total_credits += NEW.delta),
|
|
301
|
+
// so the INSERT above has ALREADY moved the balance. This function used to
|
|
302
|
+
// add it a second time, doubling every credit it minted — a real
|
|
303
|
+
// financial-ledger bug (task 1003641). Every other credit_log writer in the
|
|
304
|
+
// repo relies on the trigger alone; backfill-subagent-rewards.js is the
|
|
305
|
+
// sibling exemplar. If you are adding a balance update here, the trigger is
|
|
306
|
+
// the thing to change, once, not this call site.
|
|
302
307
|
remaining -= delta;
|
|
303
308
|
mintedRows++;
|
|
304
309
|
mintedDrachmae += delta;
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.715'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// tests/credit_log_single_writer.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1003641 — the balance is moved by the TRIGGER, and by nothing else.
|
|
4
|
+
//
|
|
5
|
+
// THE BUG. scripts/gds/backfill-task-rewards.js applyPlan() did both: INSERT INTO
|
|
6
|
+
// credit_log (which fires trg_credit_log_apply AFTER INSERT) *and* a separate
|
|
7
|
+
// `UPDATE builders SET total_credits = total_credits + $1`. The trigger had already
|
|
8
|
+
// applied that delta, so every credit the script minted landed twice. A real
|
|
9
|
+
// financial-ledger bug, and the kind that is invisible in review because each half
|
|
10
|
+
// looks correct on its own.
|
|
11
|
+
//
|
|
12
|
+
// WHY THIS FILE IS NOT JUST A TEST OF THAT ONE LINE. The invariant is repo-wide:
|
|
13
|
+
// exactly one place applies a credit_log delta to builders.total_credits. Every
|
|
14
|
+
// other writer already relied on the trigger; only the backfill script double-wrote.
|
|
15
|
+
// So the durable guard is "no credit_log writer also updates the balance", which
|
|
16
|
+
// catches the NEXT script someone writes as well as this one.
|
|
17
|
+
//
|
|
18
|
+
// The existing tests/backfill_task_rewards.mjs never referenced applyPlan,
|
|
19
|
+
// total_credits or the trigger at all, which is how this survived. Source
|
|
20
|
+
// assertions, because the DB-free gate cannot run SQL; runtime behaviour is proven
|
|
21
|
+
// against real Postgres at deploy.
|
|
22
|
+
//
|
|
23
|
+
// Run: node tests/credit_log_single_writer.mjs
|
|
24
|
+
|
|
25
|
+
import { strict as assert } from 'node:assert';
|
|
26
|
+
import { test } from 'node:test';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
33
|
+
// Comments carry the rationale; strip them before asserting on the executable body
|
|
34
|
+
// so a comment ABOUT the bug can never satisfy an assertion against it.
|
|
35
|
+
const code = (src) => src
|
|
36
|
+
.replace(/^\s*\/\/.*$/gm, '')
|
|
37
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
38
|
+
|
|
39
|
+
// Every file in the repo that inserts a credit_log row.
|
|
40
|
+
const WRITERS = [
|
|
41
|
+
['scripts', 'gds', 'backfill-task-rewards.js'],
|
|
42
|
+
['scripts', 'gds', 'backfill-subagent-rewards.js'],
|
|
43
|
+
['modules', 'economy', 'achievements.js'],
|
|
44
|
+
['modules', 'economy', 'credits.js'],
|
|
45
|
+
['modules', 'lifecycle', 'db-rank-authz.js'],
|
|
46
|
+
['src', 'bongos', 'db.js'],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
test('the trigger is the ONE place a credit_log delta reaches total_credits', () => {
|
|
50
|
+
const mig = read('migrations', '003_pms.sql');
|
|
51
|
+
assert.match(mig, /CREATE OR REPLACE FUNCTION pms_apply_credit\(\)/);
|
|
52
|
+
assert.match(mig, /SET total_credits = total_credits \+ NEW\.delta/,
|
|
53
|
+
'the trigger applies the delta');
|
|
54
|
+
assert.match(mig, /AFTER INSERT ON credit_log/,
|
|
55
|
+
'on insert, so any INSERT has already moved the balance');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('no credit_log writer ALSO updates builders.total_credits', () => {
|
|
59
|
+
// The invariant. A writer that does both double-credits, silently.
|
|
60
|
+
const offenders = [];
|
|
61
|
+
for (const parts of WRITERS) {
|
|
62
|
+
const src = code(read(...parts));
|
|
63
|
+
if (!/INSERT INTO credit_log/i.test(src)) continue; // moved on; not our business
|
|
64
|
+
// Match an ASSIGNMENT, not a mention. `UPDATE builders SET rank = $2 ...
|
|
65
|
+
// RETURNING id, ..., total_credits, ...` READS the column and must not trip
|
|
66
|
+
// this — two files do exactly that, and an earlier draft of this test flagged
|
|
67
|
+
// both as double-writers. The two shapes that actually move a balance are the
|
|
68
|
+
// self-referential increment and a direct set.
|
|
69
|
+
const writesBalance = /\btotal_credits\s*=\s*total_credits/i.test(src)
|
|
70
|
+
|| /SET\s+total_credits\s*=/i.test(src);
|
|
71
|
+
if (writesBalance) offenders.push(parts.join('/'));
|
|
72
|
+
}
|
|
73
|
+
assert.deepEqual(offenders, [],
|
|
74
|
+
`these insert a credit_log row AND move the balance themselves, so every credit lands twice: ${offenders.join(', ')}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('backfill-task-rewards.js specifically — the regression that cost real credits', () => {
|
|
78
|
+
const src = read('scripts', 'gds', 'backfill-task-rewards.js');
|
|
79
|
+
assert.match(src, /INSERT INTO credit_log/, 'it still mints the audit row');
|
|
80
|
+
assert.doesNotMatch(code(src), /\btotal_credits\s*=/i,
|
|
81
|
+
'and must never assign total_credits — the trigger owns the balance');
|
|
82
|
+
// The reason WHY must survive too, or the next reader re-adds it.
|
|
83
|
+
assert.match(src, /trg_credit_log_apply applies the delta/,
|
|
84
|
+
'the call site explains why there is no balance update here');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('the idempotency key still guards re-runs', () => {
|
|
88
|
+
// Removing the double-write must not disturb the other half of this script's
|
|
89
|
+
// safety: a second run of the same backfill must mint nothing.
|
|
90
|
+
const src = code(read('scripts', 'gds', 'backfill-task-rewards.js'));
|
|
91
|
+
assert.match(src, /SELECT 1 FROM credit_log WHERE task_id = \$1 AND reason = \$2/,
|
|
92
|
+
'the per-task idempotency re-check inside the transaction');
|
|
93
|
+
assert.match(src, /BACKFILL_REASON/, 'keyed on the backfill reason');
|
|
94
|
+
});
|
package/tests/helpers.mjs
CHANGED
|
@@ -330,6 +330,14 @@ export function makeEl(tagName, attrs = {}) {
|
|
|
330
330
|
while (node) { if (matches(node, sel)) return node; node = node.parentNode; }
|
|
331
331
|
return null;
|
|
332
332
|
},
|
|
333
|
+
// Node.contains — true for the node itself and any descendant. A DELEGATED
|
|
334
|
+
// listener guards on it (`!root.contains(el)`), so without this a test that
|
|
335
|
+
// dispatches on the section throws instead of exercising the handler.
|
|
336
|
+
contains(node) {
|
|
337
|
+
let n = node;
|
|
338
|
+
while (n) { if (n === this) return true; n = n.parentNode; }
|
|
339
|
+
return false;
|
|
340
|
+
},
|
|
333
341
|
addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); },
|
|
334
342
|
focus() {},
|
|
335
343
|
fire(type, event) { (this.listeners[type] || []).forEach((fn) => fn(event)); },
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// tests/watch_roster_self_row.mjs — the reader's OWN row on the Watch roster,
|
|
2
|
+
// EXECUTED (task 1003720, re-filed from task 1002951).
|
|
3
|
+
//
|
|
4
|
+
// The bug, as the owner reported it from Discord #bugs: "I cannot press
|
|
5
|
+
// offboard". GET /builders/roster returns EVERY builder — the reader included —
|
|
6
|
+
// and the row painted a live rank <select> and a live Offboard pill on all of
|
|
7
|
+
// them. But both routes behind that row refuse a self-change categorically and
|
|
8
|
+
// always will:
|
|
9
|
+
//
|
|
10
|
+
// PATCH /builders/:id/rank → 403 cannot_change_own_rank
|
|
11
|
+
// PATCH /builders/:id/status → 403 cannot_change_own_status
|
|
12
|
+
//
|
|
13
|
+
// (src/bongos/routes/builders.js — the guard that stops the last Archon demoting
|
|
14
|
+
// or offboarding themselves and locking everyone out.) res.fail() defaults the
|
|
15
|
+
// envelope's message to the CODE, so pressing your own Offboard produced a toast
|
|
16
|
+
// reading "Could not change standing: cannot_change_own_status" and nothing else.
|
|
17
|
+
// From the owner's seat that is a button that does not work.
|
|
18
|
+
//
|
|
19
|
+
// The page now renders no control on that row and states why once under the
|
|
20
|
+
// table. These are assertions about what a human sees and clicks, so this runs
|
|
21
|
+
// the real watch.js inside `vm` against the real watch.html, with a makeLedger
|
|
22
|
+
// that actually calls rowHtml — a source-text grep cannot see which row got
|
|
23
|
+
// which cell, and the no-op ledger stub in tests/watch_applications_queue.mjs
|
|
24
|
+
// renders no roster rows at all.
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/watch_roster_self_row.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import vm from 'node:vm';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { makeRunner, makeEl, parseInto } from './helpers.mjs';
|
|
34
|
+
|
|
35
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
37
|
+
const SRC = fs.readFileSync(path.join(HALL, 'watch.js'), 'utf8');
|
|
38
|
+
const PAGE = fs.readFileSync(path.join(HALL, 'watch.html'), 'utf8');
|
|
39
|
+
|
|
40
|
+
const { test, summary } = makeRunner();
|
|
41
|
+
|
|
42
|
+
const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g,
|
|
43
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
44
|
+
|
|
45
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
46
|
+
async function settle() { for (let i = 0; i < 40; i++) await tick(); }
|
|
47
|
+
|
|
48
|
+
const ok = (data) => ({ status: 200, ok: true, data });
|
|
49
|
+
|
|
50
|
+
// The branding pack renames every rank (the neutral pack calls archon "Owner"),
|
|
51
|
+
// which is why a hardcoded rank word in page copy is banned. This stub renames
|
|
52
|
+
// them too, so a test for "the foot line does not spell Archon" is real.
|
|
53
|
+
const RANK_LABELS = { xenos: 'Newcomer', thetes: 'Contributor', metic: 'Trusted', archon: 'Owner' };
|
|
54
|
+
const rankLabel = (r) => RANK_LABELS[r] || String(r || '');
|
|
55
|
+
|
|
56
|
+
// The viewer is builder 1. Rows as GET /builders/roster returns them — note the
|
|
57
|
+
// viewer sits in the MIDDLE, which is where the live roster puts the owner: the
|
|
58
|
+
// order is inactive-last, then still-onboarding, then created_at DESC.
|
|
59
|
+
const VIEWER_ID = '1';
|
|
60
|
+
const ROSTER = [
|
|
61
|
+
{ id: '90', display_name: 'Alpha', github_login: 'alpha', rank: 'archon', total_credits: 9200, karma: 4, active_claim_count: 1, stale_claim: false, status: 'active' },
|
|
62
|
+
{ id: '1', display_name: 'The Owner', github_login: 'theowner', rank: 'archon', total_credits: 6896, karma: 68, active_claim_count: 2, stale_claim: false, status: 'active' },
|
|
63
|
+
{ id: '92', display_name: 'Gamma', github_login: 'gamma', rank: 'thetes', total_credits: 830, karma: 1, active_claim_count: 0, stale_claim: false, status: 'active' },
|
|
64
|
+
{ id: '93', display_name: 'Delta', github_login: 'delta', rank: 'xenos', total_credits: 40, karma: 0, active_claim_count: 0, stale_claim: false, status: 'inactive' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// ===========================================================================
|
|
68
|
+
// Boot the real page against a scripted API and hand back handles.
|
|
69
|
+
// ===========================================================================
|
|
70
|
+
|
|
71
|
+
async function boot(opts = {}) {
|
|
72
|
+
const {
|
|
73
|
+
builders = ROSTER,
|
|
74
|
+
me = { id: VIEWER_ID, rank: 'archon', github_login: 'theowner' },
|
|
75
|
+
patchResult = null,
|
|
76
|
+
} = opts;
|
|
77
|
+
|
|
78
|
+
const requests = [];
|
|
79
|
+
const toasts = [];
|
|
80
|
+
const prompts = [];
|
|
81
|
+
|
|
82
|
+
const api = {
|
|
83
|
+
async request(method, p, init) {
|
|
84
|
+
const body = init && init.body;
|
|
85
|
+
requests.push({ method, path: p, body });
|
|
86
|
+
const route = String(p).replace('/api/bongos', '').split('?')[0];
|
|
87
|
+
if (method === 'GET' && route === '/me') return ok({ builder: me });
|
|
88
|
+
if (method === 'GET' && route === '/builders/roster') return ok({ builders });
|
|
89
|
+
if (method === 'PATCH' && route.startsWith('/builders/')) {
|
|
90
|
+
return patchResult || ok({ builder: { id: route.split('/')[2] } });
|
|
91
|
+
}
|
|
92
|
+
// Every other panel on this page loads independently and degrades on its
|
|
93
|
+
// own; an unscripted shape is exactly the failure `section` swallows.
|
|
94
|
+
return ok({});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const documentObj = {
|
|
99
|
+
readyState: 'complete',
|
|
100
|
+
body: makeEl('body'),
|
|
101
|
+
documentElement: makeEl('html'),
|
|
102
|
+
createElement: (tag) => makeEl(tag),
|
|
103
|
+
getElementById(id) { return this.body.querySelector(`#${id}`); },
|
|
104
|
+
querySelector(sel) { return this.body.querySelector(sel); },
|
|
105
|
+
querySelectorAll(sel) { return this.body.querySelectorAll(sel); },
|
|
106
|
+
addEventListener() {},
|
|
107
|
+
};
|
|
108
|
+
parseInto(documentObj.body, PAGE);
|
|
109
|
+
|
|
110
|
+
const locationObj = { hostname: 'builders.example.test', pathname: '/builders/watch', search: '', hash: '', assign() {} };
|
|
111
|
+
|
|
112
|
+
const kit = {
|
|
113
|
+
makeTabs: () => ({}),
|
|
114
|
+
// A ledger that REALLY renders: the whole question here is which row got
|
|
115
|
+
// which cell, and a no-op render answers none of it.
|
|
116
|
+
makeLedger: ({ mount, columns, rowHtml }) => ({
|
|
117
|
+
render(rows) {
|
|
118
|
+
mount.innerHTML =
|
|
119
|
+
'<table class="ledger"><thead><tr>' +
|
|
120
|
+
columns.map((c) => `<th>${escapeHtml(typeof c === 'string' ? c : c.label)}</th>`).join('') +
|
|
121
|
+
'</tr></thead><tbody>' + rows.map((r) => rowHtml(r)).join('') + '</tbody></table>';
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
makePager: () => ({ page: 1, perPage: 25 }),
|
|
125
|
+
emptyStateHtml: (msg) => `<div class="empty-state"><p>${escapeHtml(msg)}</p></div>`,
|
|
126
|
+
makeFilterBar: (o) => { o.mount.innerHTML = '<div class="filters"></div>'; return { el: o.mount, values: () => ({}) }; },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const windowObj = {
|
|
130
|
+
__BRANDING__: { project: { joinability: 'apply' } },
|
|
131
|
+
OTB: {
|
|
132
|
+
escapeHtml,
|
|
133
|
+
$: (sel) => documentObj.querySelector(sel),
|
|
134
|
+
num: (v) => Number(v),
|
|
135
|
+
fmtInt: (n) => String(n),
|
|
136
|
+
fmtCompact: (n) => String(n),
|
|
137
|
+
fmtDate: (iso) => String(iso).slice(0, 10),
|
|
138
|
+
rankLabel,
|
|
139
|
+
toast: (m) => toasts.push(m),
|
|
140
|
+
},
|
|
141
|
+
OTBKit: kit,
|
|
142
|
+
OTBBranding: { currencyLabel: () => 'Credits' },
|
|
143
|
+
BongosClient: { createClient: () => api },
|
|
144
|
+
location: locationObj,
|
|
145
|
+
prompt: (msg) => { prompts.push(msg); return 'left for a new job'; },
|
|
146
|
+
addEventListener() {},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const sandbox = {
|
|
150
|
+
window: windowObj, document: documentObj, location: locationObj,
|
|
151
|
+
console: { log() {}, warn() {}, error() {} },
|
|
152
|
+
setTimeout, clearTimeout, Promise, Date, Math, JSON, Error, isNaN,
|
|
153
|
+
Number, String, Object, Array, Boolean, Set, Map, RegExp,
|
|
154
|
+
encodeURIComponent, decodeURIComponent, parseInt, parseFloat,
|
|
155
|
+
};
|
|
156
|
+
sandbox.globalThis = sandbox;
|
|
157
|
+
vm.createContext(sandbox);
|
|
158
|
+
vm.runInContext(SRC, sandbox, { filename: 'watch.js' });
|
|
159
|
+
await settle();
|
|
160
|
+
|
|
161
|
+
const rosterBody = () => documentObj.getElementById('roster-body');
|
|
162
|
+
const rows = () => rosterBody().querySelectorAll('tr').filter((tr) => tr.querySelectorAll('td').length);
|
|
163
|
+
const rowFor = (login) => rows().find((tr) => tr.querySelectorAll('td')[0].textContent.includes('@' + login));
|
|
164
|
+
const patches = () => requests.filter((r) => r.method === 'PATCH');
|
|
165
|
+
const footText = () => rosterBody().querySelectorAll('p').map((p) => p.textContent).join(' ');
|
|
166
|
+
|
|
167
|
+
return { requests, toasts, prompts, documentObj, rosterBody, rows, rowFor, patches, footText, settle };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cells = (tr) => tr.querySelectorAll('td');
|
|
171
|
+
|
|
172
|
+
// ===========================================================================
|
|
173
|
+
// 1. The viewer's own row offers neither control.
|
|
174
|
+
// ===========================================================================
|
|
175
|
+
|
|
176
|
+
await test('the viewer\'s own row renders no Offboard button', async () => {
|
|
177
|
+
const h = await boot();
|
|
178
|
+
const mine = h.rowFor('theowner');
|
|
179
|
+
assert.ok(mine, 'the roster did not render the viewer\'s own row');
|
|
180
|
+
assert.equal(mine.querySelectorAll('[data-roster-status]').length, 0,
|
|
181
|
+
'the viewer\'s own row still carries a status button the server refuses outright');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await test('the viewer\'s own row renders no rank select', async () => {
|
|
185
|
+
const h = await boot();
|
|
186
|
+
const mine = h.rowFor('theowner');
|
|
187
|
+
assert.equal(mine.querySelectorAll('[data-roster-rank]').length, 0,
|
|
188
|
+
'the viewer\'s own row still carries a rank select the server refuses outright');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await test('the own row still SHOWS the rank and says whose row it is', async () => {
|
|
192
|
+
const h = await boot();
|
|
193
|
+
const c = cells(h.rowFor('theowner'));
|
|
194
|
+
assert.equal(c[1].textContent.trim(), 'Owner', 'the own row lost the rank it should still state');
|
|
195
|
+
assert.match(c[6].textContent, /you/i, 'the own row\'s action cell says nothing about why it is empty');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ===========================================================================
|
|
199
|
+
// 2. Every OTHER row keeps both controls — the fix must not disarm the roster.
|
|
200
|
+
// ===========================================================================
|
|
201
|
+
|
|
202
|
+
await test('every other row keeps its Offboard / Reactivate button', async () => {
|
|
203
|
+
const h = await boot();
|
|
204
|
+
for (const login of ['alpha', 'gamma', 'delta']) {
|
|
205
|
+
const tr = h.rowFor(login);
|
|
206
|
+
assert.equal(tr.querySelectorAll('[data-roster-status]').length, 1,
|
|
207
|
+
`row @${login} lost its status control`);
|
|
208
|
+
}
|
|
209
|
+
// The inactive builder offers the OTHER verb, as before.
|
|
210
|
+
assert.equal(h.rowFor('delta').querySelector('[data-roster-status]').getAttribute('data-roster-status'), 'active');
|
|
211
|
+
assert.equal(h.rowFor('alpha').querySelector('[data-roster-status]').getAttribute('data-roster-status'), 'inactive');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await test('every other row keeps its rank select', async () => {
|
|
215
|
+
const h = await boot();
|
|
216
|
+
for (const login of ['alpha', 'gamma', 'delta']) {
|
|
217
|
+
assert.equal(h.rowFor(login).querySelectorAll('[data-roster-rank]').length, 1,
|
|
218
|
+
`row @${login} lost its rank select`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await test('another builder\'s Offboard still reaches the route', async () => {
|
|
223
|
+
const h = await boot();
|
|
224
|
+
const btn = h.rowFor('gamma').querySelector('[data-roster-status]');
|
|
225
|
+
// The roster DELEGATES: the listener is on #roster-body, not on the button, so
|
|
226
|
+
// a list that pages cannot strand a row without one. The DOM stub's fire()
|
|
227
|
+
// does not bubble, so firing on the button reaches nothing — dispatch on the
|
|
228
|
+
// section and name the button as the target, the way a real click arrives.
|
|
229
|
+
h.rosterBody().fire('click', { target: btn });
|
|
230
|
+
await h.settle();
|
|
231
|
+
const p = h.patches();
|
|
232
|
+
assert.equal(p.length, 1, `expected one PATCH, got ${p.length}`);
|
|
233
|
+
assert.match(p[0].path, /\/builders\/92\/status$/);
|
|
234
|
+
assert.equal(p[0].body.status, 'inactive');
|
|
235
|
+
assert.equal(p[0].body.reason, 'left for a new job', 'the offboarding reason was dropped');
|
|
236
|
+
assert.equal(h.prompts.length, 1, 'the reason prompt no longer runs');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ===========================================================================
|
|
240
|
+
// 3. The foot line says why, in the pack's own words.
|
|
241
|
+
// ===========================================================================
|
|
242
|
+
|
|
243
|
+
await test('a foot line under the roster explains the empty row', async () => {
|
|
244
|
+
const h = await boot();
|
|
245
|
+
assert.match(h.footText(), /your own row carries no controls/i,
|
|
246
|
+
'nothing under the roster explains why one row has no controls');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await test('the foot line names the rank from the pack, never a hardcoded "Archon"', async () => {
|
|
250
|
+
const h = await boot();
|
|
251
|
+
const foot = h.footText();
|
|
252
|
+
assert.match(foot, /Owner/, 'the foot line does not name the rank through rankLabel()');
|
|
253
|
+
assert.ok(!/archon/i.test(foot), `the foot line hardcodes a rank word: ${foot}`);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await test('no foot line when the viewer is not on the roster', async () => {
|
|
257
|
+
// A Metic reading the roster of a project they are not listed on: there is no
|
|
258
|
+
// own row, so there is nothing to explain.
|
|
259
|
+
const h = await boot({ builders: ROSTER.filter((b) => b.id !== VIEWER_ID) });
|
|
260
|
+
assert.ok(!/your own row/i.test(h.footText()), 'the foot line rendered with no own row on the table');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ===========================================================================
|
|
264
|
+
// 4. Degrade: an unidentifiable viewer must not mark the WRONG row.
|
|
265
|
+
// ===========================================================================
|
|
266
|
+
|
|
267
|
+
await test('no id from /me leaves every row exactly as it was', async () => {
|
|
268
|
+
const h = await boot({ me: { rank: 'archon', github_login: 'theowner' } });
|
|
269
|
+
assert.equal(h.rows().length, ROSTER.length);
|
|
270
|
+
for (const b of ROSTER) {
|
|
271
|
+
assert.equal(h.rowFor(b.github_login).querySelectorAll('[data-roster-status]').length, 1,
|
|
272
|
+
`row @${b.github_login} was disarmed although the viewer could not be identified`);
|
|
273
|
+
}
|
|
274
|
+
assert.ok(!/your own row/i.test(h.footText()));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await test('the match is by value, not by type — a numeric id still matches', async () => {
|
|
278
|
+
// node-postgres hands bigints back as TEXT, so /me may answer with a string
|
|
279
|
+
// where the roster answers with a number (or the reverse). A `===` comparison
|
|
280
|
+
// here is the same bug the /rank route's own guard was fixed for in #348.
|
|
281
|
+
const h = await boot({
|
|
282
|
+
me: { id: 1, rank: 'archon', github_login: 'theowner' },
|
|
283
|
+
builders: ROSTER.map((b) => (b.id === VIEWER_ID ? { ...b, id: '1' } : b)),
|
|
284
|
+
});
|
|
285
|
+
assert.equal(h.rowFor('theowner').querySelectorAll('[data-roster-status]').length, 0,
|
|
286
|
+
'a numeric /me id did not match the roster\'s string id — the own row is armed again');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
summary('watch roster — the viewer\'s own row');
|