@bongos/core 1.23.17 → 1.23.18
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 +15 -15
- package/docs/module-api-changelog.md +2 -0
- package/modules/copy-desk/auto-close.js +9 -3
- package/modules/copy-desk/page-status.js +25 -13
- package/modules/copy-desk/tests/copy_page_auto_close.mjs +8 -0
- package/modules/copy-desk/tests/copy_page_status.mjs +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/copy_stuck_page_tweaks.mjs +39 -2
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/bongos/package-core.js",
|
|
5
|
-
"core_version": "1.23.
|
|
6
|
-
"core_contract": "1.23.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.23.18",
|
|
6
|
+
"core_contract": "1.23.18",
|
|
7
|
+
"source_commit": "cf436c265cc3be4912f3c0ee9500d8a6fc8e2e20",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-28T00:
|
|
9
|
+
"built_at": "2026-10-28T00:49:26.518Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 616,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 4233,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "eba7a9ced17fd552ba4b96d63bed4bf6de2b1b6b5b8825168fc83c764da4724a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2937,7 +2937,7 @@
|
|
|
2937
2937
|
{
|
|
2938
2938
|
"path": "docs/module-api-changelog.md",
|
|
2939
2939
|
"mode": "0000644",
|
|
2940
|
-
"sha256": "
|
|
2940
|
+
"sha256": "5231965cae7f22d595883889e831987e6f14ad158155df16fb439918dc6e44fc"
|
|
2941
2941
|
},
|
|
2942
2942
|
{
|
|
2943
2943
|
"path": "docs/modules-contract.md",
|
|
@@ -4487,7 +4487,7 @@
|
|
|
4487
4487
|
{
|
|
4488
4488
|
"path": "modules/copy-desk/auto-close.js",
|
|
4489
4489
|
"mode": "0000644",
|
|
4490
|
-
"sha256": "
|
|
4490
|
+
"sha256": "f9f4e8e009fab44c47fd3651c5d44da94cec1fd4cb7ad0931515fadbd6526280"
|
|
4491
4491
|
},
|
|
4492
4492
|
{
|
|
4493
4493
|
"path": "modules/copy-desk/docx.js",
|
|
@@ -4537,7 +4537,7 @@
|
|
|
4537
4537
|
{
|
|
4538
4538
|
"path": "modules/copy-desk/page-status.js",
|
|
4539
4539
|
"mode": "0000644",
|
|
4540
|
-
"sha256": "
|
|
4540
|
+
"sha256": "0d3ae54e54203d62a6b8b7874706a478476d2c22f680ed92dc6d91d2c8285c3e"
|
|
4541
4541
|
},
|
|
4542
4542
|
{
|
|
4543
4543
|
"path": "modules/copy-desk/pages.js",
|
|
@@ -4607,7 +4607,7 @@
|
|
|
4607
4607
|
{
|
|
4608
4608
|
"path": "modules/copy-desk/tests/copy_page_auto_close.mjs",
|
|
4609
4609
|
"mode": "0000644",
|
|
4610
|
-
"sha256": "
|
|
4610
|
+
"sha256": "f992174839c4f2be3c3a6f942c27493a5a7081e46ce01293f9075ce9a26330be"
|
|
4611
4611
|
},
|
|
4612
4612
|
{
|
|
4613
4613
|
"path": "modules/copy-desk/tests/copy_page_draft.mjs",
|
|
@@ -4622,7 +4622,7 @@
|
|
|
4622
4622
|
{
|
|
4623
4623
|
"path": "modules/copy-desk/tests/copy_page_status.mjs",
|
|
4624
4624
|
"mode": "0000644",
|
|
4625
|
-
"sha256": "
|
|
4625
|
+
"sha256": "e90260a6fc9b95c65efd1aa1488c88f7344d94e6d315055a52648d4c92f416dc"
|
|
4626
4626
|
},
|
|
4627
4627
|
{
|
|
4628
4628
|
"path": "modules/copy-desk/tests/copy_proposals.mjs",
|
|
@@ -10957,12 +10957,12 @@
|
|
|
10957
10957
|
{
|
|
10958
10958
|
"path": "package-lock.json",
|
|
10959
10959
|
"mode": "0000644",
|
|
10960
|
-
"sha256": "
|
|
10960
|
+
"sha256": "f2aa7d637ca9724249d7da6c5522c3c610d8f8f577ad45800c87bc5c34e85400"
|
|
10961
10961
|
},
|
|
10962
10962
|
{
|
|
10963
10963
|
"path": "package.json",
|
|
10964
10964
|
"mode": "0000644",
|
|
10965
|
-
"sha256": "
|
|
10965
|
+
"sha256": "3e4721211b08ace73fce53f51c9f456fc53915ba54df2816c8e9e4c34df20f87"
|
|
10966
10966
|
},
|
|
10967
10967
|
{
|
|
10968
10968
|
"path": "public-docs/index.html",
|
|
@@ -10982,7 +10982,7 @@
|
|
|
10982
10982
|
{
|
|
10983
10983
|
"path": "release-notes.json",
|
|
10984
10984
|
"mode": "0000644",
|
|
10985
|
-
"sha256": "
|
|
10985
|
+
"sha256": "48cc837f2857d4db190398862cce83da8cf092ac339e77d043c9b998993ba957"
|
|
10986
10986
|
},
|
|
10987
10987
|
{
|
|
10988
10988
|
"path": "scripts/bongos/CLAUDE.md",
|
|
@@ -14857,7 +14857,7 @@
|
|
|
14857
14857
|
{
|
|
14858
14858
|
"path": "src/module-api.js",
|
|
14859
14859
|
"mode": "0000644",
|
|
14860
|
-
"sha256": "
|
|
14860
|
+
"sha256": "a69374b5230240bb5f2551554f6ce7e1ac40163e1ddd9ab6bcc5027619734e67"
|
|
14861
14861
|
},
|
|
14862
14862
|
{
|
|
14863
14863
|
"path": "src/module-loader/catalog.js",
|
|
@@ -15907,7 +15907,7 @@
|
|
|
15907
15907
|
{
|
|
15908
15908
|
"path": "tests/copy_stuck_page_tweaks.mjs",
|
|
15909
15909
|
"mode": "0000644",
|
|
15910
|
-
"sha256": "
|
|
15910
|
+
"sha256": "e53e4b7c768b0e1b5cb2343d174eba8886586678cc3d3a70560d107faa88a1f5"
|
|
15911
15911
|
},
|
|
15912
15912
|
{
|
|
15913
15913
|
"path": "tests/core_258_box_session_claims_db.mjs",
|
|
@@ -3249,5 +3249,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3249
3249
|
landed since 1.23.15 with no explicit bump. run 40607196561. (task 1002620)
|
|
3250
3250
|
1.23.17 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3251
3251
|
landed since 1.23.16 with no explicit bump. run 40609586769. (task 1002620)
|
|
3252
|
+
1.23.18 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3253
|
+
landed since 1.23.17 with no explicit bump. run 40611101597. (task 1002620)
|
|
3252
3254
|
---------------------------------------------------------------------------
|
|
3253
3255
|
```
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
// rewrite left the batch for an engineer task at submit
|
|
14
14
|
// (unplaced, shared or removed lines), so /tweak has no line
|
|
15
15
|
// to apply and the round can never move on its own.
|
|
16
|
+
// draft_expired `ready` with a draft and no batch, untouched for
|
|
17
|
+
// page-status's DRAFT_EXPIRES_AFTER (14 days, task 1004901):
|
|
18
|
+
// its artist was reminded at 7 days and never came back.
|
|
16
19
|
//
|
|
17
|
-
//
|
|
20
|
+
// All three are abandoned through the lifecycle port's releaseStuckTweak (no compose,
|
|
18
21
|
// so the note is the reason and nothing is written to the description). The
|
|
19
22
|
// engineer tasks a submit filed are untouched: they carry the artist's words.
|
|
20
23
|
//
|
|
@@ -24,20 +27,23 @@
|
|
|
24
27
|
|
|
25
28
|
const api = require('../../src/module-api');
|
|
26
29
|
const pages = require('./pages');
|
|
30
|
+
const pageStatus = require('./page-status');
|
|
27
31
|
|
|
28
32
|
const GRACE_MS = Number(process.env.COPY_AUTO_CLOSE_GRACE_MS) || 60 * 60_000;
|
|
29
33
|
|
|
30
34
|
const NOTES = Object.freeze({
|
|
31
35
|
blank: 'Closed by itself: the round was opened but nothing was written in it.',
|
|
32
36
|
nothing_to_apply: 'Closed by itself: every rewritten line went to an engineer task, so nothing was left to apply on the page.',
|
|
37
|
+
draft_expired: 'Closed by itself: the draft sat untouched for two weeks. Its words stay on this task; claim the page again to start over.',
|
|
33
38
|
});
|
|
34
39
|
|
|
35
|
-
function closeReason(task, { now = Date.now(), graceMs = GRACE_MS } = {}) {
|
|
40
|
+
function closeReason(task, { now = Date.now(), graceMs = GRACE_MS, draftExpiresMs = pageStatus.DRAFT_EXPIRES_AFTER } = {}) {
|
|
36
41
|
if (!task || task.status !== 'ready' || task.holder_id != null) return null;
|
|
37
42
|
const touched = Date.parse(task.updated_at || task.created_at || '');
|
|
38
43
|
if (!Number.isFinite(touched) || now - touched < graceMs) return null;
|
|
39
44
|
const p = pages.blockPresence(task.description);
|
|
40
|
-
|
|
45
|
+
// task 1004901: a draft its artist left untouched for two weeks expires.
|
|
46
|
+
if (!p.batch) return p.draft ? (now - touched >= draftExpiresMs ? 'draft_expired' : null) : 'blank';
|
|
41
47
|
const b = pages.parseBatchBlock(task.description);
|
|
42
48
|
if (b.ok && b.batch.lines.length === 0) return 'nothing_to_apply';
|
|
43
49
|
return null;
|
|
@@ -306,22 +306,29 @@ function orderedSurfaces(inventory) {
|
|
|
306
306
|
// (applied rounds only) stayed empty: the artist's finished work looked lost.
|
|
307
307
|
// This is every page with an open round, where it is, since when (the round's
|
|
308
308
|
// last update), and whether it has sat there too long. Waiting for review is
|
|
309
|
-
// the artist's own move and a draft is theirs to finish
|
|
310
|
-
//
|
|
311
|
-
// a
|
|
309
|
+
// the artist's own move and a draft is theirs to finish: a draft or a page
|
|
310
|
+
// being written is stuck after 7 days and a draft expires after 14 (task
|
|
311
|
+
// 1004901); a review is stuck after 3 days (task 1004777, so its artist is
|
|
312
|
+
// told); a state the reads cannot name always needs to be reviewed.
|
|
312
313
|
// ---------------------------------------------------------------------------
|
|
313
314
|
|
|
314
315
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
316
|
+
// task 1004901: every state names WHO moves it next (waits_on: 'artist' is the
|
|
317
|
+
// round's own artist, 'appliers' the Metic and up builders who apply and land
|
|
318
|
+
// work) and WHEN it is overdue (stuck_after: then it is pushed to that person on
|
|
319
|
+
// Home and in the daily digest). A draft nobody touches also EXPIRES
|
|
320
|
+
// (expires_after): the auto-close sweep abandons it, its words kept on the task.
|
|
321
|
+
const DRAFT_EXPIRES_AFTER = 14 * DAY_MS;
|
|
315
322
|
const IN_FLIGHT = Object.freeze({
|
|
316
|
-
draft: { word: 'Being written', stuck_after:
|
|
317
|
-
writing: { word: 'Being written', stuck_after:
|
|
318
|
-
words_review: { word: 'Words waiting for review', stuck_after: 3 * DAY_MS },
|
|
319
|
-
submitted: { word: 'Approved, waiting to be applied', stuck_after: 3 * DAY_MS },
|
|
320
|
-
applying: { word: 'Being applied', stuck_after: DAY_MS },
|
|
323
|
+
draft: { word: 'Being written', stuck_after: 7 * DAY_MS, waits_on: 'artist', expires_after: DRAFT_EXPIRES_AFTER },
|
|
324
|
+
writing: { word: 'Being written', stuck_after: 7 * DAY_MS, waits_on: 'artist' },
|
|
325
|
+
words_review: { word: 'Words waiting for review', stuck_after: 3 * DAY_MS, waits_on: 'artist' },
|
|
326
|
+
submitted: { word: 'Approved, waiting to be applied', stuck_after: 3 * DAY_MS, waits_on: 'appliers' },
|
|
327
|
+
applying: { word: 'Being applied', stuck_after: DAY_MS, waits_on: 'appliers' },
|
|
321
328
|
// task 1004777: a review left 3 days is stuck too, so its artist is told on Home.
|
|
322
|
-
waiting_for_artist: { word: 'Waiting for your review', stuck_after: 3 * DAY_MS },
|
|
323
|
-
landing: { word: 'Approved, going live', stuck_after: DAY_MS },
|
|
324
|
-
unknown: { word: 'Needs to be reviewed', stuck_after: 0 },
|
|
329
|
+
waiting_for_artist: { word: 'Waiting for your review', stuck_after: 3 * DAY_MS, waits_on: 'artist' },
|
|
330
|
+
landing: { word: 'Approved, going live', stuck_after: DAY_MS, waits_on: 'appliers' },
|
|
331
|
+
unknown: { word: 'Needs to be reviewed', stuck_after: 0, waits_on: 'appliers' },
|
|
325
332
|
});
|
|
326
333
|
const IN_FLIGHT_ORDER = ['unknown', 'landing', 'applying', 'submitted', 'waiting_for_artist', 'words_review', 'writing', 'draft'];
|
|
327
334
|
|
|
@@ -344,6 +351,9 @@ function inFlightFor(list, byPage, nowMs) {
|
|
|
344
351
|
state_word: kind.word,
|
|
345
352
|
since,
|
|
346
353
|
stuck: kind.stuck_after != null && (kind.stuck_after === 0 || (age != null && age > kind.stuck_after)),
|
|
354
|
+
waits_on: kind.waits_on,
|
|
355
|
+
due_at: since && kind.stuck_after != null ? new Date(Date.parse(since) + kind.stuck_after).toISOString() : null,
|
|
356
|
+
expires_at: since && kind.expires_after != null ? new Date(Date.parse(since) + kind.expires_after).toISOString() : null,
|
|
347
357
|
holder: o.holder,
|
|
348
358
|
artist_id: r.artist_id != null ? String(r.artist_id) : null,
|
|
349
359
|
});
|
|
@@ -380,8 +390,8 @@ function stuckPageTweaks({ inventory, tasks, now = Date.now() }) {
|
|
|
380
390
|
state_word: f.state_word,
|
|
381
391
|
since: f.since,
|
|
382
392
|
days: ms == null ? null : Math.max(0, Math.floor(ms / DAY_MS)),
|
|
383
|
-
//
|
|
384
|
-
waits_on: f.
|
|
393
|
+
// The state's own owner (task 1004901), one table for the Studio and the pushes.
|
|
394
|
+
waits_on: f.waits_on,
|
|
385
395
|
artist_id: f.artist_id,
|
|
386
396
|
};
|
|
387
397
|
});
|
|
@@ -968,4 +978,6 @@ module.exports = {
|
|
|
968
978
|
stuckPageTweaks,
|
|
969
979
|
stuckPageTweaksFor,
|
|
970
980
|
stuckDigestText,
|
|
981
|
+
// task 1004901: when an untouched draft expires (the auto-close sweep reads it).
|
|
982
|
+
DRAFT_EXPIRES_AFTER,
|
|
971
983
|
};
|
|
@@ -56,6 +56,14 @@ await test('a blank round and an empty batch are the only two that close', () =>
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
await test('a draft left untouched for two weeks expires; a younger one stays (task 1004901)', () => {
|
|
60
|
+
const D = 86400000;
|
|
61
|
+
const at = (days) => round({ description: draft, updated_at: new Date(NOW - days * D).toISOString() });
|
|
62
|
+
assert.equal(autoClose.closeReason(at(13.9), { now: NOW }), null);
|
|
63
|
+
assert.equal(autoClose.closeReason(at(14.1), { now: NOW }), 'draft_expired');
|
|
64
|
+
assert.equal(autoClose.closeReason({ ...at(30), holder_id: '7' }, { now: NOW }), null, 'a held draft is being written, never expired');
|
|
65
|
+
});
|
|
66
|
+
|
|
59
67
|
await test('the grace period is honoured: a round touched within it stays', () => {
|
|
60
68
|
const t = round({ updated_at: new Date(NOW - 59 * 60_000).toISOString() });
|
|
61
69
|
assert.equal(autoClose.closeReason(t, { now: NOW, graceMs: 60 * 60_000 }), null);
|
|
@@ -257,7 +257,7 @@ ${pages.composeWordsApprovedBlock({ at: ago(9), by: '7' })}`;
|
|
|
257
257
|
round(2, 'landing:b', { status: 'ready', description: `${pages.roundTaskBody(pg('landing:b'), 1)}\n\n${batch('landing:b')}`, updated_at: ago(1) }),
|
|
258
258
|
round(3, 'landing:c', { status: 'confirmed', updated_at: ago(17) }),
|
|
259
259
|
round(4, 'builders:d', { status: 'completed', grade_passed: true, updated_at: ago(20) }),
|
|
260
|
-
round(5, 'builders:e', { status: 'active', holder_id: '7', holder_login: 'me', holder_worktree: false, updated_at: ago(
|
|
260
|
+
round(5, 'builders:e', { status: 'active', holder_id: '7', holder_login: 'me', holder_worktree: false, updated_at: ago(5) }),
|
|
261
261
|
round(6, 'builders:f', { status: 'ready', updated_at: ago(0.2) }),
|
|
262
262
|
round(7, 'builders:g', { status: 'shipped', shipped_at: ago(3), updated_at: ago(3) }),
|
|
263
263
|
round(8, 'status:h', { status: 'active', holder_id: '8', holder_worktree: true, updated_at: ago(2) }),
|
|
@@ -271,7 +271,7 @@ ${pages.composeWordsApprovedBlock({ at: ago(9), by: '7' })}`;
|
|
|
271
271
|
['builders:d', 'waiting_for_artist', 'Waiting for your review', true],
|
|
272
272
|
['landing:b', 'submitted', 'Approved, waiting to be applied', false],
|
|
273
273
|
['builders:e', 'writing', 'Being written', false],
|
|
274
|
-
], 'a shipped round is not in flight; writing is
|
|
274
|
+
], 'a shipped round is not in flight; writing is not overdue before 7 days (task 1004901), and a review left 20 days is (task 1004777)');
|
|
275
275
|
assert.equal(out.in_flight[1].since, ago(17));
|
|
276
276
|
assert.equal(out.in_flight[1].task_id, '3');
|
|
277
277
|
assert.equal(out.pages.find((p) => p.id === 'landing:a').status_label, 'Untweaked', 'the status still counts only what shipped');
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.18",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.18",
|
|
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.23.
|
|
3
|
+
"version": "1.23.18",
|
|
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/release-notes.json
CHANGED
|
@@ -10491,5 +10491,11 @@
|
|
|
10491
10491
|
"id": "1004899",
|
|
10492
10492
|
"text": "Page tweak rounds that hold nothing to do now close themselves after an hour, so the Studio no longer shows empty rounds as needing a look forever."
|
|
10493
10493
|
}
|
|
10494
|
+
],
|
|
10495
|
+
"1.23.18": [
|
|
10496
|
+
{
|
|
10497
|
+
"id": "1004901",
|
|
10498
|
+
"text": "Every stage of a page tweak now has a named person and a deadline. Drafts left alone get a reminder after a week and close after two, so no page sits in the Studio without someone being told."
|
|
10499
|
+
}
|
|
10494
10500
|
]
|
|
10495
10501
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.23.
|
|
84
|
+
const CORE_VERSION = '1.23.18'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -34,8 +34,8 @@ const TASKS = [
|
|
|
34
34
|
round(2, 'landing:account', { status: 'completed', grade_passed: true, updated_at: ago(5) }),
|
|
35
35
|
// submitted yesterday: NOT stuck
|
|
36
36
|
round(3, 'builders:studio', { status: 'ready', description: `${pages.roundTaskBody(pg('builders:studio'), 1)}\n\n${batch('builders:studio')}`, updated_at: ago(1) }),
|
|
37
|
-
// being written for
|
|
38
|
-
round(4, 'landing:projects', { status: 'active', holder_id: '7', holder_worktree: false, updated_at: ago(
|
|
37
|
+
// being written for 3 days: not overdue (that is 7 days, task 1004901)
|
|
38
|
+
round(4, 'landing:projects', { status: 'active', holder_id: '7', holder_worktree: false, updated_at: ago(3) }),
|
|
39
39
|
];
|
|
40
40
|
const stuck = () => status.stuckPageTweaks({ inventory: INVENTORY, tasks: TASKS, now: NOW });
|
|
41
41
|
|
|
@@ -46,6 +46,43 @@ await test('the composer: only stuck pages, each with where it sits, how long, a
|
|
|
46
46
|
]);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// task 1004901: every state names who moves it next and when it is overdue.
|
|
50
|
+
await test('every state has an owner and a deadline: overdue only past it, told to that owner', () => {
|
|
51
|
+
const D = 86400000;
|
|
52
|
+
const p = (id) => pg(id);
|
|
53
|
+
const desc = (id, blocks) => [pages.roundTaskBody(p(id), 1), ...blocks].join('\n\n');
|
|
54
|
+
const draftBlock = (id) => pages.composeDraftBlock({ page_id: id, reading_hash: 'h', saved_at: ago(1), lines: [] });
|
|
55
|
+
const batchBlock = (id) => pages.composeBatchBlock({ page_id: id, reading_hash: 'h', submitted_at: ago(1), lines: [] });
|
|
56
|
+
const approved = pages.composeWordsApprovedBlock({ at: ago(1), by: ARTIST });
|
|
57
|
+
// [state, task shape, owner, days until overdue]
|
|
58
|
+
const table = [
|
|
59
|
+
['draft', (id) => ({ status: 'ready', description: desc(id, [draftBlock(id)]) }), 'artist', 7],
|
|
60
|
+
['writing', () => ({ status: 'active', holder_id: ARTIST, holder_worktree: false }), 'artist', 7],
|
|
61
|
+
['words_review', (id) => ({ status: 'ready', description: desc(id, [batchBlock(id)]) }), 'artist', 3],
|
|
62
|
+
['submitted', (id) => ({ status: 'ready', description: desc(id, [batchBlock(id), approved]) }), 'appliers', 3],
|
|
63
|
+
['applying', () => ({ status: 'active', holder_id: '99', holder_worktree: true }), 'appliers', 1],
|
|
64
|
+
['waiting_for_artist', () => ({ status: 'completed', grade_passed: true }), 'artist', 3],
|
|
65
|
+
['landing', () => ({ status: 'confirmed' }), 'appliers', 1],
|
|
66
|
+
['unknown', () => ({ status: 'ready' }), 'appliers', 0],
|
|
67
|
+
];
|
|
68
|
+
for (const [state, shape, owner, days] of table) {
|
|
69
|
+
const id = 'landing:projects';
|
|
70
|
+
const at = (age) => status.composePages({ inventory: INVENTORY, readings: null, tasks: [round(50, id, { ...shape(id), updated_at: ago(age) })], now: NOW }).in_flight[0];
|
|
71
|
+
const before = at(Math.max(days - 0.5, 0));
|
|
72
|
+
assert.equal(before.state, state);
|
|
73
|
+
assert.equal(before.waits_on, owner, `${state} waits on ${owner}`);
|
|
74
|
+
assert.equal(Date.parse(before.due_at) - Date.parse(before.since), days * D, `${state} is overdue after ${days} day(s)`);
|
|
75
|
+
if (days > 0) assert.equal(before.stuck, false, `${state} is not overdue before its deadline`);
|
|
76
|
+
const after = at(days + 0.5);
|
|
77
|
+
assert.equal(after.stuck, true, `${state} is overdue after its deadline`);
|
|
78
|
+
const told = status.stuckPageTweaksFor({ builderId: ARTIST, rank: 'xenos', stuck: status.stuckPageTweaks({ inventory: INVENTORY, tasks: [round(50, id, { ...shape(id), updated_at: ago(days + 0.5) })], now: NOW }) });
|
|
79
|
+
assert.equal(told.length, owner === 'artist' ? 1 : 0, `${state}: its artist is told only when it waits on them`);
|
|
80
|
+
}
|
|
81
|
+
const draft = status.composePages({ inventory: INVENTORY, readings: null, tasks: [round(51, 'landing:projects', { status: 'ready', description: desc('landing:projects', [draftBlock('landing:projects')]), updated_at: ago(2) })], now: NOW }).in_flight[0];
|
|
82
|
+
assert.equal(Date.parse(draft.expires_at) - Date.parse(draft.since), status.DRAFT_EXPIRES_AFTER, 'a draft says when it expires');
|
|
83
|
+
assert.equal(status.DRAFT_EXPIRES_AFTER, 14 * D);
|
|
84
|
+
});
|
|
85
|
+
|
|
49
86
|
await test('submitted words left unreviewed wait on their artist, not the appliers (task 1004900)', () => {
|
|
50
87
|
const words = round(9, 'landing:projects', { status: 'ready', description: `${pages.roundTaskBody(pg('landing:projects'), 1)}\n\n${pages.composeBatchBlock({ page_id: 'landing:projects', reading_hash: 'h', submitted_at: ago(4), lines: [] })}`, updated_at: ago(4) });
|
|
51
88
|
const got = status.stuckPageTweaks({ inventory: INVENTORY, tasks: [words], now: NOW });
|