@bongos/core 1.19.504 → 1.19.505
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 +20 -15
- package/docs/copy-inventory.md +12 -12
- package/docs/copy-registry.json +12 -12
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/task-detail.js +9 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/promote_goal_id.mjs +4 -1
- package/tests/task_detail_ui.mjs +710 -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.505",
|
|
6
|
+
"core_contract": "1.19.505",
|
|
7
|
+
"source_commit": "3ab4e5ad43e20da1374af1e120d51911fd799680",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-03T17:
|
|
9
|
+
"built_at": "2026-09-03T17:32:35.678Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 427,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1944,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2395,
|
|
20
|
+
"tree_sha256": "1bc57733b6267db17eb8b78650effda973d902370c46f3be51c0764e1858de00",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1727,12 +1727,12 @@
|
|
|
1727
1727
|
{
|
|
1728
1728
|
"path": "docs/copy-inventory.md",
|
|
1729
1729
|
"mode": "0000644",
|
|
1730
|
-
"sha256": "
|
|
1730
|
+
"sha256": "276273f5edb4e920ce7ffdf33707b3e08c259ab22a45427ebde2f4f1bfdc2831"
|
|
1731
1731
|
},
|
|
1732
1732
|
{
|
|
1733
1733
|
"path": "docs/copy-registry.json",
|
|
1734
1734
|
"mode": "0000644",
|
|
1735
|
-
"sha256": "
|
|
1735
|
+
"sha256": "b6bf1dcc9d8b2bbef68d5ff7b8f2e2b9a3e5d2d504562c21cacd0e3b9a0e9905"
|
|
1736
1736
|
},
|
|
1737
1737
|
{
|
|
1738
1738
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2577,7 +2577,7 @@
|
|
|
2577
2577
|
{
|
|
2578
2578
|
"path": "docs/module-api-changelog.md",
|
|
2579
2579
|
"mode": "0000644",
|
|
2580
|
-
"sha256": "
|
|
2580
|
+
"sha256": "6e57f6714292cf1ab9df6ec0c481390848d08ee35f399719ec5833d2699ed8bd"
|
|
2581
2581
|
},
|
|
2582
2582
|
{
|
|
2583
2583
|
"path": "docs/modules-contract.md",
|
|
@@ -5052,7 +5052,7 @@
|
|
|
5052
5052
|
{
|
|
5053
5053
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
5054
5054
|
"mode": "0000644",
|
|
5055
|
-
"sha256": "
|
|
5055
|
+
"sha256": "5a88cd76fb6da7058ba109c3467f11c20c973daff8b03f4ed1b4be67b8807a87"
|
|
5056
5056
|
},
|
|
5057
5057
|
{
|
|
5058
5058
|
"path": "modules/hall-ui/public/task-lib.js",
|
|
@@ -7227,12 +7227,12 @@
|
|
|
7227
7227
|
{
|
|
7228
7228
|
"path": "package-lock.json",
|
|
7229
7229
|
"mode": "0000644",
|
|
7230
|
-
"sha256": "
|
|
7230
|
+
"sha256": "5b46ca655ee12c47c36c6d2e05baea5d10bdf139404a931c041225e7e0343426"
|
|
7231
7231
|
},
|
|
7232
7232
|
{
|
|
7233
7233
|
"path": "package.json",
|
|
7234
7234
|
"mode": "0000644",
|
|
7235
|
-
"sha256": "
|
|
7235
|
+
"sha256": "54330892dd305c453d6e4f8b18051ba2116277c101651f912da36c07b15e314c"
|
|
7236
7236
|
},
|
|
7237
7237
|
{
|
|
7238
7238
|
"path": "public-docs/index.html",
|
|
@@ -8902,7 +8902,7 @@
|
|
|
8902
8902
|
{
|
|
8903
8903
|
"path": "src/module-api.js",
|
|
8904
8904
|
"mode": "0000644",
|
|
8905
|
-
"sha256": "
|
|
8905
|
+
"sha256": "da3ceeaa7c239b0ec879e2724b32c30d3efae1ef2440d5d8defa0a729f18c69d"
|
|
8906
8906
|
},
|
|
8907
8907
|
{
|
|
8908
8908
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11097,7 +11097,7 @@
|
|
|
11097
11097
|
{
|
|
11098
11098
|
"path": "tests/promote_goal_id.mjs",
|
|
11099
11099
|
"mode": "0000644",
|
|
11100
|
-
"sha256": "
|
|
11100
|
+
"sha256": "29d69119eb019e74e5f5407855080c908820f06fca70b93146694d4b57d1e25d"
|
|
11101
11101
|
},
|
|
11102
11102
|
{
|
|
11103
11103
|
"path": "tests/prose_edits.mjs",
|
|
@@ -11794,6 +11794,11 @@
|
|
|
11794
11794
|
"mode": "0000644",
|
|
11795
11795
|
"sha256": "1e4d480e64d915be750d5bb9581f2da83767800daa7769a551103bc00ca79497"
|
|
11796
11796
|
},
|
|
11797
|
+
{
|
|
11798
|
+
"path": "tests/task_detail_ui.mjs",
|
|
11799
|
+
"mode": "0000644",
|
|
11800
|
+
"sha256": "5e7150f8651a3c147b17f51800f5f8afa1e9534b3ec545584e5c37792475b2aa"
|
|
11801
|
+
},
|
|
11797
11802
|
{
|
|
11798
11803
|
"path": "tests/task_goal_link.mjs",
|
|
11799
11804
|
"mode": "0000644",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -337,7 +337,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
337
337
|
| `28ec06f6060a` | certain | Counted, not judged: each question is worth the same, and earns its share in proportion to how much real text | `modules/hall-ui/public/ideas.html:167` |
|
|
338
338
|
| `38827a954f3b` | certain | Create a custom rank first. | `modules/hall-ui/public/government.js:466` |
|
|
339
339
|
| `dd8ead7adc9a` | certain | Create rank | `modules/hall-ui/public/government.html:94` |
|
|
340
|
-
| `d7beb841395e` | certain | Create task | `modules/hall-ui/public/task-detail.js:
|
|
340
|
+
| `d7beb841395e` | certain | Create task | `modules/hall-ui/public/task-detail.js:372` |
|
|
341
341
|
| `533f24219487` | certain | Create your | `modules/hall-ui/public/hall-render.js:744` |
|
|
342
342
|
| `e91a4d2e3e1c` | certain | Created rank {…}. | `modules/hall-ui/public/government.js:449` |
|
|
343
343
|
| `a55e423263f0` | certain | Cross-project record | `modules/hall-ui/public/profile.html:94` |
|
|
@@ -382,7 +382,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
382
382
|
| `8d2b4f119ee3` | certain | End session | `modules/hall-ui/public/settings-sessions.js:46` |
|
|
383
383
|
| `8e82771e5b63` | certain | Ending a session takes effect immediately; the token stops working on its very next request. | `modules/hall-ui/public/settings.html:174` |
|
|
384
384
|
| `58d381535285` | certain | Engagement reports | `modules/hall-ui/public/watch.html:133` |
|
|
385
|
-
| `bd7a40639a1c` | certain | Estimate (min) | `modules/hall-ui/public/task-detail.js:
|
|
385
|
+
| `bd7a40639a1c` | certain | Estimate (min) | `modules/hall-ui/public/task-detail.js:367` |
|
|
386
386
|
| `c15d1f0ddfb6` | certain | Estimation accuracy | `modules/hall-ui/public/watch.html:151` |
|
|
387
387
|
| `44f928d13699` | certain | Event sounds | `modules/hall-ui/public/settings.html:328` |
|
|
388
388
|
| `8690c6424585` | certain | Every builder in {{worldName}} — rank, {{currency}}, karma, and active claims. | `modules/hall-ui/public/watch.html:69` |
|
|
@@ -418,7 +418,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
418
418
|
| `448d00771dc8` | certain | Flagged. It is in the queue. | `modules/hall-ui/public/copy-desk.js:391` |
|
|
419
419
|
| `6149e9ca69f7` | certain | Fleet summary | `modules/hall-ui/public/harbor.html:44` |
|
|
420
420
|
| `617f13fa3fdb` | certain | Flip the switch. | `modules/hall-ui/public/hall-render.js:744` |
|
|
421
|
-
| `42cee5cb3584` | certain | Fold this idea into # | `modules/hall-ui/public/task-detail.js:
|
|
421
|
+
| `42cee5cb3584` | certain | Fold this idea into # | `modules/hall-ui/public/task-detail.js:462` |
|
|
422
422
|
| `647bcfcfdb03` | certain | Fresh CLI token issued. Paste it in your terminal: | `modules/hall-ui/public/settings.js:988` |
|
|
423
423
|
| `55c71d76a94c` | certain | From creation to production — and the exact step where {{currency}} are awarded (at | `modules/hall-ui/public/diagrams.html:52` |
|
|
424
424
|
| `d32c5e058d1e` | certain | From the system | `modules/hall-ui/public/hall-render.js:623` |
|
|
@@ -538,7 +538,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
538
538
|
| `ceec2218efe3` | certain | Loading the roadmap… | `modules/hall-ui/public/roadmap.html:33` |
|
|
539
539
|
| `db04803d9e72` | certain | Loading the roster… | `modules/hall-ui/public/roster.html:39` |
|
|
540
540
|
| `0cdad42009e6` | certain | Loading the task | `modules/hall-ui/public/task.html:46` |
|
|
541
|
-
| `d28d321e8a1e` | certain | Loading versions… | `modules/hall-ui/public/task-detail.js:
|
|
541
|
+
| `d28d321e8a1e` | certain | Loading versions… | `modules/hall-ui/public/task-detail.js:325` |
|
|
542
542
|
| `3c43f157035e` | certain | Loading your goals… | `modules/hall-ui/public/goals.js:95` |
|
|
543
543
|
| `7a239a17afc8` | certain | Loading your preferences… | `modules/hall-ui/public/settings.html:32` |
|
|
544
544
|
| `04df1bd30129` | certain | Loading your rank… | `modules/hall-ui/public/ranks.html:30` |
|
|
@@ -609,7 +609,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
609
609
|
| `2c19f41704b9` | certain | Not yet available — {{worldName}} | `modules/hall-ui/public/not-ready.html:6` |
|
|
610
610
|
| `2c19f41704b9` | certain | Not yet available — {{worldName}} | `modules/hall-ui/public/not-ready.html:6` |
|
|
611
611
|
| `571b9b22dbda` | certain | Not yet ready — blocked by dependencies or awaiting promotion. These become claimable when their blockers ship | `modules/hall-ui/public/work.html:98` |
|
|
612
|
-
| `0c59d118db9b` | certain | Note (optional) | `modules/hall-ui/public/task-detail.js:
|
|
612
|
+
| `0c59d118db9b` | certain | Note (optional) | `modules/hall-ui/public/task-detail.js:465` |
|
|
613
613
|
| `cd652d04d616` | certain | Nothing changes yet. This files a task carrying your wording; whoever claims it lands the diff, and it ships g | `modules/hall-ui/public/copy-desk.html:168` |
|
|
614
614
|
| `fec5d78497e8` | certain | Nothing in the backlog right now. | `modules/hall-ui/public/work.js:1055` |
|
|
615
615
|
| `2c6e0dfbd781` | certain | Nothing is waiting on you. Requests reach you by your craft — set yours in | `modules/hall-ui/public/collab.html:44` |
|
|
@@ -699,7 +699,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
699
699
|
| `26e6c9879035` | certain | Reading the diagrams | `modules/hall-ui/public/diagrams.html:39` |
|
|
700
700
|
| `81cd2cdf954d` | certain | Reading the request… | `modules/hall-ui/public/pair.html:28` |
|
|
701
701
|
| `5b581cca590f` | certain | Ready — tasks to do | `modules/hall-ui/public/work.html:64` |
|
|
702
|
-
| `9ea556e7b78b` | certain | Reason (optional) | `modules/hall-ui/public/task-detail.js:
|
|
702
|
+
| `9ea556e7b78b` | certain | Reason (optional) | `modules/hall-ui/public/task-detail.js:516` |
|
|
703
703
|
| `7df3fd4ebf41` | certain | Reason (shown to the builder) | `modules/hall-ui/public/harbor.js:148` |
|
|
704
704
|
| `4f619c19a8e7` | certain | Rebase owed | `modules/hall-ui/public/hall-render.js:603` |
|
|
705
705
|
| `06cbba1440b1` | certain | Rebase owed — new claims paused | `modules/hall-ui/public/hall-render.js:488` |
|
|
@@ -1074,7 +1074,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1074
1074
|
| `06aab18fc3b5` | certain | holds the repo. Follow the numbered path to see how a credential-less box gets its work to prod. This is the l | `modules/hall-ui/public/diagrams.html:74` |
|
|
1075
1075
|
| `17ab8db79e1f` | certain | holds the worker, the code, and your local memory; the | `modules/hall-ui/public/diagrams.html:74` |
|
|
1076
1076
|
| `7d5cbe3d81e4` | certain | how long a vote stays open | `modules/hall-ui/public/government.html:55` |
|
|
1077
|
-
| `dacb901c4f10` | certain | idea number | `modules/hall-ui/public/task-detail.js:
|
|
1077
|
+
| `dacb901c4f10` | certain | idea number | `modules/hall-ui/public/task-detail.js:463` |
|
|
1078
1078
|
| `7df22e74c87a` | certain | idea {…} | `modules/hall-ui/public/idea.js:210` |
|
|
1079
1079
|
| `7df22e74c87a` | certain | idea {…} | `modules/hall-ui/public/ideas.js:348` |
|
|
1080
1080
|
| `2123a62d69e3` | certain | if this stays empty. | `modules/hall-ui/public/collab.html:45` |
|
|
@@ -1090,7 +1090,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1090
1090
|
| `c767b3e07826` | certain | is only the screen; the | `modules/hall-ui/public/diagrams.html:74` |
|
|
1091
1091
|
| `aa530bd15088` | certain | itself). The registry stays the single source of truth for what the hall shows; a fitness check (scripts/gds/f | `modules/hall-ui/public/index.html:53` |
|
|
1092
1092
|
| `9018bf6e99eb` | certain | last used {…} · signed in {…} | `modules/hall-ui/public/settings-sessions.js:46` |
|
|
1093
|
-
| `2ce7738b1eb0` | certain | loading goals… | `modules/hall-ui/public/task-detail.js:
|
|
1093
|
+
| `2ce7738b1eb0` | certain | loading goals… | `modules/hall-ui/public/task-detail.js:389` |
|
|
1094
1094
|
| `754bdec368ed` | certain | lowest credits | `modules/hall-ui/public/hall-widgets.js:136` |
|
|
1095
1095
|
| `cffba8409070` | certain | manage its visibility in Settings → Cross-project record | `modules/hall-ui/public/profile.js:261` |
|
|
1096
1096
|
| `5c3fdd34cd02` | certain | means expressible through the real configuration — a rank-based membership plus a pass rule — with no new code | `modules/hall-ui/public/government.js:721` |
|
|
@@ -1173,8 +1173,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1173
1173
|
| `e72c90163726` | certain | what's blocking | `modules/hall-ui/public/goals-render.js:161` |
|
|
1174
1174
|
| `81f05d9b2bbf` | certain | what's wrong, or what you'd like to see | `modules/hall-ui/public/ideas.html:69` |
|
|
1175
1175
|
| `1651cb1876f5` | certain | who sits on the board | `modules/hall-ui/public/government.html:55` |
|
|
1176
|
-
| `143f77e2b782` | certain | why these are the same | `modules/hall-ui/public/task-detail.js:
|
|
1177
|
-
| `ec7c228cb4ce` | certain | why this isn’t worth doing | `modules/hall-ui/public/task-detail.js:
|
|
1176
|
+
| `143f77e2b782` | certain | why these are the same | `modules/hall-ui/public/task-detail.js:466` |
|
|
1177
|
+
| `ec7c228cb4ce` | certain | why this isn’t worth doing | `modules/hall-ui/public/task-detail.js:517` |
|
|
1178
1178
|
| `66d979d0db89` | certain | your vote | `modules/hall-ui/public/government.js:819` |
|
|
1179
1179
|
| `50d4a70a2eb2` | certain | {…} Access is invite-based: an Archon approves each new builder. To begin, request access below. | `modules/hall-ui/public/hall-render.js:278` |
|
|
1180
1180
|
| `9aac280c89ec` | certain | {…} Anyone with a GitHub account can join: sign in and you're in, starting at the newcomer rank. | `modules/hall-ui/public/hall-render.js:261` |
|
|
@@ -1236,7 +1236,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1236
1236
|
| `4a5ff4f662df` | certain | — ${…} of ${…} this month ({…}%). {…} | `modules/hall-ui/public/hall-render.js:455` |
|
|
1237
1237
|
| `a6ae9cbdee30` | certain | — all builders — | `modules/hall-ui/public/builders.js:671` |
|
|
1238
1238
|
| `729d4ce22b99` | certain | — any — | `modules/hall-ui/public/hall-widgets.js:132` |
|
|
1239
|
-
| `9ac518dfa95e` | certain | — none (the version's general bucket) — | `modules/hall-ui/public/task-detail.js:
|
|
1239
|
+
| `9ac518dfa95e` | certain | — none (the version's general bucket) — | `modules/hall-ui/public/task-detail.js:320` |
|
|
1240
1240
|
| `da9962dc1729` | certain | — or open any diagram fullscreen. | `modules/hall-ui/public/diagrams.html:33` |
|
|
1241
1241
|
| `d4ff85363f89` | certain | — so if Claude Desktop then says | `modules/hall-ui/public/box-panel.js:160` |
|
|
1242
1242
|
| `e5c5e94152b3` | certain | — that's all the tooling you need up front. | `modules/hall-ui/public/hall-render.js:811` |
|
|
@@ -1254,7 +1254,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1254
1254
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:195` |
|
|
1255
1255
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:207` |
|
|
1256
1256
|
| `1e7cf7b1e64e` | certain | ← Newer | `modules/hall-ui/public/sessions.html:68` |
|
|
1257
|
-
| `2180e0ecf138` | certain | → no idea # | `modules/hall-ui/public/task-detail.js:
|
|
1257
|
+
| `2180e0ecf138` | certain | → no idea # | `modules/hall-ui/public/task-detail.js:484` |
|
|
1258
1258
|
| `d5d76f1c0dfe` | certain | ○ reserved | `modules/hall-ui/public/diagrams.html:44` |
|
|
1259
1259
|
| `97b0773e72a5` | certain | ● LIVE | `modules/hall-ui/public/diagrams.html:42` |
|
|
1260
1260
|
| `1375334c77f6` | certain | ⚠ This box is running | `modules/hall-ui/public/box-panel.js:202` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3900,7 +3900,7 @@
|
|
|
3900
3900
|
"surface": "builders-hall",
|
|
3901
3901
|
"text": "Create task",
|
|
3902
3902
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
3903
|
-
"line":
|
|
3903
|
+
"line": 372,
|
|
3904
3904
|
"origin": "js-markup",
|
|
3905
3905
|
"confidence": "certain"
|
|
3906
3906
|
},
|
|
@@ -4305,7 +4305,7 @@
|
|
|
4305
4305
|
"surface": "builders-hall",
|
|
4306
4306
|
"text": "Estimate (min)",
|
|
4307
4307
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
4308
|
-
"line":
|
|
4308
|
+
"line": 367,
|
|
4309
4309
|
"origin": "js-markup",
|
|
4310
4310
|
"confidence": "certain"
|
|
4311
4311
|
},
|
|
@@ -4629,7 +4629,7 @@
|
|
|
4629
4629
|
"surface": "builders-hall",
|
|
4630
4630
|
"text": "Fold this idea into #",
|
|
4631
4631
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
4632
|
-
"line":
|
|
4632
|
+
"line": 462,
|
|
4633
4633
|
"origin": "js-markup",
|
|
4634
4634
|
"confidence": "certain"
|
|
4635
4635
|
},
|
|
@@ -5709,7 +5709,7 @@
|
|
|
5709
5709
|
"surface": "builders-hall",
|
|
5710
5710
|
"text": "Loading versions…",
|
|
5711
5711
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
5712
|
-
"line":
|
|
5712
|
+
"line": 325,
|
|
5713
5713
|
"origin": "js-markup",
|
|
5714
5714
|
"confidence": "certain"
|
|
5715
5715
|
},
|
|
@@ -6348,7 +6348,7 @@
|
|
|
6348
6348
|
"surface": "builders-hall",
|
|
6349
6349
|
"text": "Note (optional)",
|
|
6350
6350
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
6351
|
-
"line":
|
|
6351
|
+
"line": 465,
|
|
6352
6352
|
"origin": "js-markup",
|
|
6353
6353
|
"confidence": "certain"
|
|
6354
6354
|
},
|
|
@@ -7158,7 +7158,7 @@
|
|
|
7158
7158
|
"surface": "builders-hall",
|
|
7159
7159
|
"text": "Reason (optional)",
|
|
7160
7160
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
7161
|
-
"line":
|
|
7161
|
+
"line": 516,
|
|
7162
7162
|
"origin": "js-markup",
|
|
7163
7163
|
"confidence": "certain"
|
|
7164
7164
|
},
|
|
@@ -10533,7 +10533,7 @@
|
|
|
10533
10533
|
"surface": "builders-hall",
|
|
10534
10534
|
"text": "idea number",
|
|
10535
10535
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
10536
|
-
"line":
|
|
10536
|
+
"line": 463,
|
|
10537
10537
|
"origin": "js-markup",
|
|
10538
10538
|
"confidence": "certain"
|
|
10539
10539
|
},
|
|
@@ -10677,7 +10677,7 @@
|
|
|
10677
10677
|
"surface": "builders-hall",
|
|
10678
10678
|
"text": "loading goals…",
|
|
10679
10679
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
10680
|
-
"line":
|
|
10680
|
+
"line": 389,
|
|
10681
10681
|
"origin": "js-markup",
|
|
10682
10682
|
"confidence": "certain"
|
|
10683
10683
|
},
|
|
@@ -11424,7 +11424,7 @@
|
|
|
11424
11424
|
"surface": "builders-hall",
|
|
11425
11425
|
"text": "why these are the same",
|
|
11426
11426
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
11427
|
-
"line":
|
|
11427
|
+
"line": 466,
|
|
11428
11428
|
"origin": "js-markup",
|
|
11429
11429
|
"confidence": "certain"
|
|
11430
11430
|
},
|
|
@@ -11433,7 +11433,7 @@
|
|
|
11433
11433
|
"surface": "builders-hall",
|
|
11434
11434
|
"text": "why this isn’t worth doing",
|
|
11435
11435
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
11436
|
-
"line":
|
|
11436
|
+
"line": 517,
|
|
11437
11437
|
"origin": "js-markup",
|
|
11438
11438
|
"confidence": "certain"
|
|
11439
11439
|
},
|
|
@@ -11991,7 +11991,7 @@
|
|
|
11991
11991
|
"surface": "builders-hall",
|
|
11992
11992
|
"text": "— none (the version's general bucket) —",
|
|
11993
11993
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
11994
|
-
"line":
|
|
11994
|
+
"line": 320,
|
|
11995
11995
|
"origin": "js-markup",
|
|
11996
11996
|
"confidence": "certain"
|
|
11997
11997
|
},
|
|
@@ -12153,7 +12153,7 @@
|
|
|
12153
12153
|
"surface": "builders-hall",
|
|
12154
12154
|
"text": "→ no idea #",
|
|
12155
12155
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
12156
|
-
"line":
|
|
12156
|
+
"line": 484,
|
|
12157
12157
|
"origin": "js-text-assign",
|
|
12158
12158
|
"confidence": "certain"
|
|
12159
12159
|
},
|
|
@@ -1445,5 +1445,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1445
1445
|
landed since 1.19.502 with no explicit bump. run 33783963947. (task 1002620)
|
|
1446
1446
|
1.19.504 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1447
1447
|
landed since 1.19.503 with no explicit bump. run 33784387586. (task 1002620)
|
|
1448
|
+
1.19.505 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1449
|
+
landed since 1.19.504 with no explicit bump. run 33785121177. (task 1002620)
|
|
1448
1450
|
---------------------------------------------------------------------------
|
|
1449
1451
|
```
|
|
@@ -271,11 +271,16 @@
|
|
|
271
271
|
// triage path: the STORED hint (idea.suggested_goal_id, written by
|
|
272
272
|
// inbox.captureIdea precisely so triage inherits the guess) wins; the R07
|
|
273
273
|
// suggester only fills in when there is no hint. Returns option HTML.
|
|
274
|
-
|
|
274
|
+
// `allowHint` scopes the stored hint to the version it was filed against. A
|
|
275
|
+
// goal belongs to exactly ONE version and POST /tasks 400s `bad_goal_id` on a
|
|
276
|
+
// goal from another one, so carrying the hint across a version change offered
|
|
277
|
+
// — pre-selected — the single option guaranteed to fail (found by
|
|
278
|
+
// tests/task_detail_ui.mjs, task 1003516).
|
|
279
|
+
async function goalOptionsFor(versionId, allowHint) {
|
|
275
280
|
// Number() so a junk stored value cannot reach the DOM as value="NaN" and
|
|
276
281
|
// then be POSTed as a goal id.
|
|
277
282
|
var raw = Number(idea.suggested_goal_id);
|
|
278
|
-
var hint = Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
283
|
+
var hint = allowHint && Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
279
284
|
var suggestions = [];
|
|
280
285
|
// The suggester is a convenience, so an outage degrades to "no suggestions"
|
|
281
286
|
// and must not stop a triage verdict — the hint and the opt-out below are
|
|
@@ -348,7 +353,7 @@
|
|
|
348
353
|
// Fetched for the PREFERRED version; the change listener below re-fetches,
|
|
349
354
|
// because goals belong to a version and showing another version's goals
|
|
350
355
|
// would offer a home the server would reject.
|
|
351
|
-
var goalOptions = await goalOptionsFor(preferred);
|
|
356
|
+
var goalOptions = await goalOptionsFor(preferred, true);
|
|
352
357
|
var priOptions = [1, 2, 3, 4, 5].map(function (p) {
|
|
353
358
|
return '<option value="' + p + '"' + (p === pri ? ' selected' : '') + '>P' + p + '</option>';
|
|
354
359
|
}).join('');
|
|
@@ -382,7 +387,7 @@
|
|
|
382
387
|
var mine = ++goalFetch;
|
|
383
388
|
gsel.disabled = true;
|
|
384
389
|
gsel.innerHTML = '<option value="">loading goals…</option>';
|
|
385
|
-
var html = await goalOptionsFor(vsel.value);
|
|
390
|
+
var html = await goalOptionsFor(vsel.value, vsel.value === preferred);
|
|
386
391
|
if (mine !== goalFetch) return;
|
|
387
392
|
gsel.innerHTML = html;
|
|
388
393
|
gsel.disabled = false;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.505",
|
|
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.505",
|
|
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.505",
|
|
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",
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.505'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -294,7 +294,10 @@ await test('DEGRADES: the hall re-fetches goals when the version changes, newest
|
|
|
294
294
|
// Goals belong to a version; keeping stale options would offer a home the
|
|
295
295
|
// server would reject.
|
|
296
296
|
assert.match(HALL, /vsel\.addEventListener\('change'/);
|
|
297
|
-
|
|
297
|
+
// The second argument (task 1003516) scopes the filed goal hint to the version
|
|
298
|
+
// it was filed against — matched loosely so this pins the re-fetch, not the
|
|
299
|
+
// argument list.
|
|
300
|
+
assert.match(HALL, /await goalOptionsFor\(vsel\.value/);
|
|
298
301
|
});
|
|
299
302
|
|
|
300
303
|
await test('the CLI uses the GENERATED typed client method, not the raw escape hatch', () => {
|
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
// tests/task_detail_ui.mjs — the hall's idea overlay, EXECUTED (task 1003516).
|
|
2
|
+
//
|
|
3
|
+
// modules/hall-ui/public/task-detail.js carries the whole in-hall idea-triage
|
|
4
|
+
// surface: the Promote / Merge / Discard disclosure forms (task 1002447), the
|
|
5
|
+
// version picker, and the goal picker with its hint-beats-suggester precedence
|
|
6
|
+
// and out-of-order-fetch race guard (task 1003071). Until this file its only
|
|
7
|
+
// test asserted on the SOURCE TEXT, so a ReferenceError inside a submit handler
|
|
8
|
+
// passed the suite and 500'd at click time — the class of gap behind the task
|
|
9
|
+
// 1003102 blocker. Browser code fails when a human clicks, and only a harness
|
|
10
|
+
// that clicks can see that.
|
|
11
|
+
//
|
|
12
|
+
// Why a vm harness rather than jsdom: there is no jsdom in this repo. The
|
|
13
|
+
// precedent is tests/goal_criterion_confirm_ui.mjs (after tests/hall_signout.mjs),
|
|
14
|
+
// which runs the real page script inside `vm` under a hand-built DOM stub. This
|
|
15
|
+
// stub goes one step further than those: it PARSES the markup the file writes
|
|
16
|
+
// into innerHTML, so `querySelector('[data-goal-select]')` resolves only if the
|
|
17
|
+
// element is genuinely there, form fields carry the real values the file
|
|
18
|
+
// rendered (escaping included), and every handler runs for real.
|
|
19
|
+
//
|
|
20
|
+
// Covers:
|
|
21
|
+
// • the overlay boots from the hash and renders the Metic triage panel
|
|
22
|
+
// • promote with a goal chosen — POST /tasks carries goal_id, the idea is then
|
|
23
|
+
// PATCHed promoted, and the panel links the created task
|
|
24
|
+
// • promote opting out — NO goal_id key at all (the server's catch-all bucket
|
|
25
|
+
// applies only when the key is absent)
|
|
26
|
+
// • the version change re-fetches that version's goals and repaints the picker;
|
|
27
|
+
// a raced pair of changes lets only the newest write
|
|
28
|
+
// • the two-write failure path — the task is created, the idea PATCH fails, and
|
|
29
|
+
// the created task id survives in the message the builder reads
|
|
30
|
+
// • merge and discard submit their PATCH, announce, and close the overlay
|
|
31
|
+
// • a 403 is read out of the NESTED error envelope rather than swallowed
|
|
32
|
+
//
|
|
33
|
+
// Run: node tests/task_detail_ui.mjs
|
|
34
|
+
|
|
35
|
+
import { strict as assert } from 'node:assert';
|
|
36
|
+
import fs from 'node:fs';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
import vm from 'node:vm';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
import { makeRunner } from './helpers.mjs';
|
|
41
|
+
|
|
42
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/task-detail.js'), 'utf8');
|
|
44
|
+
|
|
45
|
+
const { test, summary } = makeRunner();
|
|
46
|
+
|
|
47
|
+
// ===========================================================================
|
|
48
|
+
// A minimal DOM that actually parses what the file writes.
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
|
|
51
|
+
const VOID_TAGS = new Set(['input', 'br', 'img', 'hr', 'meta', 'link']);
|
|
52
|
+
|
|
53
|
+
function decodeEntities(s) {
|
|
54
|
+
return String(s)
|
|
55
|
+
.replace(/</g, '<').replace(/>/g, '>')
|
|
56
|
+
.replace(/"/g, '"').replace(/'/g, "'")
|
|
57
|
+
.replace(/&/g, '&');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseAttrs(text) {
|
|
61
|
+
const out = {};
|
|
62
|
+
const re = /([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = re.exec(text))) {
|
|
65
|
+
const raw = m[2] !== undefined ? m[2] : m[3] !== undefined ? m[3] : m[4] !== undefined ? m[4] : '';
|
|
66
|
+
out[m[1].toLowerCase()] = decodeEntities(raw);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Compound selectors only (tag / .class / #id / [attr] / [attr="v"], comma lists)
|
|
72
|
+
// — that is every selector task-detail.js uses, and refusing anything richer
|
|
73
|
+
// keeps the stub honest about what it can prove.
|
|
74
|
+
function matchesOne(el, sel) {
|
|
75
|
+
let ok = true;
|
|
76
|
+
const re = /([a-zA-Z][\w-]*)|\.([\w-]+)|#([\w-]+)|\[([\w:-]+)(?:=["']([^"']*)["'])?\]/g;
|
|
77
|
+
let m;
|
|
78
|
+
while ((m = re.exec(sel))) {
|
|
79
|
+
if (m[1]) ok = ok && el.tagName === m[1].toLowerCase();
|
|
80
|
+
else if (m[2]) ok = ok && String(el.className || '').split(/\s+/).includes(m[2]);
|
|
81
|
+
else if (m[3]) ok = ok && el.id === m[3];
|
|
82
|
+
else if (m[4]) {
|
|
83
|
+
const have = el.attrs[m[4].toLowerCase()];
|
|
84
|
+
ok = ok && (m[5] === undefined ? have !== undefined : have === m[5]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return ok;
|
|
88
|
+
}
|
|
89
|
+
const matches = (el, sel) => String(sel).split(',').map((s) => s.trim()).filter(Boolean).some((s) => matchesOne(el, s));
|
|
90
|
+
|
|
91
|
+
function walk(el, fn) {
|
|
92
|
+
for (const child of el.children) {
|
|
93
|
+
const hit = fn(child);
|
|
94
|
+
if (hit) return hit;
|
|
95
|
+
const deeper = walk(child, fn);
|
|
96
|
+
if (deeper) return deeper;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function makeEl(tagName, attrs = {}) {
|
|
102
|
+
const el = {
|
|
103
|
+
tagName: String(tagName).toLowerCase(),
|
|
104
|
+
attrs,
|
|
105
|
+
children: [],
|
|
106
|
+
parentNode: null,
|
|
107
|
+
className: attrs.class || '',
|
|
108
|
+
id: attrs.id || '',
|
|
109
|
+
name: attrs.name,
|
|
110
|
+
dataset: {},
|
|
111
|
+
disabled: 'disabled' in attrs,
|
|
112
|
+
style: {},
|
|
113
|
+
listeners: {},
|
|
114
|
+
_text: '',
|
|
115
|
+
_html: null,
|
|
116
|
+
_value: null,
|
|
117
|
+
|
|
118
|
+
get value() {
|
|
119
|
+
if (this._value !== null) return this._value;
|
|
120
|
+
if (this.tagName === 'select') {
|
|
121
|
+
const opts = this.querySelectorAll('option');
|
|
122
|
+
const chosen = opts.find((o) => 'selected' in o.attrs) || opts[0];
|
|
123
|
+
return chosen ? (chosen.attrs.value !== undefined ? chosen.attrs.value : chosen.textContent) : '';
|
|
124
|
+
}
|
|
125
|
+
return this.attrs.value !== undefined ? this.attrs.value : '';
|
|
126
|
+
},
|
|
127
|
+
set value(v) { this._value = String(v); },
|
|
128
|
+
|
|
129
|
+
get textContent() { return this._text + this.children.map((c) => c.textContent).join(''); },
|
|
130
|
+
set textContent(v) { this._text = String(v == null ? '' : v); this.children = []; this._html = null; },
|
|
131
|
+
|
|
132
|
+
get innerHTML() { return this._html === null ? '' : this._html; },
|
|
133
|
+
set innerHTML(v) { this._html = String(v); this._value = null; parseInto(this, this._html); },
|
|
134
|
+
|
|
135
|
+
setAttribute(k, v) { this.attrs[String(k).toLowerCase()] = String(v); },
|
|
136
|
+
getAttribute(k) { const v = this.attrs[String(k).toLowerCase()]; return v === undefined ? null : v; },
|
|
137
|
+
removeAttribute(k) { delete this.attrs[String(k).toLowerCase()]; },
|
|
138
|
+
appendChild(child) { child.parentNode = this; this.children.push(child); return child; },
|
|
139
|
+
remove() {
|
|
140
|
+
const p = this.parentNode;
|
|
141
|
+
if (!p) return;
|
|
142
|
+
p.children = p.children.filter((c) => c !== this);
|
|
143
|
+
this.parentNode = null;
|
|
144
|
+
},
|
|
145
|
+
querySelector(sel) { return walk(this, (c) => (matches(c, sel) ? c : null)); },
|
|
146
|
+
querySelectorAll(sel) {
|
|
147
|
+
const out = [];
|
|
148
|
+
walk(this, (c) => { if (matches(c, sel)) out.push(c); return null; });
|
|
149
|
+
return out;
|
|
150
|
+
},
|
|
151
|
+
closest(sel) {
|
|
152
|
+
let node = this;
|
|
153
|
+
while (node) { if (matches(node, sel)) return node; node = node.parentNode; }
|
|
154
|
+
return null;
|
|
155
|
+
},
|
|
156
|
+
addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); },
|
|
157
|
+
focus() {},
|
|
158
|
+
fire(type, event) { (this.listeners[type] || []).forEach((fn) => fn(event)); },
|
|
159
|
+
};
|
|
160
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
161
|
+
if (!k.startsWith('data-')) continue;
|
|
162
|
+
el.dataset[k.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = v;
|
|
163
|
+
}
|
|
164
|
+
return el;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The reserved names a <form>'s named-field shorthand (form.title.value) must
|
|
168
|
+
// never shadow — everything else on the element is fair game, same as the DOM.
|
|
169
|
+
const FORM_FIELD_RESERVED = new Set(['tagName', 'attrs', 'children', 'parentNode', 'className', 'id', 'dataset',
|
|
170
|
+
'disabled', 'style', 'listeners', 'value', 'textContent', 'innerHTML', 'name']);
|
|
171
|
+
|
|
172
|
+
function parseInto(host, html) {
|
|
173
|
+
host.children = [];
|
|
174
|
+
host._text = '';
|
|
175
|
+
const stack = [host];
|
|
176
|
+
const re = /<(\/?)([a-zA-Z][\w-]*)([^>]*?)(\/?)>/g;
|
|
177
|
+
let last = 0;
|
|
178
|
+
let m;
|
|
179
|
+
const addText = (raw) => { if (raw) stack[stack.length - 1]._text += decodeEntities(raw); };
|
|
180
|
+
while ((m = re.exec(html))) {
|
|
181
|
+
addText(html.slice(last, m.index));
|
|
182
|
+
last = re.lastIndex;
|
|
183
|
+
const tag = m[2].toLowerCase();
|
|
184
|
+
if (m[1]) {
|
|
185
|
+
for (let i = stack.length - 1; i > 0; i--) if (stack[i].tagName === tag) { stack.length = i; break; }
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const el = makeEl(tag, parseAttrs(m[3]));
|
|
189
|
+
stack[stack.length - 1].appendChild(el);
|
|
190
|
+
if (!m[4] && !VOID_TAGS.has(tag)) stack.push(el);
|
|
191
|
+
}
|
|
192
|
+
addText(html.slice(last));
|
|
193
|
+
// Named-field access on every form the markup just produced.
|
|
194
|
+
walk(host, (el) => {
|
|
195
|
+
if (el.tagName !== 'form') return null;
|
|
196
|
+
for (const field of el.querySelectorAll('input, select, textarea')) {
|
|
197
|
+
if (!field.name || FORM_FIELD_RESERVED.has(field.name) || field.name in el) continue;
|
|
198
|
+
el[field.name] = field;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ===========================================================================
|
|
205
|
+
// The harness: run the real file against a scripted API and hand back handles.
|
|
206
|
+
// ===========================================================================
|
|
207
|
+
|
|
208
|
+
const IDEA_ID = 1000817;
|
|
209
|
+
const DEFAULT_IDEA = {
|
|
210
|
+
id: IDEA_ID,
|
|
211
|
+
status: 'open',
|
|
212
|
+
title: 'Cover the overlay\'s "promote" & <merge> handlers',
|
|
213
|
+
body_md: 'The overlay has no executing test.',
|
|
214
|
+
kind: 'blocker-resolution',
|
|
215
|
+
suggested_version: 'BV2',
|
|
216
|
+
suggested_priority: 2,
|
|
217
|
+
suggested_goal_id: 1000071,
|
|
218
|
+
};
|
|
219
|
+
const DEFAULT_VERSIONS = [
|
|
220
|
+
{ id: 'BV1', name: 'Platform one', status: 'planning' },
|
|
221
|
+
{ id: 'BV2', name: 'Platform two', status: 'active' },
|
|
222
|
+
{ id: 'OLD', name: 'Already out', status: 'shipped' },
|
|
223
|
+
];
|
|
224
|
+
const DEFAULT_SUGGESTIONS = {
|
|
225
|
+
BV1: [{ id: 1000058, title: 'Consumer path', score: 4, matched_terms: ['overlay'] }],
|
|
226
|
+
BV2: [{ id: 1000062, title: 'Idea surfaces', score: 6, matched_terms: ['promote', 'merge'] }],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g,
|
|
230
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
231
|
+
|
|
232
|
+
const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms));
|
|
233
|
+
async function settle(extraMs = 0) {
|
|
234
|
+
if (extraMs) await tick(extraMs);
|
|
235
|
+
for (let i = 0; i < 30; i++) await tick(0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function fail(status, code, message) {
|
|
239
|
+
return { status, ok: false, data: { error: { code, message } } };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function boot(opts = {}) {
|
|
243
|
+
const {
|
|
244
|
+
idea = DEFAULT_IDEA,
|
|
245
|
+
rank = 'metic',
|
|
246
|
+
versions = DEFAULT_VERSIONS,
|
|
247
|
+
suggestions = DEFAULT_SUGGESTIONS,
|
|
248
|
+
suggestDelayMs = {},
|
|
249
|
+
suggestThrows = false,
|
|
250
|
+
createdTask = { id: 1003600, title: 'Cover the overlay handlers' },
|
|
251
|
+
createFails = null,
|
|
252
|
+
patchFails = null,
|
|
253
|
+
mergeTargets = { 1000900: { id: 1000900, title: 'The canonical idea' } },
|
|
254
|
+
} = opts;
|
|
255
|
+
|
|
256
|
+
const requests = [];
|
|
257
|
+
const toasts = [];
|
|
258
|
+
const events = [];
|
|
259
|
+
const navigations = [];
|
|
260
|
+
|
|
261
|
+
const ok = (data) => ({ status: 200, ok: true, data });
|
|
262
|
+
const api = {
|
|
263
|
+
async request(method, p, init) {
|
|
264
|
+
const body = init && init.body;
|
|
265
|
+
requests.push({ method, path: p, body });
|
|
266
|
+
const url = p.replace('/api/bongos', '');
|
|
267
|
+
const [route, query] = url.split('?');
|
|
268
|
+
if (method === 'GET' && route === '/me') return ok({ builder: { id: 1, rank } });
|
|
269
|
+
if (method === 'GET' && route === '/versions') return ok({ versions });
|
|
270
|
+
if (method === 'GET' && route === '/goals/suggestions') {
|
|
271
|
+
const version = new URLSearchParams(query || '').get('version');
|
|
272
|
+
const delay = suggestDelayMs[version] || 0;
|
|
273
|
+
if (delay) await tick(delay);
|
|
274
|
+
if (suggestThrows) return { status: 503, ok: false, data: null };
|
|
275
|
+
return ok({ suggestions: suggestions[version] || [] });
|
|
276
|
+
}
|
|
277
|
+
if (method === 'GET' && route.startsWith('/inbox/')) {
|
|
278
|
+
const id = Number(route.slice('/inbox/'.length));
|
|
279
|
+
if (id === Number(idea.id)) return ok({ idea });
|
|
280
|
+
if (mergeTargets[id]) return ok({ idea: mergeTargets[id] });
|
|
281
|
+
return { status: 404, ok: false, data: null };
|
|
282
|
+
}
|
|
283
|
+
if (method === 'POST' && route === '/tasks') return createFails || ok({ task: { ...createdTask, title: body.title } });
|
|
284
|
+
if (method === 'PATCH' && route.startsWith('/inbox/')) return patchFails || ok({ idea: { ...idea, status: body.status } });
|
|
285
|
+
return ok({});
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const locationObj = {
|
|
290
|
+
hostname: 'builders.example.test',
|
|
291
|
+
pathname: '/builders',
|
|
292
|
+
search: '',
|
|
293
|
+
hash: `#/idea/${idea.id}`,
|
|
294
|
+
href: '',
|
|
295
|
+
assign(u) { navigations.push(['assign', u]); },
|
|
296
|
+
replace(u) { navigations.push(['replace', u]); },
|
|
297
|
+
};
|
|
298
|
+
const documentObj = {
|
|
299
|
+
readyState: 'complete',
|
|
300
|
+
body: makeEl('body'),
|
|
301
|
+
documentElement: makeEl('html'),
|
|
302
|
+
createElement: (tag) => makeEl(tag),
|
|
303
|
+
getElementById(id) { return walk(this.body, (el) => (el.id === id ? el : null)); },
|
|
304
|
+
querySelector(sel) { return this.body.querySelector(sel); },
|
|
305
|
+
querySelectorAll(sel) { return this.body.querySelectorAll(sel); },
|
|
306
|
+
createTextNode(text) { const n = makeEl('#text'); n._text = String(text); return n; },
|
|
307
|
+
addEventListener() {},
|
|
308
|
+
};
|
|
309
|
+
const windowObj = {
|
|
310
|
+
OTB: { escapeHtml, toast: (m) => toasts.push(m) },
|
|
311
|
+
BongosClient: { createClient: () => api },
|
|
312
|
+
location: locationObj,
|
|
313
|
+
addEventListener() {},
|
|
314
|
+
dispatchEvent(ev) { events.push(ev); return true; },
|
|
315
|
+
};
|
|
316
|
+
class CustomEvent {
|
|
317
|
+
constructor(type, init) { this.type = type; this.detail = init && init.detail; }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const sandbox = {
|
|
321
|
+
window: windowObj, document: documentObj, location: locationObj, CustomEvent,
|
|
322
|
+
history: { replaceState() {} },
|
|
323
|
+
sessionStorage: { getItem: () => null, setItem() {}, removeItem() {} },
|
|
324
|
+
console, setTimeout, clearTimeout, Promise, Date, Math, JSON, Error,
|
|
325
|
+
Number, String, Object, Array, Boolean, Set, Map, RegExp, encodeURIComponent,
|
|
326
|
+
};
|
|
327
|
+
sandbox.globalThis = sandbox;
|
|
328
|
+
vm.createContext(sandbox);
|
|
329
|
+
vm.runInContext(SRC, sandbox, { filename: 'task-detail.js' });
|
|
330
|
+
await settle();
|
|
331
|
+
|
|
332
|
+
const overlay = () => documentObj.getElementById('deeplink-card');
|
|
333
|
+
const panel = () => { const o = overlay(); return o ? o.querySelector('.td-triage') : null; };
|
|
334
|
+
const errText = () => { const p = panel(); const e = p && p.querySelector('[data-err]'); return e ? e.textContent : null; };
|
|
335
|
+
const requestsTo = (method, re) => requests.filter((r) => r.method === method && re.test(r.path));
|
|
336
|
+
|
|
337
|
+
// A real delegated click: the panel's own listener, handed a target whose
|
|
338
|
+
// closest() walks the parsed tree — exactly what the browser passes it.
|
|
339
|
+
async function clickGo(mode) {
|
|
340
|
+
const p = panel();
|
|
341
|
+
assert.ok(p, 'the triage panel must be rendered before a click');
|
|
342
|
+
const btn = p.querySelector(`[data-go="${mode}"]`);
|
|
343
|
+
assert.ok(btn, `no [data-go="${mode}"] control in the panel — got: ${p.innerHTML}`);
|
|
344
|
+
p.fire('click', { target: btn });
|
|
345
|
+
await settle();
|
|
346
|
+
return btn;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function submitForm(which) {
|
|
350
|
+
const form = panel().querySelector(`[data-form="${which}"]`);
|
|
351
|
+
assert.ok(form, `no ${which} form rendered`);
|
|
352
|
+
let prevented = false;
|
|
353
|
+
form.fire('submit', { preventDefault() { prevented = true; }, target: form });
|
|
354
|
+
await settle();
|
|
355
|
+
// The handler is wired with preventDefault() first: if this is false the
|
|
356
|
+
// submit listener never ran and every assertion below would be vacuous.
|
|
357
|
+
assert.equal(prevented, true, `the ${which} submit handler did not execute`);
|
|
358
|
+
return form;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function changeVersion(versionId, { wait = 0 } = {}) {
|
|
362
|
+
const sel = panel().querySelector('select[name="version"]');
|
|
363
|
+
assert.ok(sel, 'the promote form must offer a version picker');
|
|
364
|
+
sel.value = versionId;
|
|
365
|
+
sel.fire('change', {});
|
|
366
|
+
if (wait) await settle(wait);
|
|
367
|
+
return sel;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
requests, requestsTo, toasts, events, navigations,
|
|
372
|
+
overlay, panel, errText, clickGo, submitForm, changeVersion, settle,
|
|
373
|
+
goalSelect: () => panel().querySelector('[data-goal-select]'),
|
|
374
|
+
field: (which, name) => panel().querySelector(`[data-form="${which}"]`)[name],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ===========================================================================
|
|
379
|
+
// The overlay boots and offers the triage panel
|
|
380
|
+
// ===========================================================================
|
|
381
|
+
|
|
382
|
+
await test('the overlay boots off the hash and renders the panel for a Metic', async () => {
|
|
383
|
+
const h = await boot();
|
|
384
|
+
assert.ok(h.overlay(), 'a #deeplink-card overlay is mounted');
|
|
385
|
+
assert.ok(h.panel(), 'and an open idea carries the triage panel');
|
|
386
|
+
const verbs = h.panel().querySelectorAll('[data-go]').map((b) => b.dataset.go);
|
|
387
|
+
assert.deepEqual(verbs, ['promote', 'merge', 'discard'], 'all three verdicts are reachable');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await test('a Xenos gets the card but no triage panel (the server gate is mirrored, not trusted)', async () => {
|
|
391
|
+
const h = await boot({ rank: 'xenos' });
|
|
392
|
+
assert.ok(h.overlay(), 'the record is still readable');
|
|
393
|
+
assert.equal(h.panel(), null, 'the actions are not offered below Metic');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
await test('a closed idea offers no triage panel', async () => {
|
|
397
|
+
const h = await boot({ idea: { ...DEFAULT_IDEA, status: 'promoted' } });
|
|
398
|
+
assert.equal(h.panel(), null, 'a settled idea has nothing left to decide');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ===========================================================================
|
|
402
|
+
// Promote — the form the version and goal pickers live on
|
|
403
|
+
// ===========================================================================
|
|
404
|
+
|
|
405
|
+
await test('opening Promote loads open versions only, and preselects the idea\'s suggested one', async () => {
|
|
406
|
+
const h = await boot();
|
|
407
|
+
await h.clickGo('promote');
|
|
408
|
+
const vsel = h.panel().querySelector('select[name="version"]');
|
|
409
|
+
const ids = vsel.querySelectorAll('option').map((o) => o.attrs.value);
|
|
410
|
+
assert.deepEqual(ids, ['BV1', 'BV2'], 'a shipped version would 409 the create, so it is not offered');
|
|
411
|
+
assert.equal(vsel.value, 'BV2', 'the idea\'s suggested_version is honoured');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
await test('the goal picker leads with the STORED hint, then the suggester, then the opt-out', async () => {
|
|
415
|
+
const h = await boot();
|
|
416
|
+
await h.clickGo('promote');
|
|
417
|
+
const opts = h.goalSelect().querySelectorAll('option');
|
|
418
|
+
assert.equal(opts[0].attrs.value, '1000071', 'the hint carried from the filing comes first');
|
|
419
|
+
assert.ok('selected' in opts[0].attrs, 'and it is what the form will submit');
|
|
420
|
+
assert.match(opts[0].textContent, /carried from the filing/);
|
|
421
|
+
assert.equal(opts[1].attrs.value, '1000062', 'the suggester fills in below it');
|
|
422
|
+
assert.match(opts[1].textContent, /matched: promote, merge/, 'with the evidence for judging it');
|
|
423
|
+
assert.equal(opts[opts.length - 1].attrs.value, '', 'the opt-out is always available');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await test('with no stored hint the top suggestion is preselected instead', async () => {
|
|
427
|
+
const h = await boot({ idea: { ...DEFAULT_IDEA, suggested_goal_id: null } });
|
|
428
|
+
await h.clickGo('promote');
|
|
429
|
+
assert.equal(h.goalSelect().value, '1000062', 'the suggester supplies the default when nothing was named');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await test('a suggester outage degrades to the opt-out rather than blocking the verdict', async () => {
|
|
433
|
+
const h = await boot({ suggestThrows: true, idea: { ...DEFAULT_IDEA, suggested_goal_id: null } });
|
|
434
|
+
await h.clickGo('promote');
|
|
435
|
+
const opts = h.goalSelect().querySelectorAll('option');
|
|
436
|
+
assert.equal(opts.length, 1, 'only the opt-out remains');
|
|
437
|
+
assert.equal(h.goalSelect().value, '', 'and it is selected, so the form is still submittable');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
await test('promote with a goal chosen POSTs goal_id, then marks the idea promoted', async () => {
|
|
441
|
+
const h = await boot();
|
|
442
|
+
await h.clickGo('promote');
|
|
443
|
+
await h.submitForm('promote');
|
|
444
|
+
|
|
445
|
+
const posts = h.requestsTo('POST', /\/tasks$/);
|
|
446
|
+
assert.equal(posts.length, 1, `exactly one create — got ${JSON.stringify(h.requests.map((r) => r.method + ' ' + r.path))}`);
|
|
447
|
+
const body = posts[0].body;
|
|
448
|
+
assert.equal(body.goal_id, 1000071, 'the chosen goal reaches the server as a number');
|
|
449
|
+
assert.equal(body.version_id, 'BV2');
|
|
450
|
+
assert.equal(body.priority, 2, 'suggested_priority is carried, clamped into the 1-5 the column allows');
|
|
451
|
+
assert.equal(body.source_ref, `idea:${IDEA_ID}`, 'provenance makes a retried promote idempotent');
|
|
452
|
+
assert.equal(body.kind, 'blocker-resolution', 'the idea\'s kind saves the classifier a guess');
|
|
453
|
+
assert.match(body.description, /Promoted from idea/, 'the task says where it came from');
|
|
454
|
+
assert.ok(!('est_minutes' in body) && !('credits_reward' in body), 'blank number fields are omitted, not sent as 0');
|
|
455
|
+
|
|
456
|
+
const patches = h.requestsTo('PATCH', new RegExp(`/inbox/${IDEA_ID}$`));
|
|
457
|
+
assert.equal(patches.length, 1, 'the idea is linked to the task it became');
|
|
458
|
+
assert.equal(patches[0].body.status, 'promoted');
|
|
459
|
+
assert.equal(patches[0].body.promoted_to_task_id, 1003600);
|
|
460
|
+
|
|
461
|
+
assert.match(h.panel().innerHTML, /Promoted to task/, 'the panel reports the result');
|
|
462
|
+
assert.match(h.panel().innerHTML, /href="\/task\/1003600"/, 'and links the task it made');
|
|
463
|
+
assert.ok(h.events.some((e) => e.type === 'otb:idea-triaged' && e.detail.verdict === 'promoted'),
|
|
464
|
+
'home\'s attention inbox is told so the queue advances behind the overlay');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await test('the title survives escaping — what the form renders is what gets created', async () => {
|
|
468
|
+
const h = await boot();
|
|
469
|
+
await h.clickGo('promote');
|
|
470
|
+
await h.submitForm('promote');
|
|
471
|
+
assert.equal(h.requestsTo('POST', /\/tasks$/)[0].body.title, DEFAULT_IDEA.title,
|
|
472
|
+
'quotes and angle brackets must round-trip through the value attribute unmangled');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
await test('promote opting OUT of a goal sends no goal_id key at all', async () => {
|
|
476
|
+
const h = await boot();
|
|
477
|
+
await h.clickGo('promote');
|
|
478
|
+
h.goalSelect().value = '';
|
|
479
|
+
await h.submitForm('promote');
|
|
480
|
+
const body = h.requestsTo('POST', /\/tasks$/)[0].body;
|
|
481
|
+
assert.ok(!('goal_id' in body),
|
|
482
|
+
'the version\'s general bucket applies only on an ABSENT key — sending null or "" would be a different create');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await test('an empty title is refused in the browser, before a doomed request', async () => {
|
|
486
|
+
const h = await boot();
|
|
487
|
+
await h.clickGo('promote');
|
|
488
|
+
h.panel().querySelector('[name="title"]').value = ' ';
|
|
489
|
+
await h.submitForm('promote');
|
|
490
|
+
assert.equal(h.requestsTo('POST', /\/tasks$/).length, 0, 'nothing is sent');
|
|
491
|
+
assert.match(h.errText(), /A task needs a title/);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// ===========================================================================
|
|
495
|
+
// The version change re-fetches goals (task 1003071's race guard)
|
|
496
|
+
// ===========================================================================
|
|
497
|
+
|
|
498
|
+
await test('changing the version re-fetches THAT version\'s goals and repaints the picker', async () => {
|
|
499
|
+
const h = await boot();
|
|
500
|
+
await h.clickGo('promote');
|
|
501
|
+
const before = h.requestsTo('GET', /\/goals\/suggestions/).length;
|
|
502
|
+
await h.changeVersion('BV1', { wait: 5 });
|
|
503
|
+
|
|
504
|
+
const fetches = h.requestsTo('GET', /\/goals\/suggestions/);
|
|
505
|
+
assert.equal(fetches.length, before + 1, 'exactly one re-fetch');
|
|
506
|
+
assert.match(fetches[fetches.length - 1].path, /version=BV1/, 'for the version now selected');
|
|
507
|
+
|
|
508
|
+
const opts = h.goalSelect().querySelectorAll('option').map((o) => o.attrs.value);
|
|
509
|
+
assert.ok(opts.includes('1000058'), `BV1's goals are painted — got ${JSON.stringify(opts)}`);
|
|
510
|
+
assert.ok(!opts.includes('1000062'), 'and BV2\'s are gone — offering them would be a home the server rejects');
|
|
511
|
+
assert.equal(h.goalSelect().disabled, false, 'the control is handed back to the builder');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
await test('two quick version changes: only the NEWEST is allowed to write', async () => {
|
|
515
|
+
// The slow reply belongs to the abandoned selection; if it were allowed to
|
|
516
|
+
// land it would paint the wrong version's goals over the right ones.
|
|
517
|
+
const h = await boot({ suggestDelayMs: { BV1: 40 } });
|
|
518
|
+
await h.clickGo('promote');
|
|
519
|
+
await h.changeVersion('BV1');
|
|
520
|
+
await h.changeVersion('BV2');
|
|
521
|
+
await h.settle(80);
|
|
522
|
+
|
|
523
|
+
const opts = h.goalSelect().querySelectorAll('option').map((o) => o.attrs.value);
|
|
524
|
+
assert.ok(opts.includes('1000062'), `the newest request's goals stand — got ${JSON.stringify(opts)}`);
|
|
525
|
+
assert.ok(!opts.includes('1000058'), 'the stale reply is dropped, not painted');
|
|
526
|
+
assert.equal(h.goalSelect().disabled, false, 'and the newest request re-enabled the control');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
await test('after a version change the promoted task is created against the NEW version', async () => {
|
|
530
|
+
const h = await boot();
|
|
531
|
+
await h.clickGo('promote');
|
|
532
|
+
await h.changeVersion('BV1', { wait: 5 });
|
|
533
|
+
await h.submitForm('promote');
|
|
534
|
+
const body = h.requestsTo('POST', /\/tasks$/)[0].body;
|
|
535
|
+
assert.equal(body.version_id, 'BV1');
|
|
536
|
+
assert.equal(body.goal_id, 1000058, 'and against a goal that belongs to it');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await test('the filed goal hint does NOT survive a version change — it would 400', async () => {
|
|
540
|
+
// The defect this harness was built to find. A goal belongs to exactly one
|
|
541
|
+
// version and POST /tasks rejects a foreign one with bad_goal_id, so carrying
|
|
542
|
+
// the filing's hint onto another version offered the one option guaranteed to
|
|
543
|
+
// fail — pre-selected, as the default the builder would submit.
|
|
544
|
+
const h = await boot();
|
|
545
|
+
await h.clickGo('promote');
|
|
546
|
+
await h.changeVersion('BV1', { wait: 5 });
|
|
547
|
+
const opts = h.goalSelect().querySelectorAll('option').map((o) => o.attrs.value);
|
|
548
|
+
assert.ok(!opts.includes('1000071'), `the other version's hint is not offered — got ${JSON.stringify(opts)}`);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
await test('changing BACK to the filed version restores the hint as the default', async () => {
|
|
552
|
+
const h = await boot();
|
|
553
|
+
await h.clickGo('promote');
|
|
554
|
+
await h.changeVersion('BV1', { wait: 5 });
|
|
555
|
+
await h.changeVersion('BV2', { wait: 5 });
|
|
556
|
+
assert.equal(h.goalSelect().value, '1000071', 'the filing\'s guess is honoured wherever it is valid');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// ===========================================================================
|
|
560
|
+
// The two-write failure path
|
|
561
|
+
// ===========================================================================
|
|
562
|
+
|
|
563
|
+
await test('create succeeds but the idea PATCH fails: the task id is NOT lost', async () => {
|
|
564
|
+
const h = await boot({ patchFails: fail(500, 'internal_error', 'boom') });
|
|
565
|
+
await h.clickGo('promote');
|
|
566
|
+
await h.submitForm('promote');
|
|
567
|
+
|
|
568
|
+
assert.equal(h.requestsTo('POST', /\/tasks$/).length, 1, 'the task was created');
|
|
569
|
+
const msg = h.errText();
|
|
570
|
+
assert.match(msg, /1003600/, 'the created task id is in front of the builder — "could not create" would send them to make a twin');
|
|
571
|
+
assert.match(msg, /Press Create task again/, 'and the retry is named as the fix');
|
|
572
|
+
assert.match(msg, /it does not make a second one/, 'because the create is idempotent on source_ref');
|
|
573
|
+
assert.match(h.panel().innerHTML, /data-form="promote"/, 'the form is still standing to retry from');
|
|
574
|
+
assert.equal(h.panel().querySelector('[data-submit]').disabled, false, 'and the button is usable again');
|
|
575
|
+
assert.equal(h.panel().querySelector('[data-submit]').textContent, 'Create task', 'restored from its busy label');
|
|
576
|
+
assert.ok(!h.events.some((e) => e.type === 'otb:idea-triaged'),
|
|
577
|
+
'nothing is announced — the idea is still open');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
await test('retrying after that failure re-sends the SAME create rather than a second one', async () => {
|
|
581
|
+
const h = await boot({ patchFails: fail(500, 'internal_error', 'boom') });
|
|
582
|
+
await h.clickGo('promote');
|
|
583
|
+
await h.submitForm('promote');
|
|
584
|
+
await h.submitForm('promote');
|
|
585
|
+
const posts = h.requestsTo('POST', /\/tasks$/);
|
|
586
|
+
assert.equal(posts.length, 2, 'the retry goes back through the create');
|
|
587
|
+
assert.equal(posts[0].body.source_ref, posts[1].body.source_ref,
|
|
588
|
+
'with the same idempotency key, so the server resolves it to one task');
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await test('a failed create reports the server\'s reason and leaves the idea alone', async () => {
|
|
592
|
+
const h = await boot({ createFails: fail(409, 'version_closed', 'nope') });
|
|
593
|
+
await h.clickGo('promote');
|
|
594
|
+
await h.submitForm('promote');
|
|
595
|
+
assert.equal(h.requestsTo('PATCH', /\/inbox\//).length, 0, 'no idea is marked promoted against a task that does not exist');
|
|
596
|
+
assert.match(h.errText(), /That version has shipped/, 'the code is mapped to something a human can act on');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
await test('a 403 is read out of the NESTED error envelope, not swallowed', async () => {
|
|
600
|
+
// err.data.error is an OBJECT: reading it as a string would make every code
|
|
601
|
+
// comparison silently miss and fall through to the generic message.
|
|
602
|
+
const h = await boot({ createFails: fail(403, 'permission_forbidden', 'no') });
|
|
603
|
+
await h.clickGo('promote');
|
|
604
|
+
await h.submitForm('promote');
|
|
605
|
+
assert.match(h.errText(), /Only Metic\+ can triage ideas/);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ===========================================================================
|
|
609
|
+
// Merge and Discard
|
|
610
|
+
// ===========================================================================
|
|
611
|
+
|
|
612
|
+
await test('merge PATCHes the fold, announces it, and closes the overlay', async () => {
|
|
613
|
+
const h = await boot();
|
|
614
|
+
await h.clickGo('merge');
|
|
615
|
+
h.field('merge', 'target').value = '1000900';
|
|
616
|
+
h.field('merge', 'note').value = 'same request, twice';
|
|
617
|
+
await h.submitForm('merge');
|
|
618
|
+
|
|
619
|
+
const patch = h.requestsTo('PATCH', new RegExp(`/inbox/${IDEA_ID}$`))[0];
|
|
620
|
+
assert.equal(patch.body.status, 'merged');
|
|
621
|
+
assert.equal(patch.body.merged_into_idea_id, 1000900);
|
|
622
|
+
assert.equal(patch.body.triage_note, 'same request, twice');
|
|
623
|
+
assert.ok(h.events.some((e) => e.detail.verdict === 'merged'), 'the queue behind the overlay is told');
|
|
624
|
+
assert.ok(h.toasts.some((t) => /merged into/.test(t)), `the verdict is confirmed — got ${JSON.stringify(h.toasts)}`);
|
|
625
|
+
assert.equal(h.overlay(), null, 'a verdict that leaves nothing to look at closes the card');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
await test('the merge form echoes the target idea\'s title so a typo is visible', async () => {
|
|
629
|
+
const h = await boot();
|
|
630
|
+
await h.clickGo('merge');
|
|
631
|
+
const input = h.field('merge', 'target');
|
|
632
|
+
input.value = '1000900';
|
|
633
|
+
input.fire('change', {});
|
|
634
|
+
await h.settle();
|
|
635
|
+
assert.match(h.panel().querySelector('[data-target-name]').textContent, /The canonical idea/);
|
|
636
|
+
|
|
637
|
+
input.value = '999999';
|
|
638
|
+
input.fire('change', {});
|
|
639
|
+
await h.settle();
|
|
640
|
+
assert.match(h.panel().querySelector('[data-target-name]').textContent, /no idea/, 'an unknown number says so');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
await test('merge refuses a blank target in the browser', async () => {
|
|
644
|
+
const h = await boot();
|
|
645
|
+
await h.clickGo('merge');
|
|
646
|
+
await h.submitForm('merge');
|
|
647
|
+
assert.equal(h.requestsTo('PATCH', /\/inbox\//).length, 0);
|
|
648
|
+
assert.match(h.errText(), /Give the idea number/);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await test('discard PATCHes the reason and closes the overlay', async () => {
|
|
652
|
+
const h = await boot();
|
|
653
|
+
await h.clickGo('discard');
|
|
654
|
+
h.field('discard', 'note').value = 'already covered elsewhere';
|
|
655
|
+
await h.submitForm('discard');
|
|
656
|
+
const patch = h.requestsTo('PATCH', new RegExp(`/inbox/${IDEA_ID}$`))[0];
|
|
657
|
+
assert.equal(patch.body.status, 'discarded');
|
|
658
|
+
assert.equal(patch.body.triage_note, 'already covered elsewhere');
|
|
659
|
+
assert.ok(h.events.some((e) => e.detail.verdict === 'discarded'));
|
|
660
|
+
assert.equal(h.overlay(), null);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
await test('a discard with no reason sends null, not an empty string', async () => {
|
|
664
|
+
const h = await boot();
|
|
665
|
+
await h.clickGo('discard');
|
|
666
|
+
await h.submitForm('discard');
|
|
667
|
+
assert.equal(h.requestsTo('PATCH', /\/inbox\//)[0].body.triage_note, null);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
await test('a failed discard keeps the form up with the reason typed into it', async () => {
|
|
671
|
+
// No envelope at all (a proxy 500, a dropped body): the fallback sentence has
|
|
672
|
+
// to stand in, because an empty message would leave the panel silent.
|
|
673
|
+
const h = await boot({ patchFails: { status: 500, ok: false, data: null } });
|
|
674
|
+
await h.clickGo('discard');
|
|
675
|
+
h.field('discard', 'note').value = 'no longer wanted';
|
|
676
|
+
await h.submitForm('discard');
|
|
677
|
+
assert.ok(h.overlay(), 'the overlay stays open on a failure');
|
|
678
|
+
assert.match(h.errText(), /Could not discard that idea/);
|
|
679
|
+
assert.equal(h.field('discard', 'note').value, 'no longer wanted', 'the typed reason is not thrown away');
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
await test('a server message in the envelope is shown verbatim rather than replaced', async () => {
|
|
683
|
+
const h = await boot({ patchFails: fail(422, 'unprocessable', 'That idea was already triaged.') });
|
|
684
|
+
await h.clickGo('discard');
|
|
685
|
+
await h.submitForm('discard');
|
|
686
|
+
assert.equal(h.errText(), 'That idea was already triaged.',
|
|
687
|
+
'an unmapped code still tells the builder what the server said');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// ===========================================================================
|
|
691
|
+
// Getting back out of a form
|
|
692
|
+
// ===========================================================================
|
|
693
|
+
|
|
694
|
+
await test('Cancel returns to the three verbs without sending anything', async () => {
|
|
695
|
+
const h = await boot();
|
|
696
|
+
await h.clickGo('promote');
|
|
697
|
+
await h.clickGo('idle');
|
|
698
|
+
const verbs = h.panel().querySelectorAll('[data-go]').map((b) => b.dataset.go);
|
|
699
|
+
assert.deepEqual(verbs, ['promote', 'merge', 'discard']);
|
|
700
|
+
assert.equal(h.requestsTo('POST', /\/tasks$/).length, 0);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
await test('a version list that will not load offers a way back rather than a dead panel', async () => {
|
|
704
|
+
const h = await boot({ versions: [] });
|
|
705
|
+
await h.clickGo('promote');
|
|
706
|
+
assert.match(h.panel().textContent, /No open version/);
|
|
707
|
+
assert.ok(h.panel().querySelector('[data-go="idle"]'), 'a builder is never stranded mid-panel');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
summary();
|