Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d15eb5fc2 | ||
|
|
02b50ddf35 |
No files matched your search
@@ -0,0 +1,94 @@
|
||||
# Row 173 (#1671): a Seen click that waited out a Board read keeps the meaning it was clicked with
|
||||
|
||||
Brief: docs/plans/2026-10-19_seen-click-intent.md (blob 225ac823b). Branch dewey/row173 off aca16404c.
|
||||
Queue reviewer: Darkwing. Gate (queue row): Darkwing approves on #1671 naming the candidate manifest; webui and
|
||||
every scripts/test-*.sh green on Sage's gate rerun.
|
||||
|
||||
## Design
|
||||
|
||||
- Intent. `seenButton` draws `data-intent="seen"` on a Seen button and `data-intent="unsee"` on an Unsee
|
||||
button, from the same `r.seen` that picks the label. The click handler reads it into `mark` before
|
||||
`writing = true` and before the wait, so the fresh row can't change it. I chose a `data-` value over reading the
|
||||
label text so a later relabel can't change what a click posts.
|
||||
- The fresh row already holds the intent (`r.seen === mark`). This happens when another viewer marked or unmarked
|
||||
the row while the read was in flight. Nothing is posted, the button is re-enabled, and the page announces
|
||||
`<agent> already seen` or `<agent> already waiting`.
|
||||
- Why post nothing. The route takes a repeat without harm: `markSeen` with `seen:true` rewrites the same mark,
|
||||
and `seen:false` deletes a key that is already gone (control-board/src/scan.mjs:255-256). So posting the same
|
||||
value would also be correct. It would cost a write and a rescan, though, and the read that just finished has
|
||||
already drawn the row. The brief allows either.
|
||||
- An unseen row can still hold an old mark for an earlier lastActivity. Posting nothing on Unsee leaves that
|
||||
mark, which is harmless because `scanAgent` only counts a mark equal to the row's lastActivity (scan.mjs:340).
|
||||
- Otherwise the click posts `seen: mark` and announces from `mark`, not from the fresh row.
|
||||
- Unchanged: the row 172 wait, the single-click guard (`writing`), the no-row branch, and the `/api/seen` route
|
||||
(out of scope).
|
||||
- `seen.disabled = false` in the new branch matches the no-row branch. After the read, `accept` redraws the
|
||||
inspector and the cards, so `seen` is usually a detached button and the line has no visible effect. No test
|
||||
can tell it apart; see M5.
|
||||
|
||||
## Test
|
||||
|
||||
packages/webui/tests/board-held.test.mjs gains one test: "a Seen or Unsee click that waited out a Board read
|
||||
keeps its meaning: another viewer's mark in that read posts nothing". `setup` returns `boardURL: f.boardURL` so
|
||||
the test can post to the board as the other viewer.
|
||||
- Steps, for each of Seen and Unsee:
|
||||
- it holds `/api/board` with `window.__hold` and clicks Refresh;
|
||||
- while the read is held, it posts the other viewer's mark straight to the board;
|
||||
- it clicks the inspector's button and releases the read.
|
||||
- Checks: the announcement (`agent1 already seen`, then `agent1 already waiting`), `#seen-count` (1, then 0), and
|
||||
that the page sent no `/api/seen` at all.
|
||||
- The held fetch hasn't reached the board when the other viewer posts (the stub awaits the hold before calling
|
||||
the real fetch), so the released answer carries the other viewer's mark.
|
||||
|
||||
Hash pins and readers checked (git grep, with a positive control for 64-hex): nothing pins app.js. shell.test
|
||||
counts existing app.js strings only. browser.test, sso-browser.test and the other board-held tests click
|
||||
`[data-seen]` buttons the page draws, so they get `data-intent` with them.
|
||||
|
||||
## Mutants (run on CLEAR only; each one edit to app.js, then board-held.test.mjs whole)
|
||||
|
||||
| id | edit | expected kill |
|
||||
|----|------|---------------|
|
||||
| M1 | post `seen: !r.seen` and drop the `r.seen === mark` branch (the old toggle) | new test (a post, and "returned to waiting") |
|
||||
| M2 | drop the `r.seen === mark` branch only (post the repeat) | new test (a post, and "marked seen") |
|
||||
| M3 | `seenButton` swaps the intent (`r.seen ? 'seen' : 'unsee'`) | row 172 test (`#seen-count` never 1) and the new test |
|
||||
| M4 | the new branch returns without `announce` | new test (announcement wait times out) |
|
||||
| M5 | drop `seen.disabled = false` in the new branch | equivalent: the button is redrawn by `accept`; named, not run |
|
||||
|
||||
Each run: `timeout -k 10 180 node --test --test-force-exit packages/webui/tests/board-held.test.mjs`; a kill
|
||||
counts only as an assertion or a named wait failing in the named test.
|
||||
|
||||
## Runs
|
||||
|
||||
None yet. Gander holds the gate slot (row 253 run 5) from 16:32Z. The claim, the push and every run wait for
|
||||
Sage's CLEAR.
|
||||
|
||||
## Round 2: rebase onto 78b69e1e6 (sage-4361)
|
||||
|
||||
- Branch dewey/row173-r2 off refactor 78b69e1e6. The round 1 commit 25ff7cd3f (on aca16404c, 555 commits
|
||||
behind) was cherry-picked as a new commit; dewey/row173 is untouched. No force-push.
|
||||
- The pick merged on its own. `git range-diff aca16404c..25ff7cd3f 78b69e1e6..<pick>` reports the commit equal.
|
||||
- Overlap with refactor: one app.js line, the `/api/config` handler at the end of the file (federation render).
|
||||
It is far from both row 173 hunks and changes no assertion or reviewed line. board-held.test.mjs and the brief
|
||||
(blob 225ac823b) are unchanged on refactor.
|
||||
- No test pins app.js by hash (grepped tests for app.js plus 64-hex).
|
||||
|
||||
### Run list
|
||||
|
||||
app.js is a page script, loaded by index.html, not imported by any module. Its test readers:
|
||||
- by name: board-held, browser, granted-user, serve, shell, sso-browser, user-option-swap;
|
||||
- by page load: every webui test that opens the Console through tests/browser.mjs (31 test files).
|
||||
No test outside packages/webui reads or serves app.js (`git grep` over packages/*/tests and scripts: none).
|
||||
|
||||
Order, each run with `TMPDIR` from `mktemp -d /mnt/storage/scratch/tmp/d173XXXXXX` and absolute paths:
|
||||
1. M1-M4 below, each with board-held.test.mjs whole.
|
||||
2. board-held.test.mjs whole on the restored tree.
|
||||
3. packages/webui whole (all 70 test files), which covers both reader groups.
|
||||
case1-accept.test.mjs is not in the list (mos-1612). Runs need Sage's CLEAR.
|
||||
|
||||
### Mutant procedure
|
||||
|
||||
The M1-M5 table above stands. M5 stays equivalent: the new branch is reached only after the waited-out Board
|
||||
read's `accept` (refresh, app.js:372) has redrawn the row, so `seen` is the detached old button. Each mutant is one
|
||||
edit to app.js. Before the next mutant, the restored app.js must match the candidate's sha256. Each run is in its
|
||||
own process group so a timeout kill takes its browser with it. Afterwards I sweep for orphaned node/chromium
|
||||
processes in my run directory.
|
||||
@@ -1,171 +0,0 @@
|
||||
# Row 267 (C2-15, #1772): hybrid universes, exclusions, pins and the crossover. Build notes
|
||||
|
||||
Morgana, 2026-10-25. Brief: `docs/plans/2026-10-20_case2-rows.md` "C2-15 hybrid universes: exclusions,
|
||||
pins and the end-to-end crossover" (blob 85e6fe2e). Reviewer Filbert, gate owner Sage. Claimed at rev 4087.
|
||||
Branch `morgana/row267` from refactor 78b69e1e6. Design 1.4 is `agents/gosalyn/work/case2/DESIGN.md`.
|
||||
|
||||
Status: draft design. Nothing is built or run. No runs before Sage's CLEAR.
|
||||
|
||||
## 1. What the code does today (78b69e1e6)
|
||||
|
||||
- `mapping.mjs:119-134` checks only the shape of a constraint. `exclude` is any folder path, and
|
||||
`pin.entity` is any folder path with a scalar value. Nothing checks either against the sources.
|
||||
- `query.mjs:52-57` `sees()` reads `exclude` as a path prefix only (`inside(c.exclude, file.path)`). The
|
||||
type form `second/Characters` (`mapping.test.mjs:203`) passes the validator but hides nothing, because
|
||||
no file path starts with `second/`.
|
||||
- `reader.mjs:206-210` gives R6 only for a canon link into a universe that is neither the file's own nor
|
||||
one of its sources. A hybrid canon file that links into an excluded source path gets no problem.
|
||||
- `guard.mjs:91-97` lets any mix of one hybrid and its sources pass. It doesn't check exclusions.
|
||||
- `problems.mjs` has R1-R7 and no R8.
|
||||
|
||||
## 2. Design
|
||||
|
||||
### 2.1 Validator (`mapping.mjs`)
|
||||
|
||||
The validator runs after every universe is read, alongside the existing sources check at `:141-146`,
|
||||
because it needs each source's books, playground and supporting lists. For each constraint of a hybrid:
|
||||
|
||||
- **Type form:** the first segment is one of the hybrid's `sources`, and the rest is exactly one key of
|
||||
`types`.
|
||||
- **Path form:** the path equals, or lies inside, a book folder of one of the sources.
|
||||
- Both forms fit: refuse `library-mapping-invalid` ("... reads as both a source/type and a source path").
|
||||
- Neither form fits: refuse `library-mapping-invalid` ("... lies inside no book of the hybrid's
|
||||
sources"). This covers a path in the hybrid's own books, in a non-source universe, or unmapped.
|
||||
- **Path form under a playground:** a path equal to or inside one of a source's playground prefixes
|
||||
refuses ("... lies under a playground of universes.X, which the hybrid never reads"). "Under" means the
|
||||
path equals the prefix without its trailing `/`, or starts with the prefix.
|
||||
- **`pin.entity`:** the same path-form rules apply, with no type form. An entity is one file.
|
||||
|
||||
The stored shape stays `{ exclude }` / `{ pin }`. I don't add a `form` field. `library_universe`
|
||||
returns `constraints` as they are now, and the index cache digest doesn't change for existing mappings.
|
||||
`excluded()` works out the form again each time. It's pure and cheap.
|
||||
|
||||
### 2.2 `excluded(mapping, hybrid, path)` (`scope.mjs`)
|
||||
|
||||
This returns true when any `exclude` of universe `hybrid` covers `path`:
|
||||
|
||||
- **Type form `S/T`:** `path` starts with `${book}/${T}/` for some book of source S.
|
||||
- **Path form P:** `path === P` or `path` starts with `${P}/`.
|
||||
|
||||
It returns false when `hybrid` isn't a hybrid. It reads only the path (D2). It has three callers:
|
||||
|
||||
- `query.mjs` `sees()` replaces its own `inside(c.exclude, ...)` reading with this helper.
|
||||
- `reader.mjs` uses it for the excluded-link R6.
|
||||
- `guard.mjs` uses it for the excluded cite.
|
||||
|
||||
`query-fixture.mjs` `EXCLUSION` (`Book One/Research`) is path form and still fits, so the existing
|
||||
C2-05a query tests keep their constants.
|
||||
|
||||
### 2.3 Reader: excluded-link R6 and R8
|
||||
|
||||
- **Excluded-link R6:** in the canon branch at `:206`, a hybrid file's link to a target where
|
||||
`excluded(mapping, file.universe, to.path)` is true gives `problem('R6', file.path, link.line)`, with
|
||||
the default message. A custom message isn't possible: `mirror.mjs:196-201` `validProblem` accepts a
|
||||
sync-state problem only when `message === RULES[code]`, so a new message would make every later sync
|
||||
state read refuse `library-state-invalid`. The link stays in `file.links`, as the other R-rules leave
|
||||
it. `sees()` already drops it from the hybrid's `links_out`. A link that is both foreign and excluded
|
||||
can't occur, because an excluded path always lies in a source, so each link gets at most one R6.
|
||||
- **R8:** a new `RULES.R8` entry reads "pinned value changed". The check runs once per commit, after the
|
||||
link pass, for each pin of each hybrid:
|
||||
- **Entity missing** from `byPath`: R8 at `entity`, with no line. This covers a file that is absent,
|
||||
R1, R2 or R7.
|
||||
- **Entity present:** R8 at `entity`, on the `field` key's line where there is one, unless the parsed
|
||||
front matter holds `field` as a scalar (string, number or boolean) and `String(parsed) ===
|
||||
String(value)`. A list, null, a missing key and unparsed (R3) front matter all count as changed. Under
|
||||
a plain `String()` comparison, `['grey']` would equal `'grey'`, so this rule closes that hole.
|
||||
- **Hybrid versions:** each canon file of the hybrid whose `type` (from `classify`) equals the entity's
|
||||
type, and whose name matches the entity's, is a version of it. Names are the trimmed front matter
|
||||
`name` on both sides. When either file has no text `name`, the file names (stems) compare instead.
|
||||
A version that holds `field` with another value gets R8 at its own path, on that key's line. A version
|
||||
without `field` gets nothing, and neither does any other file that only links to the entity.
|
||||
- The entity's type and name come from `classify(mapping, entity)` and the entity's parsed file. When the
|
||||
entity is missing, its stem stands in for the name.
|
||||
|
||||
### 2.4 Guard (`guard.mjs`)
|
||||
|
||||
After the existing span check at `:91-97`, the guard looks for a hybrid among the cited universes. If
|
||||
there is one, and some cite lies in one of its sources while `excluded(mapping, hybrid, c.path)` is true,
|
||||
the guard refuses `citation-cross-universe` ("citation N (path) is excluded from hybrid H"). A reply that
|
||||
cites only source paths, excluded ones included, has no hybrid among its universes, so it's unchanged.
|
||||
|
||||
### 2.5 Setup sheet
|
||||
|
||||
`docs/case2/SETUP-SHEET.md` gets one section after "Supporting folders", headed "Making a crossover
|
||||
(hybrid) universe". It covers `kind`, `name`, `sources`, `books`, `playground`, `constraints`
|
||||
(`exclude` in both forms, `pin`) and `notes`, and says that the continuity role reads and cites the notes.
|
||||
It also says that only she edits this entry (C2-04 rule M9).
|
||||
|
||||
## 3. Tests and mutants
|
||||
|
||||
New files and cases, all on scratch repos under TMPDIR (never /tmp):
|
||||
|
||||
- **`packages/library/tests/hybrid.test.mjs` (new):**
|
||||
- A business-file-shaped mapping (fixture `mapping.json` plus a `cross` hybrid) on a repo in her
|
||||
layout's shape.
|
||||
- `find` and `search` in `cross` return its own files plus the sources' canon, each carrying its source
|
||||
universe. They never return a source playground or an excluded path, and this runs once for each
|
||||
exclude form.
|
||||
- `get` keeps `links_out` into the sources and drops excluded targets.
|
||||
- A source universe's lookups never return a `cross` file.
|
||||
- `universeInfo` returns the sources, constraints and notes.
|
||||
- `excluded()` gets unit cases for both forms and for a non-hybrid.
|
||||
- **`mapping.test.mjs`:** one case per new refusal: both forms fit, neither fits, the hybrid's own book,
|
||||
an exclude under a source playground, and a pin under a source playground. `second/Characters` at
|
||||
`:203` still checks.
|
||||
- **`reader.test.mjs`:**
|
||||
- R8 cases: entity missing, entity changed, a same-name hybrid version with no link, and a list value.
|
||||
- Non-R8 cases: a hybrid place file with `age: 300` that links the entity, and a different hybrid
|
||||
character that links it.
|
||||
- The excluded-link R6, once for each exclude form.
|
||||
- **`guard.test.mjs`:** a hybrid cite with source canon passes, and a hybrid cite beside an excluded path
|
||||
refuses in both forms. A cite of only an excluded source path passes as that universe's item.
|
||||
- **`packages/bus/tests/citation-guard.test.mjs`:** through `citationWorld` with `mapChange` adding the
|
||||
hybrid, the excluded-beside-hybrid send refuses and the `messages` count stays the same.
|
||||
- **`packages/bus/tests/library-lookups.test.mjs`:** one broker case, where `library.find` and
|
||||
`library.universe` in the hybrid return what the library tests above assert.
|
||||
- **C2-04 M9 rule (agents can't write `projects`), not a mutant:** a case in `packages/business/tests/library-projects-protected.test.mjs` where an organization
|
||||
write that adds a new hybrid universe refuses `organization-projects-protected`. This is outside the
|
||||
brief's "tests in library and bus" list (§4 Q1).
|
||||
|
||||
Mutants. Each runs only its own named tests, and the tree is restored after each one.
|
||||
|
||||
| # | Mutation | Killed by |
|
||||
|---|---|---|
|
||||
| X1 | `excluded()` ignores the type form | hybrid.test lookups, type form |
|
||||
| X2 | `excluded()` ignores the path form | hybrid.test lookups, path form |
|
||||
| X3 | `sees()` keeps its old `inside()` reading | hybrid.test type form |
|
||||
| X4 | the validator accepts "both forms fit" | mapping.test |
|
||||
| X5 | the validator skips the playground check | mapping.test |
|
||||
| X6 | R8 compares with plain `String()` (list equals scalar) | reader.test list value |
|
||||
| X7 | a version is matched by link instead of name | reader.test non-R8 cases |
|
||||
| X8 | the reader drops the excluded-link R6 | reader.test |
|
||||
| X9 | the guard skips the excluded check | guard.test "C2-15 a hybrid cite beside an excluded source path refuses citation-cross-universe, both forms" (the mutant run); bus citation-guard "C2-15 excluded cite beside a hybrid refuses and inserts nothing" also fails |
|
||||
|
||||
## 4. Open questions for Sage and Filbert
|
||||
|
||||
- **Q1 (manifest):** the C2-04 M9 test lives in `packages/business/tests`, which the brief's file list leaves
|
||||
out. I recommend adding it there, beside the existing C2-04 M9 cases, and putting the business suite on the
|
||||
gate. The alternative is a bus-level `organization.set` test, which needs more setup and tests the
|
||||
same writer.
|
||||
- **Q2 (supporting):** the brief refuses a path under a source's playground. A source's supporting
|
||||
prefix is never read by the hybrid either (`sees()` passes canon only). I recommend refusing that too,
|
||||
under the same REQ-LIB-3 reason. That widens the brief by one rule, so I need a yes.
|
||||
- **Q3 (R8 reaches other readers):** adding `RULES.R8` goes beyond the reader:
|
||||
- `survey.mjs:70` counts problems per `RULES` key, so the survey line gains `R8 0`.
|
||||
`survey.test.mjs:75` pins that line and has to change. It's in the library tests, so it's in scope.
|
||||
- `mirror.mjs` `validProblem` accepts R8 through `RULES`, so a sync state written after this row
|
||||
carries R8 problems.
|
||||
- A copy running older code would refuse such a state. That only matters if a re-pin goes backwards.
|
||||
- I'll grep `docs/case2/SURVEY-RUN.md` and the Console for R-code lists before in-review.
|
||||
- **Q4 (version scope):** I count only the hybrid's canon files as versions. Playground is "what if" and
|
||||
is exempt, as it is from R5 and R6. A hybrid with `books: []` therefore never gets a version R8. It
|
||||
still gets the entity R8.
|
||||
- **Q5 (`pin.entity` not `.md`):** a non-`.md` entity is never indexed, so R8 would report it missing at
|
||||
every commit. I recommend refusing it in the validator. The brief leaves existence to the reader, but
|
||||
this is a shape check, not an existence check.
|
||||
|
||||
## 5. Gate
|
||||
|
||||
Filbert approves the manifest. The library and bus suites, plus business if Q1 stands, run green on
|
||||
Sage's gate along with every `scripts/test-*.sh`. Hash pins on the changed files are grepped before
|
||||
in-review.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Row 267: one existing assertion changes (mos-1272)
|
||||
|
||||
File: `packages/library/tests/survey.test.mjs:75`, in the test that checks the survey's last three lines
|
||||
with `--mapping`.
|
||||
|
||||
Before:
|
||||
|
||||
```
|
||||
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0']);
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```
|
||||
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0, R8 0']);
|
||||
```
|
||||
|
||||
Why: R8 ("pinned value changed") is a new rule in `problems.mjs` `RULES`, and the survey line lists a
|
||||
count for every `RULES` key (`survey.mjs:70`), so it gains `R8 0`. The fixture has no hybrid and no pin,
|
||||
so the count is 0 and no other count moves.
|
||||
|
||||
No other existing assertion in the row changes.
|
||||
@@ -66,7 +66,7 @@
|
||||
$('mode').onchange = e => { mode = e.target.value; theme(); };
|
||||
function seenButton(r) {
|
||||
if (!['waiting', 'error'].includes(r.state) || !r.lastActivity) return '';
|
||||
return `<button type="button" class="btn" data-seen="${esc(key(r))}" aria-label="${r.seen ? 'Unsee' : 'Mark seen'} ${esc(r.agent)}">${r.seen ? 'Unsee' : 'Seen'}</button>`;
|
||||
return `<button type="button" class="btn" data-seen="${esc(key(r))}" data-intent="${r.seen ? 'unsee' : 'seen'}" aria-label="${r.seen ? 'Unsee' : 'Mark seen'} ${esc(r.agent)}">${r.seen ? 'Unsee' : 'Seen'}</button>`;
|
||||
}
|
||||
const historyButton = r => `<button type="button" class="btn conv-open" data-history="${esc(key(r))}" aria-label="History for ${esc(r.agent)}">History</button>`;
|
||||
const openButton = r => `<button type="button" class="session-open" data-open="${esc(key(r))}" aria-controls="inspector" aria-expanded="${selected === key(r)}">${esc(r.agent)}</button>`;
|
||||
@@ -415,12 +415,16 @@
|
||||
if (!seen || writing) return;
|
||||
// A Board read in flight (reading) holds busy. The click waits for that read and posts on the row it
|
||||
// drew; dropping it left the click doing nothing (row 172). A second click while one waits or posts is dropped.
|
||||
// The click means what the button said when clicked (Seen marks, Unsee returns the row to waiting), read before
|
||||
// the wait. If the fresh row already holds that, another viewer got there first: nothing is posted (row 173).
|
||||
const mark = seen.dataset.intent === 'seen';
|
||||
writing = true; seen.disabled = true;
|
||||
try {
|
||||
while (reading) await reading;
|
||||
const r = row(seen.dataset.seen); if (!r) { seen.disabled = false; return; }
|
||||
if (r.seen === mark) { seen.disabled = false; announce(`${r.agent} already ${mark ? 'seen' : 'waiting'}`); return; }
|
||||
busy = true;
|
||||
try { accept(await api('/api/seen', { project: r.project, agent: r.agent, lastActivity: r.lastActivity, seen: !r.seen })); announce(`${r.agent} ${r.seen ? 'returned to waiting' : 'marked seen'}`); }
|
||||
try { accept(await api('/api/seen', { project: r.project, agent: r.agent, lastActivity: r.lastActivity, seen: mark })); announce(`${r.agent} ${mark ? 'marked seen' : 'returned to waiting'}`); }
|
||||
catch (err) { error(err); seen.disabled = false; }
|
||||
finally { busy = false; schedule(); }
|
||||
} finally { writing = false; }
|
||||
|
||||
@@ -82,7 +82,7 @@ async function setup(machine, fast = false) {
|
||||
const polls = () => b.evaluate('window.__polls');
|
||||
const settle = () => new Promise(r => setTimeout(r, 600));
|
||||
const done = async () => { await b.close(); await close(web); await f.close(); store.close(); rmSync(root, { recursive: true, force: true }); };
|
||||
return { b, base, wait, text, shown, reqs, polls, settle, done };
|
||||
return { b, base, boardURL: f.boardURL, wait, text, shown, reqs, polls, settle, done };
|
||||
}
|
||||
|
||||
const HELD = 'document.querySelectorAll("#held table tbody tr").length === 2';
|
||||
@@ -191,6 +191,37 @@ test('a Seen click while a Board read is in flight waits for that read, then pos
|
||||
} finally { await done(); }
|
||||
});
|
||||
|
||||
// Row 173 (#1671): the click took its meaning from the fresh row, so another viewer's mark landing in the read
|
||||
// it waited for turned "Seen" into an unsee. The click means what its button said; a fresh row that already
|
||||
// holds that posts nothing and says so. The other viewer posts straight to the board while the read is held.
|
||||
test('a Seen or Unsee click that waited out a Board read keeps its meaning: another viewer\'s mark in that read posts nothing', { timeout: 120000 }, async () => {
|
||||
const { b, base, boardURL, wait, text, reqs, done } = await setup(false);
|
||||
const posts = async () => (await reqs()).filter(r => r.path === '/api/seen').length;
|
||||
const other = async seen => {
|
||||
const r = (await (await fetch(`${boardURL}/api/board`)).json()).sessions.find(s => s.project === 'proj' && s.agent === 'agent1');
|
||||
const res = await fetch(`${boardURL}/api/seen`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ project: 'proj', agent: 'agent1', lastActivity: r.lastActivity, seen }) });
|
||||
assert.equal(res.status, 200);
|
||||
};
|
||||
try {
|
||||
await b.navigate(base);
|
||||
await wait(`${SESSIONS} && ${HELD}`);
|
||||
await b.evaluate(`document.querySelector('table.sessions [data-open="proj/agent1"]').click(), true`);
|
||||
await wait('document.querySelector("#inspection [data-seen]")');
|
||||
for (const [label, seen, count, said] of [['Seen', true, '1', 'agent1 already seen'], ['Unsee', false, '0', 'agent1 already waiting']]) {
|
||||
assert.equal(await text('#inspection [data-seen]'), label);
|
||||
await b.evaluate(`window.__hold = '/api/board', document.querySelector('#refresh').click(), true`);
|
||||
await wait('window.__held.length === 1');
|
||||
// The held fetch has not reached the board yet, so its answer carries the other viewer's mark.
|
||||
await other(seen);
|
||||
await b.evaluate(`document.querySelector('#inspection [data-seen]').click(), true`);
|
||||
await b.evaluate('window.__held.splice(0).forEach(r => r()), true');
|
||||
await wait(`document.querySelector("#announce").textContent === ${JSON.stringify(said)}`);
|
||||
assert.equal(await text('#seen-count'), count);
|
||||
assert.equal(await posts(), 0);
|
||||
}
|
||||
} finally { await done(); }
|
||||
});
|
||||
|
||||
test('a redirect answer on a Board read, a bus read, a conversation read, a board POST, a module read and a send signs the page out: one line, nothing stale, no further reads', { timeout: 120000 }, async () => {
|
||||
// The poll timers fire after 150 ms here, so the 600 ms settle below spans several ticks.
|
||||
const { b, base, wait, text, shown, reqs, polls, settle, done } = await setup(true, true);
|
||||
|
||||
Reference in new issue
Block a user