Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d587a552d | ||
|
|
47db04eba8 | ||
|
|
0ed4fbab9b | ||
|
|
fc05bcbc39 | ||
|
|
46bad7ea3b | ||
|
|
e67c27b322 | ||
|
|
76d651dba4 | ||
|
|
88ec5e8e47 | ||
|
|
b9ddb6c5e2 | ||
|
|
bd2c1c4ea1 | ||
|
|
28918824ca | ||
|
|
331653b385 | ||
|
|
4f66dccdec | ||
|
|
398314cf4d | ||
|
|
23816cf285 | ||
|
|
38e322ea01 | ||
|
|
4f3fbc25a0 |
No files matched your search
@@ -5411,9 +5411,3 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-23T17:29:46Z | Gyro Vikunja571 sage-3542 Connections amendment | Branch gyro/editor-connections-design cut exactly from landed PRD1.17 30e6bb680; read REQ-LIB-5 in that PRD. Replaced stale J-b waits in parent and W4. Proposed W8 owns current authorized Markdown/DOCX links/backlinks list, labels, versioned refresh, leakage refusals and fixture tests. Build order under mos-1470/comment961 is W7 closed routing candidate, W8 landing, then W7 human first-real-import opening. No REQ-LIB-8 condition or acceptance change. Q234/mos-1363/S0 M1-M14 and post-10-27/MVP priority fixed. Static diff, twelve brief sections and append-only checks pass. No repo Node, tests, build, browser, queue mutation or live action. Fenton reviews after authorized docs branch push; Sage lands on approval.
|
||||
|
||||
2026-10-23T17:32:37Z | Gyro sage-3544 fenton-0016 N1/N2 delta | Before: W7 Gate/final real-conversion list omit the W8 build-order cross-reference and W8 human-view filtering cites agent settings. After: both locations explicitly put W8 landing before human opening as mos-1470 build order, not acceptance; comment/tracked-deletion links are not current story links and never appear in the human Connections view regardless of agent settings. New commit atop approved 5af060fb5, no amend. Static diff and append-only checks pass. Docs only, no repo Node/tests/live action. Fenton checks only this delta.
|
||||
|
||||
2026-10-23 | Fethry | Row 225 C2-10b (#1726), handoff recovery. Before: Morgana's untested candidate 9a131e956 replaces renderItems with renderReply and adds Console citation/notice/status rendering and status/revert routes. Continuing that exact history on fethry/row225; preserving A1-A5 and the accepted file deviations. Remaining: confirm limits contract, review/test candidate under Sage CLEAR, obtain Mos OK through Sage for inherited old-test changes, manifest and Dewey review. No Node/tests run in this session and no green claim.
|
||||
|
||||
2026-10-23 | Fethry | Row 225 T1 lane A (sage-3641/3647). Before: nested replyLimits fallback/problem omitted, wrong-project revert receipt accepted, invalid library mapping hidden. After RED: these three local behaviors corrected; three assertions in the new handoff test files corrected against existing route/mapping/Boolean behavior, each explained in BUILD.md. Whole-file GREEN: renderer 9/0, routes 13/0, browser 9/0; full library 81/0, channel-proof 2/0. Approved old-test numstats remain 80/66 and 3/1. Full webui/build and full integration gate have not run. Isolated mutants next within the authorized 19:38Z hard stop; no completion claim.
|
||||
|
||||
2026-10-23 | Fethry | Row 225 T1 expanded evidence complete (sage-3647). Full library81/0 and channel-proof2/0; final targeted renderer9/0, routes13/0, browser9/0. Isolated kit14/14 assertion kills on507901277, every target restored/hash-checked, candidate hashes unchanged. First attempt stopped at a wait-based failure, recorded as not killed; corrected the new test's repaint wait, baseline9/0, and reran the whole kit. No processes left; lane A returned around19:29Z. Source manifest for Dewey prepared; full webui/build and Sage's integration/scripts gate still pending. Raw logs preserved in scratch, normalized display copies and raw SHA256 provenance in agent evidence.
|
||||
@@ -0,0 +1,170 @@
|
||||
# Row 224 — C2-05b broker lookups and typed tools (#1725)
|
||||
|
||||
Owner: Della; reviewer: Dewey. Assignment sage-2795; scope extension sage-2804.
|
||||
Base: `4f3fbc25a` (Researcher C2-05a). Branch: `della/row224`.
|
||||
Brief: C2-05b in `docs/plans/2026-10-20_case2-rows.md` at `aca16404c`
|
||||
and `51cc0445c`; design section 3, 5.3 M3 and 7.2; C2-05a BUILD rulings.
|
||||
|
||||
## Execution boundary and progress
|
||||
|
||||
Code/docs and local Git only while row 253 holds the gate. No Node (including
|
||||
syntax checks), HTTPS Git, queue claim, test or push until Sage's CLEAR.
|
||||
Tests are written first; their RED/GREEN runs are deferred, not claimed.
|
||||
Queue next is deferred too: its executable uses Node. Sage assigned this row
|
||||
explicitly. After 253 and 216 land, rebase under a new branch name.
|
||||
|
||||
- Pre-flight: query.mjs consumes `{repo,commit,mapping}`; broker cannot import
|
||||
Git/filesystem library code because it is in the sandbox import closure.
|
||||
Runtime injects the host lookup adapter; existing human file/status/sync stay.
|
||||
- Pre-flight: resolved has no project list and a library is never a checkout.
|
||||
Bundle needs an explicit library flag from launcher business configuration.
|
||||
- M3 reading: exact creative base is five lookups plus send_message; retain
|
||||
explicitly granted broker web_search/web_fetch for research/companion under
|
||||
landed C2-08 and sage-2804. No generic spec, task, evidence or decision tools. Residual (mos-1308): those
|
||||
reads lack broker action checks; their typed-tool absence is the only fence
|
||||
for creative roles. M1/M2 deny bash; broker-side refusal is outside this row.
|
||||
Mos confirmed this interpretation in mos-1308; design wording fix belongs to Gosalyn. Other definitions retain their current tools.
|
||||
- Multi-library businesses use an optional project ID; omit it only with one
|
||||
configured library. Universe is required for all five tools, including get.
|
||||
- Missing library uses the brief's `library-not-configured`; argument errors
|
||||
reuse `invalid-request` and C2-05a query codes. No new action or authority.
|
||||
|
||||
## Manifest (draft code, unrun)
|
||||
|
||||
- bus/src/broker.mjs: five lookup verbs, strict args, holder/business rechecks.
|
||||
- bus/src/runtime.mjs: host lookup adapter, stale-before-read sync, served pin.
|
||||
- harness/src/tools.mjs: five schemas; creative exact-set filtering.
|
||||
- harness/src/bundle.mjs: library flag forwarded to typedTools.
|
||||
- cli/src/launcher.mjs: one flag derived from trusted business projects.
|
||||
- bus/tests/library-lookups.test.mjs: real query/broker/socket/runtime cases.
|
||||
- harness/tests/library-tools.test.mjs: schemas, role snapshots, bundle parity.
|
||||
- cli/tests/thread-launch.test.mjs: flag reaches a real launched bundle with no library checkout.
|
||||
- agents/della/work/queue-224/BUILD.md: manifest, rulings and deferred verification ledger.
|
||||
|
||||
## Verification ledger
|
||||
|
||||
No test or syntax-check run yet. Acceptance requires full bus/cli/harness suites
|
||||
and every scripts/test-*.sh on Sage's gate; focused files and affected CLI/
|
||||
seat import-closure tests precede that only in an authorized CLEAR.
|
||||
|
||||
## Coordination
|
||||
|
||||
Huey272 reports no runtime/bundle/launcher overlap. Rocko219 changes launcher
|
||||
THREAD_DEFINITIONS checks only, separate from our buildBundle flag argument.
|
||||
The launcher argument is required: bundle cannot derive library presence from
|
||||
checkout IDs (a library has no agent clone). No role, authority or config schema
|
||||
changes; library project entries are already validated and protected by M9.
|
||||
|
||||
## Mount and pin impact (inspection only)
|
||||
|
||||
No new imports in broker.mjs or tools.mjs; no RUNNER_FILES or sandbox mount
|
||||
addition. runtime.mjs is host-only and already imports library index.mjs.
|
||||
Bundle forwards only a boolean, never a root/mapping/slot/credential. The
|
||||
existing import-closure tests are bus/tests/project-runtime.test.mjs and
|
||||
seat/tests/sandbox.test.mjs. Grep found no literal sha256 pin for runtime.mjs
|
||||
or bundle.mjs; bundle tests derive current source digests through SOURCES.
|
||||
Library sources remain in the existing whole-directory release copy.
|
||||
|
||||
## Runs to schedule only after CLEAR
|
||||
|
||||
Set TMPDIR=/mnt/storage/scratch/tmp. Focused new bus/harness files first,
|
||||
then existing library.test.mjs, library-sync.test.mjs, project-runtime.test.mjs,
|
||||
harness creative/tools/spec-tools/bundle files and affected CLI launch tests.
|
||||
Follow with the required whole bus/cli/harness suites and Sage's full shell gate.
|
||||
No timing claim yet: use the repository/Sage per-file limits and record every
|
||||
command, outcome, elapsed time and timeout before acceptance. No focused skip
|
||||
or pattern run substitutes for whole-file gate evidence.
|
||||
|
||||
## Read-through findings and residuals
|
||||
|
||||
- Library get requires universe although the older design table omitted it;
|
||||
section 3 isolation rule and C2-05a's accepted API are binding.
|
||||
- Query type enum matches mapping.TYPES (five values); manuscript is a distinct
|
||||
lookup and not a find type. Unknown host-path/commit/business/accept fields
|
||||
are refused; explicit project is restricted to this business's library list.
|
||||
- Async reads recheck the live holder and current project revision. A stale
|
||||
read attempts sync, then takes the new served snapshot; a returned fetch
|
||||
failure/divergence keeps serving the old pin. Busy sync uses the current pin.
|
||||
Unreadable state, no served commit or unsafe configuration refuse. Query
|
||||
and sync never hold a SQLite transaction over await.
|
||||
- Processing halt refuses new agent lookup work and rechecks before returning
|
||||
async results. Existing human library file/status/sync behavior is unchanged.
|
||||
- Timer sync and reads still use C2-03a's 60-second contract. No timer tuning or
|
||||
resource-limit schema is introduced by this row. Q222 creative reply settings
|
||||
are in C2-06; no reply guard or writing capability is implemented here.
|
||||
- The generic tool socket's existing 60-second client timeout can precede the
|
||||
server's 120-second Git timeout. This row does not change the generic timeout;
|
||||
a timed-out lookup is read-only and retryable, never a write/outcome claim.
|
||||
- All tests are authored but unrun. No RED, GREEN, syntax, runtime performance,
|
||||
sandbox, broker/harness suite, shell gate, claim or push evidence exists yet.
|
||||
|
||||
## Candidate review checklist (read-through only)
|
||||
|
||||
- [x] Five verbs mapped to real C2-05a methods, all require universe.
|
||||
- [x] Default sole library or explicit owned project; no caller repo/commit.
|
||||
- [x] Human-only file/status/sync gates kept; lookup holder rechecked.
|
||||
- [x] Async socket dispatch includes lookup verbs through LIBRARY_VERBS.
|
||||
- [x] Stale/unknown fetch syncs first; results use current immutable served pin.
|
||||
- [x] Six/eight creative names per mos-1308; other roles retain their tools.
|
||||
- [x] Launcher -> bundle -> typedTools passes presence without a library clone.
|
||||
- [x] No imports added to sandbox source closure; no mount change.
|
||||
- [ ] Execute new tests, RED characterization, full files/suites after CLEAR.
|
||||
- [ ] Rebase after rows 253/216 land, under a new branch name.
|
||||
- [ ] Claim/push only in Sage-authorized gap; Dewey review and Sage gate.
|
||||
|
||||
Candidate preservation commits are explicitly unrun under sage-2795; they are
|
||||
not completion, green-suite or landing claims. No dependencies installed.
|
||||
|
||||
Correction: the first committed-range diff --check found a blank line at EOF
|
||||
in the new library-tools test, missed while it was untracked. Removed in a
|
||||
follow-up local commit; the complete range must be checked again. No tests ran.
|
||||
|
||||
## Shared-log receipts for Sage (sage-2815)
|
||||
|
||||
The shared logs are unchanged from the base in this row. Sage appends these
|
||||
receipts in the landing sweep; they are preserved here, not in the manifest.
|
||||
|
||||
For `BUILD-LOG.md`:
|
||||
|
||||
```text
|
||||
2026-10-22T16:36:30Z | Della row224 C2-05b #1725 preparation | Before: C2-05a queries exist at 4f3fbc25a but no broker lookup verbs or typed tools. Scope: broker/tools plus Sage-authorized runtime/bundle integration, tests and docs. Existing human-only library reads retained; five agent queries require a live holder and trusted configured source. Code/docs only under row253 gate; no Node/test/push/claim. Runtime ownership coordinated with Rocko219/Huey272.
|
||||
```
|
||||
|
||||
For `docs/SESSIONS.md`:
|
||||
|
||||
```text
|
||||
2026-10-22T16:36:30Z | Della (sage-2795 / sage-2804, Q220) | Row 224 C2-05b #1725, della/row224 base 4f3fbc25a | Read both pinned briefs and C2-05a source; preparing broker/typed tools and authorized host runtime/bundle seams. No queue command, Node, HTTPS Git, claim, test or push during Gander gate. Dewey reviewer; Rocko/Huey runtime coordination sent. Tests and integration verification deferred to CLEAR.
|
||||
```
|
||||
|
||||
Local-only draft pins 19c0a7adf/7cf39c390 are superseded by the
|
||||
log-free candidate. Saved as local branch della/row224-draft-with-logs before
|
||||
rebuilding the unpublished candidate commit; no remote history changed.
|
||||
|
||||
## Dewey dewey-0852 and Sage sage-2819 — focused corrections
|
||||
|
||||
B1: inline freshness uses max(lastFetch, lastError.at), not lastFetch alone.
|
||||
An error within 60 seconds counts as a recent attempt, so later lookups serve
|
||||
the old served pin without another fetch. Keep inline sync: the brief requires
|
||||
it and businesses without timed libraries have no sync timer. The extended
|
||||
failed-fetch test makes reads at +1, +59,999 and +60,000 ms (one attempt total),
|
||||
then +60,001 ms (second attempt). All reads must return the old commit.
|
||||
|
||||
Required mutant M-B1: remove lastError.at from recentAttempt. The repeated
|
||||
reads must fail the attempted-count assertion. This mutant has NOT been run.
|
||||
|
||||
Residual (Sage will defer): the first inline fetch in each outage window can
|
||||
still exceed the tool timeout (60 seconds, or the runner broker client’s
|
||||
30 seconds). Bounding that fetch is out of scope for 224. Subsequent reads
|
||||
are paced by persisted lastError.at. No claim of first-read outage latency.
|
||||
|
||||
N1: whole bus, cli and harness suites are required, plus the shell gate.
|
||||
If the new thread-launch integration test skips due to sandboxProblem(),
|
||||
name the skip; a skip is not launcher integration proof.
|
||||
N2: cli/tests/thread-launch.test.mjs now depends on library/tests/helpers.mjs
|
||||
mappingEntry() and library/tests/fixtures/mapping.json. Changes to that fixture
|
||||
must rerun the CLI launch file.
|
||||
N3: harness/tests/library-tools.test.mjs compares the type enum to mapping.mjs
|
||||
TYPES, so adding a mapped type cannot silently leave the typed tool behind.
|
||||
|
||||
These edits are local code/docs only. No Node, RED/GREEN run, mutant, claim,
|
||||
HTTPS Git or push. New local pin goes to Dewey/Mos/Sage for focused review.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Row284 C2-05c — supporting scope and served-tree blob (#1789)
|
||||
|
||||
Owner Della; reviewer Dewey; gate Sage. Build start sage-3658/3660 (10-23).
|
||||
Branch della/row284. Original prerequisite base Honker28346bad7ea3;
|
||||
merge fc05bcbc3 carries22423816cf28 and2164f3fbc25a intact. Only conflict was
|
||||
CLI thread-launch's added footer tests: retained both full test bodies and
|
||||
assertions. No prerequisite code copied. Every prerequisite needs its own
|
||||
review/mutants/landing under mos1351; rebase after283 lands.
|
||||
|
||||
## Execution status
|
||||
|
||||
UNTESTED. No Node, node--check, test or mutant run before Sage CLEAR. First
|
||||
own commit is the authorized old-test migration; the implementation follows.
|
||||
Sage authorizes an immediate untested push. No queue write or shared-log edit.
|
||||
224 stays first;284 is critical to Bubba288. T4 for224 is not a CLEAR yet.
|
||||
|
||||
## Binding readings
|
||||
|
||||
Brief C2-05c at94c33a43c:1737-1789, Q226, sage2918 and mos1330 item3.
|
||||
get defaults canon; explicit playground/supporting opt-in; mismatch null.
|
||||
Sort full outgoing/incoming lists supporting, playground, canon before paging,
|
||||
keep cross-scope links and universe filter. Manuscript stays canon/playground.
|
||||
No all/array scope, write tool, guard, mapping or creative-name-count change.
|
||||
|
||||
## mos-1330 old-test migration (separate commit)
|
||||
|
||||
All paths below are packages/library/tests/query.test.mjs. Baseline file at
|
||||
fc05bcbc3 matches216's file. Source assertions retained; negative calls receive
|
||||
explicit scope too, so universe isolation cannot pass on default-scope denial.
|
||||
|
||||
Call/assertion 348 -> 348:
|
||||
|
||||
```js
|
||||
// OLD
|
||||
const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md' });
|
||||
// NEW
|
||||
const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' });
|
||||
```
|
||||
|
||||
Call/assertion 350 -> 350:
|
||||
|
||||
```js
|
||||
// OLD
|
||||
assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md' }), null);
|
||||
// NEW
|
||||
assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' }), null);
|
||||
```
|
||||
|
||||
Call/assertion 366 -> 366:
|
||||
|
||||
```js
|
||||
// OLD
|
||||
assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path }), null);
|
||||
// NEW
|
||||
assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path, scope: 'playground' }), null);
|
||||
```
|
||||
|
||||
Call/assertion 367 -> 367:
|
||||
|
||||
```js
|
||||
// OLD
|
||||
const own = await get(src(ref), { universe: 'harbor', path });
|
||||
// NEW
|
||||
const own = await get(src(ref), { universe: 'harbor', path, scope: 'playground' });
|
||||
```
|
||||
|
||||
Assertion 349 retained, OLD == NEW:
|
||||
|
||||
```js
|
||||
assert.equal(rin.scope, 'playground');
|
||||
```
|
||||
|
||||
Assertion 368 retained, OLD == NEW:
|
||||
|
||||
```js
|
||||
assert.equal(own.scope, 'playground');
|
||||
```
|
||||
|
||||
The null assertions at350 and366 are the complete OLD/NEW expressions above:
|
||||
expectation remains null. Added one separate no-scope playground-get -> null
|
||||
behavior test. It is unrun and expected to expose old get behavior until the
|
||||
new scope implementation lands. This migration is authorized by mos1330 under
|
||||
mos1272; no check removed or weakened. Dewey checks this list in manifest review.
|
||||
|
||||
## 283 surface and blob choice
|
||||
|
||||
Inspected actual46bad7ea3: optional editor.user is present. reader's parsed
|
||||
file records and index entries still discard tree oid. In owned query.mjs use
|
||||
the existing bounded listTree(repo, servedCommit) to obtain exact path oid,
|
||||
not HEAD and not a user-supplied revision expression. No reader/index edit.
|
||||
Tradeoff: one additional bounded tree listing per get; latency unmeasured.
|
||||
Recheck after rebasing to283's actual landing.
|
||||
|
||||
## Intended owned paths
|
||||
|
||||
query.mjs; broker.mjs lookup branch; tools.mjs lookup schemas/descriptions;
|
||||
library query.test.mjs/query-fixture.mjs; bus library-lookups.test.mjs;
|
||||
harness library-tools.test.mjs; this BUILD.md. Other files are prerequisites.
|
||||
|
||||
## Gate
|
||||
|
||||
Dewey manifest approval, whole library/bus/harness suites and every
|
||||
scripts/test-*.sh on Sage gate. Before review grep all package tests for enum
|
||||
pins and canon/playground pairs. Keep shared logs out; send receipts to Sage.
|
||||
No test or mutant evidence claimed. Manifest/digest follows complete code.
|
||||
|
||||
## Implementation head (UNTESTED, sage-3658)
|
||||
|
||||
- query scope accepts supporting for find/search/get. get defaults canon,
|
||||
mismatch null. Manuscript passes a separate two-value scope allowlist.
|
||||
- get returns blob from listTree(source.repo,index.commit) after scope/universe
|
||||
filtering. The same index.commit supplies the text; source HEAD is irrelevant.
|
||||
- Full outgoing and incoming link lists are stably sorted by supporting,
|
||||
playground,canon before combining/paging. All visible cross-scope links stay;
|
||||
universe/hybrid visibility still applies. Scope metadata isn't added to links.
|
||||
- Broker only adds optional scope to library.get's strict fields. No new verbs.
|
||||
- Typed scope expands find/search/get only; manuscript retains its own two-value
|
||||
enum. Names/M3 counts remain the row224 six/eight, no write tools added.
|
||||
- New tests: book/universe/draft supporting find/search/get; defaults/mismatch;
|
||||
absent supporting prefix; source HEAD advances but each scope's blob equals
|
||||
served:path; 300 outgoing plus300 incoming links ordered across bounded pages,
|
||||
retaining cross-scope links and dropping other-universe links; hybrid source
|
||||
stays canon-only; no all/array scope; manuscript rejects supporting; broker
|
||||
scope/blob forwarding; typed enums and unchanged names. All unrun.
|
||||
|
||||
## Additional old assertion pin (brief Gate)
|
||||
|
||||
harness/tests/library-tools.test.mjs:49 at23816cf28 (line49, unchanged in this head):
|
||||
OLD find scope enum ['canon','playground']; NEW ['canon','playground','supporting'].
|
||||
This is the exact scope-enum pin the brief directs updating. Existing six/eight
|
||||
name snapshots and TYPES equality assertion are unchanged. The new manuscript
|
||||
pin intentionally remains ['canon','playground']. No other two-scope test pin
|
||||
was found by rg over packages/*/tests at this build's prerequisite merge.
|
||||
|
||||
## Proposed mutation evidence (reviewer confirms final set)
|
||||
|
||||
All unrun. Require assertion failures with whole-file runs and pristine restore:
|
||||
M1 remove supporting from query SCOPES -> supporting lookup cases.
|
||||
M2 drop get's requested-scope comparison -> default/mismatch null cases.
|
||||
M3 use source HEAD for blob lookup -> served-tree/advanced-HEAD equality case.
|
||||
M4 remove link rank sort -> multi-page outgoing/incoming expected order.
|
||||
M5 restrict links to requested scope -> cross-scope link counts/order case.
|
||||
M6 drop sees() from visible/link filtering -> second-universe/hybrid cases.
|
||||
M7 allow supporting in manuscript validator -> manuscript refusal case.
|
||||
M8 omit get scope from broker fields -> real broker supporting get case.
|
||||
M9 widen typed manuscript enum -> typed-scope separation case.
|
||||
|
||||
## Inspection and pending gate
|
||||
|
||||
No production imports change in broker/tools, so no new sandbox closure file
|
||||
or mount. query.mjs is host library code and adds existing reader.listTree;
|
||||
no new package/export/copy-path file. rg found the one old find scope pin plus
|
||||
the intentional new manuscript two-scope pin. Whole library/bus/harness and
|
||||
shell gates remain required. CLI integration/merge context is prerequisite224/
|
||||
219, not this row's source delta; any changed gate expectation goes to Sage.
|
||||
Node checks, tests, mutants and elapsed-time evidence remain absent pending
|
||||
CLEAR. The first test-migration commit and this implementation are distinct.
|
||||
|
||||
Sage sweep receipt: Della row284 builds on28346bad7ea3 plus intact224 merge;
|
||||
mos1330 old-test migration separate, supporting scope/blob implementation and
|
||||
fixtures authored, completely UNTESTED. No shared-log/queue write or live action.
|
||||
|
||||
## Current assertion anchors after implementation imports
|
||||
|
||||
The separate migration commit0ed4fbab9 retains its complete OLD/NEW text
|
||||
above. Adding only the fs/path imports in the implementation shifts anchors:
|
||||
|
||||
query.test.mjs:130: `const blendCh2 = await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path: 'Book One/Chapters/Chapter 02.md' });`
|
||||
query.test.mjs:350: `const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' });`
|
||||
query.test.mjs:351: `assert.equal(rin.scope, 'playground');`
|
||||
query.test.mjs:352: `assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' }), null);`
|
||||
query.test.mjs:368: `assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path, scope: 'playground' }), null);`
|
||||
query.test.mjs:369: `const own = await get(src(ref), { universe: 'harbor', path, scope: 'playground' });`
|
||||
query.test.mjs:370: `assert.equal(own.scope, 'playground');`
|
||||
|
||||
No result/null assertion changed; scope arguments carry the explicit opt-in.
|
||||
|
||||
## Dewey dewey-0941 static review — round2
|
||||
|
||||
Static scope fc05bcbc3..47db04eba CLEAN; not a final verdict or test evidence.
|
||||
M1-M9 accepted as the final set. Optional M10 not added to this gate.
|
||||
N2 taken: keep the existing canon-path supporting-scope mismatch assertion,
|
||||
and ADD get(Book One/Support/Notes/Helper.md,scope:supporting) -> null under
|
||||
the default mapping with no supporting prefix. The supporting-positive test
|
||||
already returns this same path under withSupporting, so the added probe
|
||||
pins the missing-prefix case directly. No assertion removed or weakened;
|
||||
mos1330's separate migration commit and its retained assertions unchanged.
|
||||
|
||||
N1 proposed DEFERRED/follow-up for Sage: retain each tree oid in reader/index
|
||||
entries so get can return blob without cat-file/whole ls-tree -r -l per page.
|
||||
Current get's result is pinned correctly, but per-page cost grows with the
|
||||
library. No performance measurement yet. This optimization needs reader/index
|
||||
ownership and separate acceptance/performance evidence; do not silently expand
|
||||
284's manifest or change its served-snapshot contract for it.
|
||||
|
||||
No Node, tests, mutants or syntax checks run in this round; hold still applies.
|
||||
@@ -1,376 +0,0 @@
|
||||
# Row 225 (C2-10b) Console chat: replies, citations, write notices, status line
|
||||
|
||||
Owner: Morgana, from sage-3582 (18:53Z 10-23). Base: origin/refactor 45dca69f6. Branch: morgana/row225.
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md, C2-10b. Design: agents/gosalyn/work/case2/DESIGN.md 7.4,
|
||||
5.2 (the reply schema and the check result) and the write notice and `library.revert` (about line 286).
|
||||
|
||||
## Ruling on the dependency gap (sage-3591)
|
||||
|
||||
225 builds against the design contracts. `renderReply` stays pure, and the caller supplies the answers
|
||||
it can't know. The revert route proxies to the bus verb and shows the refusal code until the verb
|
||||
exists. Landing order: 225 lands after 217 (C2-06), not after 288 (C2-02). Sage adds `after 217` to
|
||||
row 225. Before 288 lands, the Revert button gets the bus's refusal and shows its code, as the brief
|
||||
allows.
|
||||
|
||||
## A1. The check result I assume (to 217, Launchpad)
|
||||
|
||||
The broker stores one check result with each agent-to-human message the guard passes. The Console reads
|
||||
it on the message row that `/api/chat/threads/<id>/messages` returns, under the key `check`:
|
||||
|
||||
```json
|
||||
{
|
||||
"check": {
|
||||
"citations": [
|
||||
{ "scope": "canon", "agent": null },
|
||||
{ "scope": "supporting", "agent": "acme/scribe" },
|
||||
{ "scope": "web", "agent": null }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `check` is `null` or absent on a message the guard didn't check: a human's message, a free-text reply,
|
||||
a notice, or a message from before 217.
|
||||
- `check.citations` has one entry per entry of the body's `citations`, in the same order.
|
||||
- `scope` is one of `canon`, `playground`, `supporting` and `web`. A url cite is `web`.
|
||||
- `agent` is the `Mosaic-Agent` trailer value, `<business>/<instance>`, from
|
||||
`git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>`, or `null` when the
|
||||
file's last commit at `commit` carries none. A url cite has `agent: null`. The Console shows the
|
||||
agent-written label when `agent` is a nonempty string, and names the agent.
|
||||
|
||||
The Console trusts nothing in the body for scope or label. A cite with no matching check entry renders
|
||||
with no badge and no label. It doesn't fall back to a scope it worked out itself.
|
||||
|
||||
If 217 uses other names, I change mine to theirs before either of us pushes (sage-3591 item 1).
|
||||
|
||||
## A2. The revert verb I assume (to 288, Honker)
|
||||
|
||||
- Verb: `library.revert`. A human verb (design M4): from an agent session it refuses `human-required`,
|
||||
like `library.status` (`broker.mjs:5133`).
|
||||
- Args: `{ "project": <business project name>, "commit": <40 lowercase hex> }`. The design writes
|
||||
`library.revert {commit}`. `project` is added to match the other library verbs, and like theirs it is the
|
||||
key of `business.projects` (`mirror.mjs:31`), which is the chat project's `repo` link, not its UUID. Both are required, and no other key
|
||||
is allowed (`invalid-request`), as the other library verbs check with `keys(args, ...)`.
|
||||
- Result: `{ "project", "path", "reverted": <the commit given>, "commit": <the new commit> }`.
|
||||
- Refusal codes the Console shows by name:
|
||||
- `library-revert-not-agent`: `commit` has no `Mosaic-Agent` trailer for this business, or isn't
|
||||
reachable from the served commit.
|
||||
- `stale-base`: a later commit changed that path.
|
||||
- `human-required`, `invalid-request`, `project-not-configured`.
|
||||
- Before 288 lands, the bus refuses the verb (expected `unknown-verb`, `broker.mjs:6578`), and the Console
|
||||
shows that code.
|
||||
- HTTP status in the Console (`packages/webui/src/bus.mjs` `busStatus`): `stale-base` and
|
||||
`library-revert-not-agent` map to 409. Every other code keeps the existing mapping.
|
||||
|
||||
## A3. The status line
|
||||
|
||||
`GET /api/library/status?project=<id>` proxies `library.status {project}` through the existing
|
||||
`libraryStatus` option. `cli.mjs:110` already passes that option, but `startServer` doesn't read it.
|
||||
The status line shows:
|
||||
|
||||
- the served commit (7 characters) and its age;
|
||||
- `library-diverged` when the sync state names it;
|
||||
- the fetch error code;
|
||||
- the reply limits in force, and the keys that fell back;
|
||||
- the count of R-problems, linking to the list.
|
||||
|
||||
It reads the fields `readSyncState` returns at the base (`packages/library/src/mirror.mjs:232`). It shows
|
||||
any field 217 adds (the limits) only when present.
|
||||
|
||||
## A4. "Changed since" (ruled sage-3596)
|
||||
|
||||
Sage ruled that 217 supplies `last`. On a `thread.messages` row that carries `check`, each path cite's
|
||||
entry gets `last`, the file's last commit at the served tip, read when the message is read. When `last`
|
||||
is absent the Console marks nothing changed, and a test pins that.
|
||||
|
||||
One more field is needed, raised in morgana-0200. The cited `commit` is a library commit, not the
|
||||
file's own last commit, so a cite made at a commit that didn't touch the file would differ from `last`
|
||||
while the file is unchanged. I asked 217 to also store `file`, the file's last commit at the cited
|
||||
commit. That's `%H` from the same `git log -1 <commit> -- <path>` call that reads the trailer, so it
|
||||
costs no extra git call. The Console marks a cite changed when both `file` and `last` are 40-hex and
|
||||
differ. The cited commit is reachable from the served one (5.2), so `file` is an ancestor of `last`,
|
||||
and differing means newer. A missing or malformed `file` or `last` marks nothing.
|
||||
|
||||
The entry the Console reads, in full: `{ scope, agent, file?, last? }`.
|
||||
|
||||
## A5. `no-cited-answer` retires
|
||||
|
||||
`renderItems` and its `NOTICES` (`no-cited-answer`, `no-answer`, `turn-failed`) go. 217 sends the
|
||||
runner's fixed texts as plain bodies (design 5.2, "Runner texts"), and renderReply shows those as free
|
||||
text. Design 7.4 names no other notice than `library-write`, so nothing is lost (sage-3596 item 3).
|
||||
|
||||
## Files
|
||||
|
||||
Both deviations below were accepted in sage-3596 and are named in the manifest for Dewey. The brief owns these:
|
||||
|
||||
- `packages/library/src/render.mjs`: `renderReply` replaces `renderItems`.
|
||||
- `packages/library/src/index.mjs`: the export.
|
||||
- `packages/library/README.md`: the section.
|
||||
- `packages/library/tests/render.test.mjs`.
|
||||
|
||||
It also owns `bus.js`, its stylesheet, `serve.mjs` (only the two routes) and new webui tests. Two
|
||||
deviations, both recorded here:
|
||||
|
||||
- The chat view's message list is `packages/webui/src/public/chat.js:85` (`window.MosaicChat`), not
|
||||
`bus.js`. The reply, notice and Revert rendering goes in `chat.js` and `chat.css`. The status line
|
||||
goes in the chat thread view, so it is also in `chat.js`.
|
||||
- `packages/webui/src/console-bus.mjs` and `cli.mjs` each get one `libraryRevert` line, beside the
|
||||
existing `libraryStatus` wiring.
|
||||
|
||||
## Overlap with row 220 (C2-10a, Researcher, 3ef6da90d)
|
||||
|
||||
220 changes one line each in the webui tests discord.test.mjs, fixture.mjs, history-fixture.mjs,
|
||||
relaunch.test.mjs and return-flow.test.mjs. Its other paths are in control-board, conversation, discord
|
||||
and business. 225 doesn't touch those five files or those packages. Its webui tests are new files.
|
||||
|
||||
## Handoff to Fethry (mos-1501, Q245)
|
||||
|
||||
The row moved to Fethry (Codex, fethry/row225). Everything below is **untested**: no suite or single file
|
||||
has run on it, since a run needs Sage's CLEAR. Each changed file passes `node --check`.
|
||||
|
||||
Done in the handoff commit:
|
||||
|
||||
- `render.mjs` `renderReply(body, {check, changed, name})`; index export; README section;
|
||||
`render.test.mjs` rewritten. `renderItems` is gone from every package.
|
||||
- `serve.mjs`: `/render.mjs` static entry (owner and user file maps), `GET /api/library/status`
|
||||
(one `project` query, answer must name that project), `POST /api/library/revert` (save's boundary,
|
||||
strict `{project, commit}`, refusal shows the code, lost or unusable answer is `outcome-unknown`,
|
||||
never retried). Both are nulled for a granted user, and `userRoute` keeps them owner-only. The tester
|
||||
listener refuses `libraryRevert`.
|
||||
- `bus.mjs`: `library-revert-not-agent: 409`. That re-pins `packages/webui/src/bus.mjs` in
|
||||
`packages/bus/tests/channel-proof.test.mjs`, with a comment line, as row 254 did. It's a third deviation,
|
||||
in another package's test.
|
||||
- `console-bus.mjs` and `cli.mjs`: `libraryRevert`, beside `libraryStatus`.
|
||||
- `chat.js`: dynamic `import('/render.mjs')` (CSP is `script-src 'self'`); body text until it loads.
|
||||
Cites with a scope badge, the agent-written label, "changed since" from `file`/`last`, links only for
|
||||
https with `rel="noopener noreferrer"`. A notice gets a Revert button behind `confirm()`. The library
|
||||
status line goes under the thread heading.
|
||||
- `chat.css`: cite, badge, notice and status styles.
|
||||
- New tests: `packages/webui/tests/library-routes.test.mjs` and
|
||||
`packages/webui/tests/chat-replies-browser.test.mjs`. 220's five files are untouched.
|
||||
|
||||
Decisions made in the build:
|
||||
|
||||
- The library name is the chat project's `repo`. A project with no repo reads no status and its
|
||||
Revert does nothing.
|
||||
- These codes hide the status line: `project-not-declared`, `library-mapping-invalid`,
|
||||
`project-not-configured`, `not-configured` and `owner-only`.
|
||||
- Otherwise every code-repo thread, and every granted user, would show "unavailable".
|
||||
- Cost: a broken library mapping also hides the line. `library.status` from the CLI still shows it.
|
||||
- "Age" is the time since `lastFetch` ("fetched 5 min ago"), not the served commit's age, which the
|
||||
status doesn't carry.
|
||||
- The limits fields are assumed to be `replyLimits {requireCitation, citationsPerReply}` and
|
||||
`replyLimitsFallback [keys]`, and are shown only when present. **Not agreed with 217.** Confirm with
|
||||
Beakley and rename.
|
||||
- A notice body's shape is all the renderer checks. An agent in a business without a library could
|
||||
send a body that looks like a notice. The page would show it, but `library.revert` still refuses it.
|
||||
|
||||
Left:
|
||||
|
||||
1. Get CLEAR and run the webui and library suites, `channel-proof.test.mjs`, and the two new files whole.
|
||||
The browser test's selectors and waits have never run.
|
||||
2. Agree the limits field names with 217 (Beakley).
|
||||
3. Granted-user R6 G5 (`granted-user.test.mjs:252`) lists owner verbs by hand. Add `library.revert`
|
||||
once 288 lands.
|
||||
4. BUILD.md: the mutant list and the mos-1275 timeout plan.
|
||||
5. The manifest names the three deviations: chat.js and chat.css in place of bus.js, the console-bus and
|
||||
cli lines, and the channel-proof re-pin.
|
||||
|
||||
## Fethry continuation (2026-10-23, Q245)
|
||||
|
||||
Owner: Fethry, thread 14f80e3c-ace7-4793-a577-e1275ca09b0b. Branch fethry/row225.
|
||||
This file carries Morgana's complete BUILD.md above unchanged, recovered from 9a131e956
|
||||
(base 45dca69f6). Morgana stopped editing 225 at that handoff. Issue #1726; Dewey reviews.
|
||||
Sage owns queue moves, test slots, gate and landing. No Node/tests until Sage CLEAR
|
||||
(sage-3611); commits/pushes before the gate are explicitly Untested. No credential files read.
|
||||
|
||||
### Confirmed interfaces and rulings
|
||||
|
||||
- Beakley beakley-0004 confirms ordered check.citations {scope, agent, file, last}; a missing
|
||||
or malformed file/last means no changed marker. Fetched age accurately describes lastFetch.
|
||||
- Beakley's concrete library.status shape is replyLimits:
|
||||
{requireCitation, citationsPerReply, fallback: [keys], problem: 'reply-limits-unreadable' | null}.
|
||||
The inherited top-level replyLimitsFallback assumption is superseded. Show nested fallback and
|
||||
problem, with a browser regression test for each. No broker or policy change in this row.
|
||||
- Bubba bubba-0002 confirms A2 and the exact broker notice shape. fethry-0005 forwards the
|
||||
project-key correction: project is chat project.repo, not the chat UUID.
|
||||
- Keep the inherited ordinary-code-repo/granted-user status hiding behavior for now, but a
|
||||
library-mapping-invalid error should stay visible: hiding an invalid mapping hides diagnosis.
|
||||
Confirm this local UI choice through regression coverage and tell Sage/Dewey.
|
||||
|
||||
### Continuation plan and acceptance evidence
|
||||
|
||||
Spec: docs/plans/2026-10-20_case2-rows.md C2-10b; design 5.2 and 7.4;
|
||||
standing rulings sage-3591, sage-3596 and sage-3611. Implement inline in this existing isolated
|
||||
T3 worktree; use the existing candidate and tests, never restart the implementation.
|
||||
|
||||
1. Static audit the handoff against the contracts and inherited tests. Add failing coverage for
|
||||
nested limits fallback/problem, wrong-project revert receipts, missing file/last and invalid
|
||||
mapping visibility. Existing tests require Mos approval through Sage before further edits.
|
||||
2. On Sage CLEAR, run the new files whole and library renderer tests with per-test deadlines;
|
||||
record RED before changing uncovered behavior. Fix only demonstrated local defects. Run
|
||||
targeted GREEN, then the full relevant suites only within the authorized slot.
|
||||
3. Record exact commands, timings and output paths. Prepare mutant kill list below; run only on
|
||||
CLEAR, using isolated copies so the candidate is never temporarily changed for mutation.
|
||||
4. Freeze a manifest containing all source/test/doc changes and all three handoff deviations.
|
||||
Push only fethry/*, as additional commits. Send exact head/results to Dewey and Sage; cc Mos
|
||||
on pushes/blockers. Sage performs queue moves, gate and landing. No merge/release/live changes.
|
||||
|
||||
### Existing test changes requiring Mos OK through Sage (mos-1272)
|
||||
|
||||
Diff against 45dca69f6, inherited from Morgana 9a131e956:
|
||||
|
||||
| File | Added | Removed | Reason |
|
||||
| --- | ---: | ---: | --- |
|
||||
| packages/library/tests/render.test.mjs | 80 | 66 | Brief requires retiring renderItems and replacing its tests with renderReply coverage |
|
||||
| packages/bus/tests/channel-proof.test.mjs | 3 | 1 | bus.mjs gains library-revert-not-agent:409; refresh that source hash and document why |
|
||||
|
||||
No further edits to these files pending approval. No edits to row220's five tests.
|
||||
The new webui test files are additions against the row base.
|
||||
|
||||
### Timeout plan (mos-1275)
|
||||
|
||||
Every Node test run uses --test-concurrency=1 and --test-timeout=30000, under an outer timeout.
|
||||
New library/route files: outer 120s each; new browser file: outer 240s. Full library and channel-proof:
|
||||
outer 180s; full webui: outer 900s, MOSAIC_REQUIRE_EDITOR_BUILD=1. Full webui requires its own
|
||||
packages/webui npm ci per agents/README.md and Sage CLEAR. Output redirects to an evidence log;
|
||||
inspect tail, failure names and summary, never claim green from an exit alone. A timeout is a
|
||||
failed run: stop, report the exact command/phase/last output, and diagnose before retrying.
|
||||
Sage's gate owns every scripts/test-*.sh; no implicit slot or test authority from this plan.
|
||||
|
||||
### Mutant plan (pending CLEAR)
|
||||
|
||||
- M1: drop a path badge or the supporting label; cite browser assertions must fail.
|
||||
- M2: allow http/javascript links; renderer and browser safe-link assertions must fail.
|
||||
- M3: compare cited library commit to last instead of file to last, or treat a missing file/last as
|
||||
changed; changed-since assertions must fail, including same-file/different-library-commit.
|
||||
- M4: ignore nested fallback/problem; status browser tests must fail.
|
||||
- M5: remove a revert fence (send, identity, exact Origin or Fetch Metadata), strict key/commit
|
||||
validation or wrong-project receipt validation; route tests must fail with zero unexpected calls.
|
||||
- M6: retry a refused/unknown revert; single-call route/browser assertions must fail.
|
||||
- M7: remove granted-user library-route fence; owner-only route coverage must fail.
|
||||
|
||||
Progress: handoff and interfaces recovered; candidate untested. No completion claim.
|
||||
|
||||
Static evidence correction: the first pushed patch artifact used ordinary context lines, whose
|
||||
single-space blank context lines make git diff --check report whitespace in the artifact. Regenerated
|
||||
inherited-old-tests.patch with --unified=0; it is the same exact two-file change, applicable with
|
||||
git apply --unidiff-zero. No source or existing test changed for this correction.
|
||||
|
||||
Mutation kit authored at mutants.py (11 concrete mutations across M1-M7), Python AST parse only.
|
||||
It archives the committed packages plus their tracked fixture dependencies into disposable snapshots,
|
||||
links the existing dependency install read-only in practice (tests do not install), enforces exactly
|
||||
one mutation anchor and requires an assertion failure for a kill. It records source hashes before
|
||||
and after. It has not run; anchors for the planned fixes intentionally require those fixes first.
|
||||
No Node/tests/build/browser work has run in Fethry's session pending Sage CLEAR.
|
||||
|
||||
### Old-test approval and preservation condition (mos-1510, sage-3623)
|
||||
|
||||
Mos approves render.test.mjs +80/-66 and channel-proof.test.mjs +3/-1 exactly. No additional edits
|
||||
are permitted to either file without a new Mos OK through Sage. The condition on the renderer:
|
||||
|
||||
- The old malformed-body fixed-line behavior no longer applies. Design 5.2 now says any body
|
||||
outside the cited-object schema is free text whose text is the whole body. Rejecting it into a
|
||||
fixed line would hide human/free-text/older messages and the four fixed runner texts. The new
|
||||
render.test.mjs tests “free text renders as itself with no citations” and “a body without the
|
||||
reply or notice shape is free text: the whole body, no citations” explicitly cover this new rule.
|
||||
- The old no-options path-badge behavior still applies. The new render.test.mjs test “without a
|
||||
check result a path cite has no badge, no label and is never changed” covers it, and the browser
|
||||
test checks an unchecked path cite has no badge/label. No path inference fallback was added.
|
||||
|
||||
Dewey checks both against the frozen old-test delta. The two numstats remain 80/66 and 3/1.
|
||||
|
||||
### Test slot and shared protocol (sage-3623, beakley-0006)
|
||||
|
||||
Targeted lane A slot is planned around 19:47Z, after Gosalyn's 226 run and Sage's queue gap.
|
||||
Wait for explicit CLEAR; the scheduled time alone grants nothing. Hard stop 20:00Z for Sage's
|
||||
226 gate. Boyd's lane B bus/cli/business work is disjoint and does not count as a busy lane A.
|
||||
The three requested files are the library renderer test and the two new webui files, whole/serial.
|
||||
|
||||
Beakley confirms reply-protocol.mjs exports only GUARD_CODES (the seven guard refusal codes) and
|
||||
FIXED_TEXTS {empty, failed, uncited, tooLong}. It has no scope/notice constants or validator.
|
||||
renderReply defines none of those codes or runner texts: it already renders the fixed bodies as free
|
||||
text. It therefore needs no protocol import and no extra browser module route (beakley-0006).
|
||||
Do not add unused imports or redefine those shared codes/texts later.
|
||||
|
||||
2026-10-23T19:21:07Z dependency update (bubba-0006): Bubba's first static row288 push is
|
||||
origin/bubba/row288 c867cada9, explicitly Untested with no Node run. A2 library.revert uses the
|
||||
business.projects key; broker notice retains the exact agreed body. Bubba adds librarySave only
|
||||
in console-bus.mjs/cli.mjs and leaves libraryRevert wiring owned by 225. Preserve both callback
|
||||
changes when Sage integrates. No dependency merge or test run performed here; 225 still lands
|
||||
after 217 and may show unknown-verb until 288 lands.
|
||||
|
||||
## T1 targeted RED to GREEN (sage-3641, 2026-10-23T19:23Z)
|
||||
|
||||
All three files ran whole and serial, with TMPDIR=/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2,
|
||||
--test-concurrency=1 --test-timeout=30000 and outer timeout -k 10 (120s renderer, 120s routes,
|
||||
240s browser). Initial renderer 9/0 (0.105s), routes 9/3 (0.226s), browser 5/3 (5.630s).
|
||||
Final renderer 9/0 (0.105s), routes 12/0 (0.212s), browser 8/0 (5.284s). Logs in evidence/.
|
||||
|
||||
Production fixes after RED:
|
||||
- Reject a library.revert receipt with the wrong project as outcome-unknown; no retry.
|
||||
- Read replyLimits.fallback and display replyLimits.problem per beakley-0004/0006.
|
||||
- Show library-mapping-invalid in the status line; its hidden-status branch is covered by the
|
||||
browser refusal matrix once the nested-limits assertion passes. This diagnosis was made statically;
|
||||
the first RED exits that matrix at the preceding fallback assertion, so visibility itself has only
|
||||
a GREEN run, unlike the fallback/problem and wrong-project fixes.
|
||||
|
||||
Three assertion corrections in NEW handoff files (not pre-existing tests, per sage-3647):
|
||||
1. library-routes.test.mjs: POST /api/library/status in preview mode expects 403, because the existing
|
||||
global preview write fence runs before the GET-only route and returns read-only. The route remains
|
||||
GET-only and no callback fires on that POST. Do not weaken or bypass the fence to get 405.
|
||||
2. library-routes.test.mjs: project-not-declared expects 400, which busStatus already maps at the
|
||||
row base. The inherited 502 expectation contradicted that existing map. No production map change.
|
||||
3. chat-replies-browser.test.mjs: coerce the whole “img element OR window.injected” expression to a
|
||||
Boolean. With no injected node/code the old expression returns undefined, not false. Assertions
|
||||
still require neither DOM injection nor executed handler. All preceding cite/link assertions pass.
|
||||
|
||||
Frozen pre-existing test deltas still render.test.mjs 80/66 and channel-proof.test.mjs 3/1.
|
||||
No Node/Chromium process with this slot TMPDIR remained after targeted tests (checked /proc).
|
||||
|
||||
Expanded CLEAR sage-3647: full library, then channel-proof whole, then isolated mutants that fit;
|
||||
still hard stop 19:38Z. No full webui suite, npm ci or editor build authorized in this window.
|
||||
|
||||
Expanded-slot evidence: full library 81/0 (9.179s), channel-proof 2/0 (0.573s), whole/serial,
|
||||
--test-timeout=30000, outer timeout -k 10 180, same TMPDIR. Logs copied to evidence/.
|
||||
To exercise invalid-mapping visibility independently, added its own browser case and temporarily
|
||||
restored only the hidden-mapping branch. Whole browser RED: 7/2 (8.810s); the direct assertion sees
|
||||
null instead of the diagnostic, and the refusal-matrix wait also fails. Finally restored the exact
|
||||
corrected source before the next run. This supersedes the earlier visibility “GREEN only” note.
|
||||
|
||||
Final targeted coverage before mutation: browser 9/0 (6014.970ms runner duration), routes 13/0.
|
||||
Added an independent send-off test with a configured libraryRevert callback: absence of --send
|
||||
still refuses with zero callback calls. This makes the send fence independently mutation-testable.
|
||||
The kit now has 14 concrete mutations, including agent-label, unreadable-limits problem and send.
|
||||
Each isolated target is restored to its candidate SHA256, and worktree hashes are checked before
|
||||
continuing to the next mutant (sage-3647). Runner uses TAP so assertion failures can be distinguished
|
||||
from timeouts/import/process failures. Full webui/build and the integration gate remain pending.
|
||||
|
||||
Dependency update beakley-0008: row217 first push origin/beakley/row217 c25f5b8da is UNTESTED,
|
||||
continued from Launchpad e25ef0373. No dependency merge here; its protocol/module mount belongs to
|
||||
217 and renderReply needs none of those codes/texts. Integration acceptance remains Sage's gate.
|
||||
|
||||
First isolated-mutant attempt stopped honestly at M3-wrong-commit: M1 badge, M1 agent label and M2
|
||||
https-only link mutants were killed by assertions; M3 failed via the browser's expected-state wait,
|
||||
so the strict runner did NOT count it as a kill. Every target restored/hash-checked and candidate
|
||||
hashes unchanged. Logs retained in evidence/mutants-attempt1/. Corrected the new browser test to wait
|
||||
for a completed repaint (independent of desired output), then assert changed-marker count. Whole
|
||||
browser baseline remains 9/0. No production change. Re-run the whole isolated kit against this test.
|
||||
|
||||
## T1 complete: isolated mutants and source-review handoff
|
||||
|
||||
sage-3647 final kit on source candidate 507901277: 14/14 assertion kills, no timeout/import/process
|
||||
failure counted. Each mutant matched one anchor, restored the target SHA256 and checked the three
|
||||
candidate source hashes before the next. evidence/mutants-final/summary.json records every result;
|
||||
final worktreeUnchanged=true. Logs name the failing tests, including M3's direct assertion after the
|
||||
refresh wait correction. /proc inspection found no remaining Node/Chromium process with the slot
|
||||
TMPDIR. Returned lane A to Sage as fethry-0016 at about19:29Z, before the19:38Z hard stop.
|
||||
|
||||
Checked-in TAP display copies remove trailing whitespace so the diff is clean. Their raw SHA256
|
||||
values are recorded in evidence/raw-log-provenance.json; original byte-for-byte logs remain under
|
||||
/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2. This changes evidence presentation only, never tests.
|
||||
|
||||
MANIFEST.md and manifest.r1.sha256 freeze the13 changed package files for Dewey. All three handoff
|
||||
deviations and Mos1510's two preservation conditions are explicit. Old-test numstats remain80/66 and3/1.
|
||||
No full webui/npm ci/build was run; Sage explicitly reserved it for a later window. Full package/script
|
||||
and dependency integration gate remains required before landing. No queue move, merge or live action.
|
||||
@@ -1,67 +0,0 @@
|
||||
# Row 225 review packet — C2-10b (#1726)
|
||||
|
||||
Owner Fethry (14f80e3c), reviewer Dewey (84fb346c). Base 45dca69f6.
|
||||
Source candidate 507901277 continues Morgana 66f7becf2 and 9a131e956; no rewritten history.
|
||||
Status: targeted tests and full library green; full webui/build and Sage's full gate still pending.
|
||||
This packet requests source/manifest review, not landing or a full-green claim.
|
||||
|
||||
## Behavior and contracts
|
||||
|
||||
The Console shows reply text and citations with path/line/quote, scope and agent-written labels,
|
||||
marks a changed file only with valid differing check.file/check.last, links only https URLs, shows
|
||||
write/delete notices and explicit Revert results, and displays library status, fetch age, divergence,
|
||||
fetch errors, nested replyLimits metadata and R-problems. Invalid library mappings remain visible.
|
||||
Revert validates {project,commit}, preserves refusal codes and reports uncertain/wrong-project
|
||||
receipts as outcome-unknown without retry. The project is chat project.repo (business.projects key).
|
||||
|
||||
A1-A5 are in BUILD.md. Dependency contracts: row217 ordered check.citations and nested replyLimits;
|
||||
row288 A2 revert result/notice. No dependency merge in this candidate. Landing order is 217 then225;
|
||||
unknown-verb is expected until288 lands. Beakley's GUARD_CODES/FIXED_TEXTS need no renderer import:
|
||||
renderReply defines neither and renders those fixed bodies as free text.
|
||||
|
||||
## Frozen implementation and tests
|
||||
|
||||
manifest.r1.sha256 freezes the 13 changed package paths, including the two approved existing-test
|
||||
changes. Check from repository root with `sha256sum -c agents/fethry/work/queue-225/manifest.r1.sha256`.
|
||||
All other changed files are BUILD/session records, handoff/approval evidence and this review packet.
|
||||
No role, policy, schema, secret, live instance, deployment or queue change is included.
|
||||
|
||||
Three departures from the brief, carried from Morgana's handoff:
|
||||
|
||||
1. Chat rendering/status live in public/chat.js and chat.css, where the message list is, instead of
|
||||
bus.js and its stylesheet (sage-3596). serve.mjs serves the same pure render.mjs module to owner
|
||||
and granted-user pages; no duplicated browser renderer and no generated asset.
|
||||
2. console-bus.mjs and cli.mjs wire libraryRevert beside libraryStatus (sage-3596). --send still
|
||||
independently gates Revert. Preserve row288's separate librarySave wiring at integration.
|
||||
3. webui/src/bus.mjs maps library-revert-not-agent to409; bus/tests/channel-proof.test.mjs re-pins
|
||||
only that source's hash and documents why. Mos read and approved this exact +3/-1 (mos-1510).
|
||||
|
||||
Existing-test authority: Mos1510 via Sage3623 approves render.test.mjs +80/-66 and channel-proof +3/-1.
|
||||
Both exact numstats are unchanged. The malformed-body fixed-line rule is superseded by design5.2's
|
||||
whole-body free text, covered by two replacement renderer tests. The old no-options path-badge rule
|
||||
is retained and covered by the replacement test plus unchecked-cite browser coverage. BUILD.md
|
||||
explains these two preservation conditions and the three new-file assertion corrections in detail.
|
||||
|
||||
## Verification and remaining gate
|
||||
|
||||
Evidence logs are under evidence/; commands, deadlines, failures and corrections are in BUILD.md.
|
||||
Targeted whole-file tests: renderer9/0, routes13/0, browser9/0. Full library81/0, channel-proof2/0.
|
||||
No skips/cancellations. Invalid-mapping visibility also has an independent RED then GREEN.
|
||||
All 14 isolated mutants were killed by assertions on source candidate507901277; each mutation
|
||||
matched one anchor, restored its source hash, and verified unchanged candidate hashes before the next
|
||||
mutation. Summary: evidence/mutants-final/summary.json. The first attempt stopped at an expected-state
|
||||
wait (not counted as a kill), then the new browser test was improved and the whole kit rerun.
|
||||
Raw logs remain under the scratch root; checked-in display copies trim trailing whitespace and record
|
||||
raw digests in evidence/raw-log-provenance.json.
|
||||
|
||||
Before landing, Sage still runs full webui with MOSAIC_REQUIRE_EDITOR_BUILD=1 after the worktree-local
|
||||
webui npm ci, every applicable package/script suite and integration against217. No full webui/build
|
||||
in T1 (sage-3647). Dewey approval and Sage's gate/landing remain required; queue moves belong to Sage.
|
||||
|
||||
Static audit: the channel-proof source pin matches webui bus.mjs; no hash pin of bus.js, serve.mjs
|
||||
or library render.mjs was found. packages' only remaining renderItems match is an explanatory comment
|
||||
in the frozen replacement test; no runtime export, import or call remains. No no-cited-answer match.
|
||||
|
||||
Review focus: stored check metadata versus untrusted body, missing/malformed file/last, https-only
|
||||
links and inert hostile text, Revert authority/receipt uncertainty, async route/sign-out isolation,
|
||||
and status diagnostics without owner transport leakage to a granted user.
|
||||
@@ -1,64 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 704.129155
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 690.249337
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 595.036289
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 627.082447
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 787.799722
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 587.311743
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 618.209773
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 633.838649
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 619.313248
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 9
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 6014.970062
|
||||
@@ -1,64 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 636.096036
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 741.239029
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 568.879998
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 641.929005
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 870.041985
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 648.87451
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 566.725897
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 611.150846
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 618.488891
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 9
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 6014.364529
|
||||
@@ -1,16 +0,0 @@
|
||||
✔ chat replies: free text, cites with badges, the agent label, changed since and safe links (583.234335ms)
|
||||
✔ chat replies: a write notice reads as its line with a Revert; a refused revert shows its code (634.71741ms)
|
||||
✔ chat replies: a read-only Console shows the notice with Revert disabled (574.836149ms)
|
||||
✔ chat status line: served commit and age, divergence, fetch error and the problems one click away (607.74749ms)
|
||||
✔ chat status line: limits when the status carries them, no problems, and refusals (861.034669ms)
|
||||
✔ chat status line: a project with no repository link reads no library status (611.433469ms)
|
||||
✔ chat status line: unreadable reply limits show their problem and both fallback keys (606.380675ms)
|
||||
✔ chat replies: unchanged file at a different library commit and incomplete checks stay unmarked (650.947719ms)
|
||||
ℹ tests 8
|
||||
ℹ suites 0
|
||||
ℹ pass 8
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 5251.735356
|
||||
@@ -1,78 +0,0 @@
|
||||
✖ chat replies: free text, cites with badges, the agent label, changed since and safe links (980.914799ms)
|
||||
✔ chat replies: a write notice reads as its line with a Revert; a refused revert shows its code (753.988771ms)
|
||||
✔ chat replies: a read-only Console shows the notice with Revert disabled (672.814814ms)
|
||||
✔ chat status line: served commit and age, divergence, fetch error and the problems one click away (622.859977ms)
|
||||
✖ chat status line: limits when the status carries them, no problems, and refusals (583.956229ms)
|
||||
✔ chat status line: a project with no repository link reads no library status (635.758222ms)
|
||||
✖ chat status line: unreadable reply limits show their problem and both fallback keys (581.636121ms)
|
||||
✔ chat replies: unchanged file at a different library commit and incomplete checks stay unmarked (653.788881ms)
|
||||
ℹ tests 8
|
||||
ℹ suites 0
|
||||
ℹ pass 5
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 5597.371605
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/webui/tests/chat-replies-browser.test.mjs:81:1
|
||||
✖ chat replies: free text, cites with badges, the agent label, changed since and safe links (980.914799ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ undefined
|
||||
- false
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:102:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: false,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/chat-replies-browser.test.mjs:152:1
|
||||
✖ chat status line: limits when the status carries them, no problems, and refusals (583.956229ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems'
|
||||
- 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:156:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems',
|
||||
expected: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/chat-replies-browser.test.mjs:173:1
|
||||
✖ chat status line: unreadable reply limits show their problem and both fallback keys (581.636121ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · no problems'
|
||||
- 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:179:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · no problems',
|
||||
expected: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: channel conversion preserves the four proof/transport byte baselines and modules cannot import a proof caller
|
||||
ok 1 - channel conversion preserves the four proof/transport byte baselines and modules cannot import a proof caller
|
||||
---
|
||||
duration_ms: 4.280459
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel recovery still requires the same shim path and rejects an absent or forged proof
|
||||
ok 2 - channel recovery still requires the same shim path and rejects an absent or forged proof
|
||||
---
|
||||
duration_ms: 413.032186
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 539.602759
|
||||
@@ -1,496 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: resolver step 1: a path from the repo root, with or without .md
|
||||
ok 1 - resolver step 1: a path from the repo root, with or without .md
|
||||
---
|
||||
duration_ms: 1.051208
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: resolver step 2: the same path with vaultPrefix/ stripped
|
||||
ok 2 - resolver step 2: the same path with vaultPrefix/ stripped
|
||||
---
|
||||
duration_ms: 0.164795
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: resolver step 3: a path relative to the linking file, never above the repo root
|
||||
ok 3 - resolver step 3: a path relative to the linking file, never above the repo root
|
||||
---
|
||||
duration_ms: 0.192765
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: resolver step 4: a base name that matches exactly one indexed file
|
||||
ok 4 - resolver step 4: a base name that matches exactly one indexed file
|
||||
---
|
||||
duration_ms: 0.201611
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a link into another of her repos resolves to nothing
|
||||
ok 5 - a link into another of her repos resolves to nothing
|
||||
---
|
||||
duration_ms: 0.130282
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: wikilink forms, embeds, and links skipped inside code
|
||||
ok 6 - wikilink forms, embeds, and links skipped inside code
|
||||
---
|
||||
duration_ms: 1.445011
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: only a body embed whose target has an extension other than .md, in any case, is an attachment
|
||||
ok 7 - only a body embed whose target has an extension other than .md, in any case, is an attachment
|
||||
---
|
||||
duration_ms: 0.25003
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the fixture mapping checks and comes back frozen
|
||||
ok 8 - the fixture mapping checks and comes back frozen
|
||||
---
|
||||
duration_ms: 1.772928
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-01c: a prefix inside or holding another entry's book refuses library-playground-other-universe
|
||||
ok 9 - C2-01c: a prefix inside or holding another entry's book refuses library-playground-other-universe
|
||||
---
|
||||
duration_ms: 0.735312
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-01c: a prefix equal to a book folder, or holding its own, refuses library-mapping-invalid
|
||||
ok 10 - C2-01c: a prefix equal to a book folder, or holding its own, refuses library-mapping-invalid
|
||||
---
|
||||
duration_ms: 0.532498
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-01c: universe-level prefixes lie outside every book, in a plain universe and in a hybrid with no books
|
||||
ok 11 - C2-01c: universe-level prefixes lie outside every book, in a plain universe and in a hybrid with no books
|
||||
---
|
||||
duration_ms: 0.668173
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-01c: a draft takes kind, name and playground only, and comes back frozen with books []
|
||||
ok 12 - C2-01c: a draft takes kind, name and playground only, and comes back frozen with books []
|
||||
---
|
||||
duration_ms: 1.032507
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: no playground prefix holds another, and no book folder holds another
|
||||
ok 13 - no playground prefix holds another, and no book folder holds another
|
||||
---
|
||||
duration_ms: 0.507943
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: shape refusals: kind, remote, branch, unknown keys, types, manuscript, folder names
|
||||
ok 14 - shape refusals: kind, remote, branch, unknown keys, types, manuscript, folder names
|
||||
---
|
||||
duration_ms: 0.740368
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a sage-2021: user is optional, kept when given, and must be a plain account name
|
||||
ok 15 - C2-03a sage-2021: user is optional, kept when given, and must be a plain account name
|
||||
---
|
||||
duration_ms: 0.659277
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a hybrid universe is a shape the query code can take (1.4), with sources that must exist
|
||||
ok 16 - a hybrid universe is a shape the query code can take (1.4), with sources that must exist
|
||||
---
|
||||
duration_ms: 0.695117
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a caller-supplied fail receives the code, as business file loading will pass it (C2-04)
|
||||
ok 17 - a caller-supplied fail receives the code, as business file loading will pass it (C2-04)
|
||||
---
|
||||
duration_ms: 0.392362
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: scope comes from the path alone (design 1.2)
|
||||
ok 18 - scope comes from the path alone (design 1.2)
|
||||
---
|
||||
duration_ms: 0.466406
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 mirror: exact default path, bare without checkout or remote, and idempotent initialization
|
||||
ok 19 - C2-04 mirror: exact default path, bare without checkout or remote, and idempotent initialization
|
||||
---
|
||||
duration_ms: 46.645994
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 mirror: simultaneous creators publish one intact mirror and leave no staging paths
|
||||
ok 20 - C2-04 mirror: simultaneous creators publish one intact mirror and leave no staging paths
|
||||
---
|
||||
duration_ms: 32.763959
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 mirror: skipped publication accepts both coreutils statuses without removing the winner
|
||||
ok 21 - C2-04 mirror: skipped publication accepts both coreutils statuses without removing the winner
|
||||
---
|
||||
duration_ms: 109.751094
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 mirror: preserve occupied directories and reject symlink ancestors and code overlaps
|
||||
ok 22 - C2-04 mirror: preserve occupied directories and reject symlink ancestors and code overlaps
|
||||
---
|
||||
duration_ms: 20.129293
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 mirror: existing metadata accepts only bare Git plus state and the precreated lock
|
||||
ok 23 - C2-04 mirror: existing metadata accepts only bare Git plus state and the precreated lock
|
||||
---
|
||||
duration_ms: 24.021555
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 state: missing state is empty only while the served ref is absent
|
||||
ok 24 - C2-04 state: missing state is empty only while the served ref is absent
|
||||
---
|
||||
duration_ms: 37.436349
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 state: valid problems and sync metadata return no content and change no state bytes
|
||||
ok 25 - C2-04 state: valid problems and sync metadata return no content and change no state bytes
|
||||
---
|
||||
duration_ms: 37.681315
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 state: strict keys, types, times, branch, commit and fixed R messages fail closed
|
||||
ok 26 - C2-04 state: strict keys, types, times, branch, commit and fixed R messages fail closed
|
||||
---
|
||||
duration_ms: 105.483341
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 state: symlink, hardlink, non-private state and missing lock refuse without replacement
|
||||
ok 27 - C2-04 state: symlink, hardlink, non-private state and missing lock refuse without replacement
|
||||
---
|
||||
duration_ms: 29.562786
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 file: served commit, not HEAD or a caller commit, owns the exact returned text
|
||||
ok 28 - C2-04 file: served commit, not HEAD or a caller commit, owns the exact returned text
|
||||
---
|
||||
duration_ms: 74.057232
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 file: R1, R2 and R7 exclusions and never-synced mirrors cannot return file bodies
|
||||
ok 29 - C2-04 file: R1, R2 and R7 exclusions and never-synced mirrors cannot return file bodies
|
||||
---
|
||||
duration_ms: 80.573844
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 lock: a first-sync and fast-forward reader wait for a consistent pair across processes
|
||||
ok 30 - C2-04 lock: a first-sync and fast-forward reader wait for a consistent pair across processes
|
||||
---
|
||||
duration_ms: 253.247948
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 lock: same-process waits, timeout code, callback expiry and throw release
|
||||
ok 31 - C2-04 lock: same-process waits, timeout code, callback expiry and throw release
|
||||
---
|
||||
duration_ms: 5031.462795
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 lock: owner death releases the kernel lock without replacing its file
|
||||
ok 32 - C2-04 lock: owner death releases the kernel lock without replacing its file
|
||||
---
|
||||
duration_ms: 61.533295
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 lock: a missing flock binary becomes a handled refusal
|
||||
ok 33 - C2-04 lock: a missing flock binary becomes a handled refusal
|
||||
---
|
||||
duration_ms: 57.176644
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-04 lock: an unawaited scoped read drains before the lock is released
|
||||
ok 34 - C2-04 lock: an unawaited scoped read drains before the lock is released
|
||||
---
|
||||
duration_ms: 22.766874
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the fixture reads clean: every resolver step, appears_in and relationships wikilinks indexed
|
||||
ok 35 - the fixture reads clean: every resolver step, appears_in and relationships wikilinks indexed
|
||||
---
|
||||
duration_ms: 33.888061
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R1: executables, symlinks, gitlinks, blobs over 1 MiB and non-UTF-8 text are not indexed
|
||||
ok 36 - R1: executables, symlinks, gitlinks, blobs over 1 MiB and non-UTF-8 text are not indexed
|
||||
---
|
||||
duration_ms: 68.951498
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R2: unmapped paths are reported and not indexed
|
||||
ok 37 - R2: unmapped paths are reported and not indexed
|
||||
---
|
||||
duration_ms: 25.70771
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R3: front matter not fully parsed stays raw, with the line where parsing stopped
|
||||
ok 38 - R3: front matter not fully parsed stays raw, with the line where parsing stopped
|
||||
---
|
||||
duration_ms: 26.059427
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: front matter forms the reader accepts and refuses
|
||||
ok 39 - front matter forms the reader accepts and refuses
|
||||
---
|
||||
duration_ms: 0.448293
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R4: a link to no file, to more than one, or into another repo is reported and not followed
|
||||
ok 40 - R4: a link to no file, to more than one, or into another repo is reported and not followed
|
||||
---
|
||||
duration_ms: 26.225692
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a block list link carries its item line, a scalar or flow link its key line, and problems follow
|
||||
ok 41 - a block list link carries its item line, a scalar or flow link its key line, and problems follow
|
||||
---
|
||||
duration_ms: 22.565103
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an unresolved attachment embed gives no problem; a link or front matter value to one is R4; a note embed resolves
|
||||
ok 42 - an unresolved attachment embed gives no problem; a link or front matter value to one is R4; a note embed resolves
|
||||
---
|
||||
duration_ms: 20.903777
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an indexed Dr.Quinn.md resolves as a body link, a block list link and an embed
|
||||
ok 43 - an indexed Dr.Quinn.md resolves as a body link, a block list link and an embed
|
||||
---
|
||||
duration_ms: 25.42747
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R5: a canon file that refers into the playground is reported, and the link still goes by path
|
||||
ok 44 - R5: a canon file that refers into the playground is reported, and the link still goes by path
|
||||
---
|
||||
duration_ms: 27.37119
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6: a canon file that refers into another universe is reported; a hybrid may refer into its sources
|
||||
ok 45 - R6: a canon file that refers into another universe is reported; a hybrid may refer into its sources
|
||||
---
|
||||
duration_ms: 30.585458
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R7: two paths that differ only in case are both left out
|
||||
ok 46 - R7: two paths that differ only in case are both left out
|
||||
---
|
||||
duration_ms: 40.709509
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the reader reads the commit, never the working tree, and refuses a bad commit id
|
||||
ok 47 - the reader reads the commit, never the working tree, and refuses a bad commit id
|
||||
---
|
||||
duration_ms: 33.579454
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bare mirror reads the same as the clone
|
||||
ok 48 - a bare mirror reads the same as the clone
|
||||
---
|
||||
duration_ms: 38.368686
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: free text renders as itself with no citations
|
||||
ok 49 - free text renders as itself with no citations
|
||||
---
|
||||
duration_ms: 1.571777
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a cited reply renders its text, then each cite with the badge from the check result
|
||||
ok 50 - a cited reply renders its text, then each cite with the badge from the check result
|
||||
---
|
||||
duration_ms: 0.537429
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a parsed body renders as its string does
|
||||
ok 51 - a parsed body renders as its string does
|
||||
---
|
||||
duration_ms: 0.304068
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: without a check result a path cite has no badge, no label and is never changed
|
||||
ok 52 - without a check result a path cite has no badge, no label and is never changed
|
||||
---
|
||||
duration_ms: 0.184363
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the label and the badge come from the check result only, never from a bad entry
|
||||
ok 53 - the label and the badge come from the check result only, never from a bad entry
|
||||
---
|
||||
duration_ms: 0.180581
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a web cite is a link only for https
|
||||
ok 54 - a web cite is a link only for https
|
||||
---
|
||||
duration_ms: 0.293669
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a write notice renders its fixed line
|
||||
ok 55 - a write notice renders its fixed line
|
||||
---
|
||||
duration_ms: 0.277197
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a body without the reply or notice shape is free text: the whole body, no citations
|
||||
ok 56 - a body without the reply or notice shape is free text: the whole body, no citations
|
||||
---
|
||||
duration_ms: 0.345807
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an empty citations list is a reply with no cites
|
||||
ok 57 - an empty citations list is a reply with no cites
|
||||
---
|
||||
duration_ms: 0.187354
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-01c: each level gives its universe, and its book or null
|
||||
ok 58 - C2-01c: each level gives its universe, and its book or null
|
||||
---
|
||||
duration_ms: 3.363132
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-01c: prefixes come first: in a book that holds a prefix, a file outside it is canon and one under it playground
|
||||
ok 59 - C2-01c: prefixes come first: in a book that holds a prefix, a file outside it is canon and one under it playground
|
||||
---
|
||||
duration_ms: 0.480483
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-01c: a canon file linking into a draft gives R6, and into its own universe-level prefix gives R5
|
||||
ok 60 - C2-01c: a canon file linking into a draft gives R6, and into its own universe-level prefix gives R5
|
||||
---
|
||||
duration_ms: 44.606565
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the survey prints folder names, counts, key sets and link-form counts, and never text
|
||||
ok 61 - the survey prints folder names, counts, key sets and link-form counts, and never text
|
||||
---
|
||||
duration_ms: 37.28555
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: folder names print DEL, C1 and bidi controls, U+061C among them, as escapes
|
||||
ok 62 - folder names print DEL, C1 and bidi controls, U+061C among them, as escapes
|
||||
---
|
||||
duration_ms: 26.222951
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an embed of an attachment counts as an embed and as an attachment; a plain link to one does not
|
||||
ok 63 - an embed of an attachment counts as an embed and as an attachment; a plain link to one does not
|
||||
---
|
||||
duration_ms: 26.23793
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: with --mapping the survey adds scope, resolver step and problem counts
|
||||
ok 64 - with --mapping the survey adds scope, resolver step and problem counts
|
||||
---
|
||||
duration_ms: 41.437066
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: survey exit codes: usage 4, bad mapping 3, unreadable repo 2
|
||||
ok 65 - survey exit codes: usage 4, bad mapping 3, unreadable repo 2
|
||||
---
|
||||
duration_ms: 17.463059
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: scripts/mosaic library survey reaches the survey
|
||||
ok 66 - scripts/mosaic library survey reaches the survey
|
||||
---
|
||||
duration_ms: 77.634947
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a argv: the fixed sequence, one https allow, no other protocol key
|
||||
ok 67 - C2-03a argv: the fixed sequence, one https allow, no other protocol key
|
||||
---
|
||||
duration_ms: 1.605597
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a argv: only an https URL with a plain host, no userinfo, and a plain user
|
||||
ok 68 - C2-03a argv: only an https URL with a plain host, no userinfo, and a plain user
|
||||
---
|
||||
duration_ms: 0.713147
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a helper: answers only the exact prompts for the configured host and user (dewey-0765)
|
||||
ok 69 - C2-03a helper: answers only the exact prompts for the configured host and user (dewey-0765)
|
||||
---
|
||||
duration_ms: 31.590474
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a first sync: served is the fetched tip, HEAD is served, the survey reads it, and the token stays in the slot
|
||||
ok 70 - C2-03a first sync: served is the fetched tip, HEAD is served, the survey reads it, and the token stays in the slot
|
||||
---
|
||||
duration_ms: 135.075159
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a fast-forward: served moves, problems are rebuilt, and a repeat changes only lastFetch
|
||||
ok 71 - C2-03a fast-forward: served moves, problems are rebuilt, and a repeat changes only lastFetch
|
||||
---
|
||||
duration_ms: 225.707972
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a Filbert N2: a read that took served before a sync still finds its objects after served moves
|
||||
ok 72 - C2-03a Filbert N2: a read that took served before a sync still finds its objects after served moves
|
||||
---
|
||||
duration_ms: 205.10431
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a overlap: a reader during a sync sees the old pair or the new one, never a mix
|
||||
ok 73 - C2-03a overlap: a reader during a sync sees the old pair or the new one, never a mix
|
||||
---
|
||||
duration_ms: 175.907929
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a diverged: a non-fast-forward keeps serving and holds until a human accepts the tip
|
||||
ok 74 - C2-03a diverged: a non-fast-forward keeps serving and holds until a human accepts the tip
|
||||
---
|
||||
duration_ms: 431.553812
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a accept starts the record over, so a changed branch gets a fresh state
|
||||
ok 75 - C2-03a accept starts the record over, so a changed branch gets a fresh state
|
||||
---
|
||||
duration_ms: 195.011432
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a fetch failure: a wrong token keeps serving and records the error, lastFetch unchanged
|
||||
ok 76 - C2-03a fetch failure: a wrong token keeps serving and records the error, lastFetch unchanged
|
||||
---
|
||||
duration_ms: 192.898644
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a a failed first fetch records the error with nothing served
|
||||
ok 77 - C2-03a a failed first fetch records the error with nothing served
|
||||
---
|
||||
duration_ms: 61.594695
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a a 302 to another host fails the fetch and the other host sees nothing
|
||||
ok 78 - C2-03a a 302 to another host fails the fetch and the other host sees nothing
|
||||
---
|
||||
duration_ms: 64.82991
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a the slot: missing or unsafe refuses before git, and sync never opens it
|
||||
ok 79 - C2-03a the slot: missing or unsafe refuses before git, and sync never opens it
|
||||
---
|
||||
duration_ms: 59.167725
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a sage-2021: an entry with no user refuses library-remote-invalid and runs no git
|
||||
ok 80 - C2-03a sage-2021: an entry with no user refuses library-remote-invalid and runs no git
|
||||
---
|
||||
duration_ms: 35.37205
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: C2-03a one sync per mirror: an overlapping call refuses library-sync-busy
|
||||
ok 81 - C2-03a one sync per mirror: an overlapping call refuses library-sync-busy
|
||||
---
|
||||
duration_ms: 118.260687
|
||||
type: 'test'
|
||||
...
|
||||
1..81
|
||||
# tests 81
|
||||
# suites 0
|
||||
# pass 81
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 9149.325891
|
||||
@@ -1,48 +0,0 @@
|
||||
✔ chat replies: free text, cites with badges, the agent label, changed since and safe links (633.807074ms)
|
||||
✔ chat replies: a write notice reads as its line with a Revert; a refused revert shows its code (621.95095ms)
|
||||
✔ chat replies: a read-only Console shows the notice with Revert disabled (577.638161ms)
|
||||
✔ chat status line: served commit and age, divergence, fetch error and the problems one click away (555.847899ms)
|
||||
✖ chat status line: limits when the status carries them, no problems, and refusals (3725.696272ms)
|
||||
✔ chat status line: a project with no repository link reads no library status (657.572856ms)
|
||||
✔ chat status line: unreadable reply limits show their problem and both fallback keys (663.163445ms)
|
||||
✔ chat replies: unchanged file at a different library commit and incomplete checks stay unmarked (616.184063ms)
|
||||
✖ chat status line: an invalid library mapping remains visible for diagnosis (597.976623ms)
|
||||
ℹ tests 9
|
||||
ℹ suites 0
|
||||
ℹ pass 7
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 8782.679569
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/webui/tests/chat-replies-browser.test.mjs:152:1
|
||||
✖ chat status line: limits when the status carries them, no problems, and refusals (3725.696272ms)
|
||||
Error: Uncaught (in promise) Error: window.__statusReads.length && document.querySelector(".chat-conversation") && document.querySelector(".chat-library")?.textContent==="Library status unavailable (library-mapping-invalid).": undefined
|
||||
at Object.evaluate (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/browser.mjs:49:35)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:162:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
|
||||
test at packages/webui/tests/chat-replies-browser.test.mjs:200:1
|
||||
✖ chat status line: an invalid library mapping remains visible for diagnosis (597.976623ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ null
|
||||
- 'Library status unavailable (library-mapping-invalid).'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/chat-replies-browser.test.mjs:203:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: null,
|
||||
expected: 'Library status unavailable (library-mapping-invalid).',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{"mutant": "M1-badges", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-badges.tap"}
|
||||
{"mutant": "M1-agent-label", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-agent-label.tap"}
|
||||
{"mutant": "M2-http-links", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M2-http-links.tap"}
|
||||
{"mutant": "M3-wrong-commit", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": false, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M3-wrong-commit.tap"}
|
||||
@@ -1,192 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 627.180675
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
... Skipped lines
|
||||
|
||||
[
|
||||
{
|
||||
agent: null,
|
||||
badge: 'canon',
|
||||
changed: false,
|
||||
...
|
||||
{
|
||||
+ agent: null,
|
||||
- agent: 'agent-written: acme/scribe',
|
||||
badge: 'supporting',
|
||||
changed: false,
|
||||
href: null,
|
||||
quote: 'green eyes',
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
actual:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:90:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 778.572201
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 641.728604
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 629.327348
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 837.886073
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 613.037186
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 651.026507
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 735.546636
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 712.174542
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 6353.090574
|
||||
@@ -1,206 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 638.589537
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
... Skipped lines
|
||||
|
||||
[
|
||||
{
|
||||
agent: null,
|
||||
+ badge: null,
|
||||
- badge: 'canon',
|
||||
changed: false,
|
||||
href: null,
|
||||
quote: 'her grey eyes',
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa',
|
||||
rel: null
|
||||
...
|
||||
agent: null,
|
||||
+ badge: null,
|
||||
- badge: 'playground',
|
||||
changed: true,
|
||||
href: null,
|
||||
quote: 'a storm',
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa',
|
||||
rel: null
|
||||
...
|
||||
agent: 'agent-written: acme/scribe',
|
||||
+ badge: null,
|
||||
- badge: 'supporting',
|
||||
changed: false,
|
||||
href: null,
|
||||
quote: 'green eyes',
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
|
||||
rel: null
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
actual:
|
||||
0:
|
||||
badge: ~
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: ~
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: ~
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:90:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 770.070531
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 519.328288
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 647.667301
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 999.237447
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 676.088845
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 634.472637
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 655.963182
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 578.819646
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 6239.022812
|
||||
@@ -1,204 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 655.901619
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
... Skipped lines
|
||||
|
||||
[
|
||||
{
|
||||
agent: null,
|
||||
badge: 'canon',
|
||||
changed: false,
|
||||
...
|
||||
changed: false,
|
||||
+ href: 'http://example.invalid/plain',
|
||||
- href: null,
|
||||
quote: 'plain http',
|
||||
ref: 'http://example.invalid/plain',
|
||||
+ rel: 'noopener noreferrer'
|
||||
- rel: null
|
||||
},
|
||||
{
|
||||
agent: null,
|
||||
badge: 'web',
|
||||
changed: false,
|
||||
+ href: 'javascript:window.injected=2',
|
||||
- href: null,
|
||||
quote: 'script',
|
||||
ref: 'javascript:window.injected=2',
|
||||
+ rel: 'noopener noreferrer'
|
||||
- rel: null
|
||||
}
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
actual:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: 'http://example.invalid/plain'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: 'javascript:window.injected=2'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:90:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 741.122544
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 616.728572
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 625.818493
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 867.033773
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 655.587703
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 615.997516
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 648.263681
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 624.323397
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 6168.633364
|
||||
@@ -1,74 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 640.32042
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 694.233373
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 611.500508
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 606.581119
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 896.462474
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 666.798177
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 629.105559
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
not ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 3654.057789
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:184:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: 'Uncaught (in promise) Error: document.querySelectorAll(".chat-changed").length===0: undefined'
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
stack: |-
|
||||
Object.evaluate (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M3-wrong-commit/packages/webui/tests/browser.mjs:49:35)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-9ui_vkk0/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:195:3)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 656.44095
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 9179.328916
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"head": "77172ce3b46eb289409068518b7022fc7be47ebe",
|
||||
"clear": "sage-3647",
|
||||
"worktreeUnchanged": true,
|
||||
"results": [
|
||||
{
|
||||
"mutant": "M1-badges",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-badges.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M1-agent-label",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M1-agent-label.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M2-http-links",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M2-http-links.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M3-wrong-commit",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": false,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants/M3-wrong-commit.tap"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{"mutant": "M1-badges", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-badges.tap"}
|
||||
{"mutant": "M1-agent-label", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-agent-label.tap"}
|
||||
{"mutant": "M2-http-links", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M2-http-links.tap"}
|
||||
{"mutant": "M3-wrong-commit", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-wrong-commit.tap"}
|
||||
{"mutant": "M3-missing-file", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-missing-file.tap"}
|
||||
{"mutant": "M4-fallback", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-fallback.tap"}
|
||||
{"mutant": "M4-problem", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/chat-replies-browser.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-problem.tap"}
|
||||
{"mutant": "M5-send", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-send.tap"}
|
||||
{"mutant": "M5-project-receipt", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-project-receipt.tap"}
|
||||
{"mutant": "M5-commit-input", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-commit-input.tap"}
|
||||
{"mutant": "M5-origin", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-origin.tap"}
|
||||
{"mutant": "M5-fetch-metadata", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-fetch-metadata.tap"}
|
||||
{"mutant": "M6-duplicate-call", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M6-duplicate-call.tap"}
|
||||
{"mutant": "M7-user-fence", "anchorHits": 1, "restored": true, "worktreeUnchanged": true, "exit": 1, "killed": true, "test": "packages/webui/tests/library-routes.test.mjs", "log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M7-user-fence.tap"}
|
||||
@@ -1,192 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 646.851404
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
... Skipped lines
|
||||
|
||||
[
|
||||
{
|
||||
agent: null,
|
||||
badge: 'canon',
|
||||
changed: false,
|
||||
...
|
||||
{
|
||||
+ agent: null,
|
||||
- agent: 'agent-written: acme/scribe',
|
||||
badge: 'supporting',
|
||||
changed: false,
|
||||
href: null,
|
||||
quote: 'green eyes',
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
actual:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-agent-label/packages/webui/tests/chat-replies-browser.test.mjs:90:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 667.90945
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 588.346681
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 485.882289
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 709.847041
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 544.885551
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 514.410016
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 542.746154
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 582.608663
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5409.980177
|
||||
@@ -1,206 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 621.562133
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
... Skipped lines
|
||||
|
||||
[
|
||||
{
|
||||
agent: null,
|
||||
+ badge: null,
|
||||
- badge: 'canon',
|
||||
changed: false,
|
||||
href: null,
|
||||
quote: 'her grey eyes',
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa',
|
||||
rel: null
|
||||
...
|
||||
agent: null,
|
||||
+ badge: null,
|
||||
- badge: 'playground',
|
||||
changed: true,
|
||||
href: null,
|
||||
quote: 'a storm',
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa',
|
||||
rel: null
|
||||
...
|
||||
agent: 'agent-written: acme/scribe',
|
||||
+ badge: null,
|
||||
- badge: 'supporting',
|
||||
changed: false,
|
||||
href: null,
|
||||
quote: 'green eyes',
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa',
|
||||
rel: null
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
actual:
|
||||
0:
|
||||
badge: ~
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: ~
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: ~
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M1-badges/packages/webui/tests/chat-replies-browser.test.mjs:90:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 731.101767
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 633.163696
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 637.761161
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 847.929653
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 635.512979
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 645.427688
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 672.272209
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 629.043251
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 6169.954716
|
||||
@@ -1,204 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
not ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 582.248796
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:81:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
... Skipped lines
|
||||
|
||||
[
|
||||
{
|
||||
agent: null,
|
||||
badge: 'canon',
|
||||
changed: false,
|
||||
...
|
||||
changed: false,
|
||||
+ href: 'http://example.invalid/plain',
|
||||
- href: null,
|
||||
quote: 'plain http',
|
||||
ref: 'http://example.invalid/plain',
|
||||
+ rel: 'noopener noreferrer'
|
||||
- rel: null
|
||||
},
|
||||
{
|
||||
agent: null,
|
||||
badge: 'web',
|
||||
changed: false,
|
||||
+ href: 'javascript:window.injected=2',
|
||||
- href: null,
|
||||
quote: 'script',
|
||||
ref: 'javascript:window.injected=2',
|
||||
+ rel: 'noopener noreferrer'
|
||||
- rel: null
|
||||
}
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
actual:
|
||||
0:
|
||||
badge: 'canon'
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'her grey eyes'
|
||||
agent: ~
|
||||
changed: false
|
||||
1:
|
||||
badge: 'playground'
|
||||
ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'a storm'
|
||||
agent: ~
|
||||
changed: true
|
||||
2:
|
||||
badge: 'supporting'
|
||||
ref: 'Notes/Eyes.md:9-10 @aaaaaaa'
|
||||
href: ~
|
||||
rel: ~
|
||||
quote: 'green eyes'
|
||||
agent: 'agent-written: acme/scribe'
|
||||
changed: false
|
||||
3:
|
||||
badge: 'web'
|
||||
ref: 'https://example.invalid/tides'
|
||||
href: 'https://example.invalid/tides'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'spring tides'
|
||||
agent: ~
|
||||
changed: false
|
||||
4:
|
||||
badge: 'web'
|
||||
ref: 'http://example.invalid/plain'
|
||||
href: 'http://example.invalid/plain'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'plain http'
|
||||
agent: ~
|
||||
changed: false
|
||||
5:
|
||||
badge: 'web'
|
||||
ref: 'javascript:window.injected=2'
|
||||
href: 'javascript:window.injected=2'
|
||||
rel: 'noopener noreferrer'
|
||||
quote: 'script'
|
||||
agent: ~
|
||||
changed: false
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M2-http-links/packages/webui/tests/chat-replies-browser.test.mjs:90:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 640.769563
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 572.506418
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 519.031091
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 743.513043
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 676.282756
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 595.166769
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 626.475981
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 596.828198
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5658.889854
|
||||
@@ -1,81 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 639.027823
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 693.772757
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 683.094789
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 637.029355
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 772.100011
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 599.342035
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 574.320042
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
not ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 621.466015
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-missing-file/packages/webui/tests/chat-replies-browser.test.mjs:184:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
1 !== 0
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 0
|
||||
actual: 1
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-missing-file/packages/webui/tests/chat-replies-browser.test.mjs:196:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 590.165121
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5921.651346
|
||||
@@ -1,81 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 704.141588
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 816.229452
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 697.826745
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 638.579753
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 850.914117
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 634.004327
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 585.469632
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
not ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 673.386968
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:184:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
1 !== 0
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 0
|
||||
actual: 1
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M3-wrong-commit/packages/webui/tests/chat-replies-browser.test.mjs:196:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 579.53998
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 6292.579267
|
||||
@@ -1,102 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 683.334532
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 686.685624
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 612.757087
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 584.651483
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
not ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 637.405759
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:152:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems'
|
||||
- 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems'
|
||||
actual: 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · no problems'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:156:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 626.790802
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
not ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 643.680732
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:173:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · reply-limits-unreadable · no problems'
|
||||
- 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
|
||||
actual: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · reply-limits-unreadable · no problems'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-fallback/packages/webui/tests/chat-replies-browser.test.mjs:179:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 645.032205
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 616.242947
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 7
|
||||
# fail 2
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5845.226677
|
||||
@@ -1,83 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
ok 1 - chat replies: free text, cites with badges, the agent label, changed since and safe links
|
||||
---
|
||||
duration_ms: 663.005514
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
ok 2 - chat replies: a write notice reads as its line with a Revert; a refused revert shows its code
|
||||
---
|
||||
duration_ms: 662.360161
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat replies: a read-only Console shows the notice with Revert disabled
|
||||
ok 3 - chat replies: a read-only Console shows the notice with Revert disabled
|
||||
---
|
||||
duration_ms: 620.68694
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
ok 4 - chat status line: served commit and age, divergence, fetch error and the problems one click away
|
||||
---
|
||||
duration_ms: 744.825035
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: limits when the status carries them, no problems, and refusals
|
||||
ok 5 - chat status line: limits when the status carries them, no problems, and refusals
|
||||
---
|
||||
duration_ms: 826.418848
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: a project with no repository link reads no library status
|
||||
ok 6 - chat status line: a project with no repository link reads no library status
|
||||
---
|
||||
duration_ms: 549.365993
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
not ok 7 - chat status line: unreadable reply limits show their problem and both fallback keys
|
||||
---
|
||||
duration_ms: 589.776762
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-problem/packages/webui/tests/chat-replies-browser.test.mjs:173:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · no problems'
|
||||
- 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems'
|
||||
actual: 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · no problems'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M4-problem/packages/webui/tests/chat-replies-browser.test.mjs:179:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
ok 8 - chat replies: unchanged file at a different library commit and incomplete checks stay unmarked
|
||||
---
|
||||
duration_ms: 590.919434
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: chat status line: an invalid library mapping remains visible for diagnosis
|
||||
ok 9 - chat status line: an invalid library mapping remains visible for diagnosis
|
||||
---
|
||||
duration_ms: 545.326381
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 9
|
||||
# suites 0
|
||||
# pass 8
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5914.325706
|
||||
@@ -1,105 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 16.128237
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 5.987607
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
not ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 4.805637
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-commit-input/packages/webui/tests/library-routes.test.mjs:64:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
{"project":"books","commit":"abc"}
|
||||
|
||||
200 !== 400
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 400
|
||||
actual: 200
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-commit-input/packages/webui/tests/library-routes.test.mjs:69:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 6.383811
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 4.000988
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.487122
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 4.9292
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 1.266317
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 0.443059
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.048018
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 0.864934
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.064626
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 0.803048
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 12
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 153.3549
|
||||
@@ -1,105 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 20.271342
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
not ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 5.700968
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-fetch-metadata/packages/webui/tests/library-routes.test.mjs:48:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
sec-fetch-site
|
||||
|
||||
200 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 200
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-fetch-metadata/packages/webui/tests/library-routes.test.mjs:57:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 6.427935
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 7.39929
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 5.425467
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.634037
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 5.727409
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 1.317954
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 0.583799
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.207375
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 0.956772
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.147745
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 0.819337
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 12
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 166.134058
|
||||
@@ -1,105 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 16.249491
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
not ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 5.463985
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-origin/packages/webui/tests/library-routes.test.mjs:48:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
origin
|
||||
|
||||
200 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 200
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-origin/packages/webui/tests/library-routes.test.mjs:57:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 6.035196
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 5.621744
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 4.928333
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.455282
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 4.838747
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 1.235209
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 0.436399
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.100574
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 0.943474
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.176957
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 0.861834
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 12
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 150.969641
|
||||
@@ -1,105 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 18.918199
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 7.26916
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 5.352342
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 6.134713
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 4.288914
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.745385
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 5.739155
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 1.264793
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 0.471965
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.045204
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
not ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 1.531252
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-project-receipt/packages/webui/tests/library-routes.test.mjs:145:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
200 !== 503
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 503
|
||||
actual: 200
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-project-receipt/packages/webui/tests/library-routes.test.mjs:148:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.60161
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 0.816068
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 12
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 171.289003
|
||||
@@ -1,106 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 18.930067
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 7.470645
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 6.646929
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 6.637697
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 4.174441
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.513131
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 5.145692
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 1.182362
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 0.471806
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.064814
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 0.910004
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.16191
|
||||
type: 'test'
|
||||
...
|
||||
# 2026-10-23T19:28:51.488Z library-revert user="jason" project="books" commit="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" commit=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
not ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 1.624909
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-send/packages/webui/tests/library-routes.test.mjs:175:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
200 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 200
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M5-send/packages/webui/tests/library-routes.test.mjs:181:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 12
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 165.919088
|
||||
@@ -1,177 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
not ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 17.922489
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:40:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
{
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
project: 'books'
|
||||
},
|
||||
+ {
|
||||
+ commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
+ project: 'books'
|
||||
+ }
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
project: 'books'
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
actual:
|
||||
0:
|
||||
project: 'books'
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
1:
|
||||
project: 'books'
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:43:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 7.053366
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 5.869486
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 6.852952
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
not ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 2.870452
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:88:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
2 !== 1
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 1
|
||||
actual: 2
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:96:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.904694
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 5.7962
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 1.518332
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 0.563294
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.17446
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
not ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 1.258633
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:145:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
{
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
project: 'books'
|
||||
},
|
||||
+ {
|
||||
+ commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
+ project: 'books'
|
||||
+ }
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0:
|
||||
project: 'books'
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
actual:
|
||||
0:
|
||||
project: 'books'
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
1:
|
||||
project: 'books'
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M6-duplicate-call/packages/webui/tests/library-routes.test.mjs:151:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.362877
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 0.848309
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 10
|
||||
# fail 3
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 169.162917
|
||||
@@ -1,126 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 18.215173
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 6.416847
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 5.529629
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 6.92431
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 4.588509
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.817257
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 6.020444
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 1.377224
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
not ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 1.015984
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:132:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
/api/library/status
|
||||
|
||||
true !== false
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: false
|
||||
actual: true
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:133:106)
|
||||
Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
Test.run (node:internal/test_runner/test:1402:25)
|
||||
Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
Test.run (node:internal/test_runner/test:1467:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.695555
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 1.378325
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
not ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.178045
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:155:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
503 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 503
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mosaic-row225-mutants-e6t705gk/M7-user-fence/packages/webui/tests/library-routes.test.mjs:167:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 1.057394
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 11
|
||||
# fail 2
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 176.864112
|
||||
@@ -1,147 +0,0 @@
|
||||
{
|
||||
"head": "50790127716002f97154800ecbfb968690a9d644",
|
||||
"clear": "sage-3647",
|
||||
"worktreeUnchanged": true,
|
||||
"results": [
|
||||
{
|
||||
"mutant": "M1-badges",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-badges.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M1-agent-label",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M1-agent-label.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M2-http-links",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M2-http-links.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M3-wrong-commit",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-wrong-commit.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M3-missing-file",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M3-missing-file.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M4-fallback",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-fallback.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M4-problem",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/chat-replies-browser.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M4-problem.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M5-send",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/library-routes.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-send.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M5-project-receipt",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/library-routes.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-project-receipt.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M5-commit-input",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/library-routes.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-commit-input.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M5-origin",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/library-routes.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-origin.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M5-fetch-metadata",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/library-routes.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M5-fetch-metadata.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M6-duplicate-call",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/library-routes.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M6-duplicate-call.tap"
|
||||
},
|
||||
{
|
||||
"mutant": "M7-user-fence",
|
||||
"anchorHits": 1,
|
||||
"restored": true,
|
||||
"worktreeUnchanged": true,
|
||||
"exit": 1,
|
||||
"killed": true,
|
||||
"test": "packages/webui/tests/library-routes.test.mjs",
|
||||
"log": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2/mutants-final/M7-user-fence.tap"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
{
|
||||
"rawRoot": "/mnt/storage/scratch/tmp/fethry-row225-KEmw8wQ2",
|
||||
"logs": [
|
||||
{
|
||||
"path": "evidence/browser-final.tap",
|
||||
"rawSha256": "4e6d5874786aa5d407b59d0b701652d419d0fecda5402ede40f52b313def12b0",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/browser-final2.tap",
|
||||
"rawSha256": "194b3150e59868aab023d9ccfc75c69b82e43afba38c1b12e87a1fb3d68fe821",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/browser-green.tap",
|
||||
"rawSha256": "030d7a5c9af70d4a7ade177906fb971b29fa4cf1fc5efd39f35643ed1969cd01",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/browser-red.tap",
|
||||
"rawSha256": "db00bee89b4353efe7d324b17417347b30964f79ad4469a5dc98c6d125691f55",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/channel-proof.tap",
|
||||
"rawSha256": "cb7c0398af2a2a0e7cae24ee2c8167601cbf7e3f9cf3ed7479b2a6ef65f5da8a",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/library-full.tap",
|
||||
"rawSha256": "5447e3fce497a0dfb03ddd3eb2795dc2a179554f794e4c37458219c70736a645",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mapping-red.tap",
|
||||
"rawSha256": "8ca7310e46df2d35fa96e3aba378721ad388dce41f003ee133b684eeb7eaf245",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-attempt1/M1-agent-label.tap",
|
||||
"rawSha256": "db7ab1c64bc05b719e5164ed4ce1dd8b79ad53309d9b8e3e1ca0cc8867bca70f",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-attempt1/M1-badges.tap",
|
||||
"rawSha256": "08351b9061351812008ebd7067a9aed24a9e5386d8f6284fb44ae0f59c8f2eca",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-attempt1/M2-http-links.tap",
|
||||
"rawSha256": "69921707c219d19fb2c33a3995268b6442d783ec702050b32f78d8d2db6fb581",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-attempt1/M3-wrong-commit.tap",
|
||||
"rawSha256": "fe9fb027efae26799e2dfac539e0986402cefe806ca31479f52420e67a8df423",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M1-agent-label.tap",
|
||||
"rawSha256": "afbab60cbc12348c4d0c4be1a8c4590dd157c71f9ffc7375bbde82facb9868bb",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M1-badges.tap",
|
||||
"rawSha256": "3bb9fb9dcf74a398934d6de48c2ca99ab2eab618fb944297b3d7db00eec0e69f",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M2-http-links.tap",
|
||||
"rawSha256": "8909921fcf602fcafa94edd19c1167a8f91ed970a3e649110e3503836690e567",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M3-missing-file.tap",
|
||||
"rawSha256": "3fd2749ea5bc4245f7d49f585b82eae60f61ed150ab4dd22e8148a1e9467c8c7",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M3-wrong-commit.tap",
|
||||
"rawSha256": "85696c155f0a2844ee903c8fb38cab0a0fb6b0610c8b9ee8aa77a08cb6012ca2",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M4-fallback.tap",
|
||||
"rawSha256": "6f59f2845e0130d4d2fe1cb4aa7a435502f6043e11ed4efd67339c12e986103a",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M4-problem.tap",
|
||||
"rawSha256": "61ab08b823eb08cbb9c9eeaac82b297ea5d1272bb5dc64e9598734688b55eb80",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M5-commit-input.tap",
|
||||
"rawSha256": "b703ee5ca47e5c64749aab3edfb5ae2e085367921ffa75337f4f0e5c7b3dc404",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M5-fetch-metadata.tap",
|
||||
"rawSha256": "6951725340e4d74764552e7e406e0fc4bafeec35172de56d13bca852b92daa1a",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M5-origin.tap",
|
||||
"rawSha256": "95fcb5a92e82fa7de2972fd8f78c4f0f6d1c5f49885123e01fb1b69c3b62ba21",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M5-project-receipt.tap",
|
||||
"rawSha256": "a86806a189ef73f61ddd88479336321964eadef0793c7b45da6573ccd3e38801",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M5-send.tap",
|
||||
"rawSha256": "4cce523356a3e2041197156412eb35088f8b6d38fcceca2d9b76b6098ad17627",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M6-duplicate-call.tap",
|
||||
"rawSha256": "6dda962389fbc7379ac7e7bc79cc24b68db00d4f9bb649ab73e75eaa87a5da00",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/mutants-final/M7-user-fence.tap",
|
||||
"rawSha256": "abec36c65c3ece6e7d0c4dfd41eede62c9a585f07b3ed4cb3b8d05f0dbabaed3",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/render-green.tap",
|
||||
"rawSha256": "98463ac3d1262f84758def8bbdc27c46fa7c674bb8e8c20b9c752083163d0864",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/render-red.tap",
|
||||
"rawSha256": "03645c7a5ba0c3d285e58e1d6fd05dc58a1174b463a258b01bcf258b2141a85e",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/routes-final.tap",
|
||||
"rawSha256": "f2b58a97c301dc9e57458d82785bd50debb2254df5d411d8e8c4e6b9c0aa00d2",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/routes-green.tap",
|
||||
"rawSha256": "c34451a0eef5f82fe6ddcbbf370179d8e8a14e36168175bdeff5b32e2072e9d6",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
},
|
||||
{
|
||||
"path": "evidence/routes-red.tap",
|
||||
"rawSha256": "5ca5bafe2465209128508e5459f4826c34d00de86d1e79671196dc4e5c57f874",
|
||||
"normalization": "trailing whitespace removed from display copy"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
✔ free text renders as itself with no citations (1.332106ms)
|
||||
✔ a cited reply renders its text, then each cite with the badge from the check result (0.493062ms)
|
||||
✔ a parsed body renders as its string does (0.2706ms)
|
||||
✔ without a check result a path cite has no badge, no label and is never changed (0.175324ms)
|
||||
✔ the label and the badge come from the check result only, never from a bad entry (0.171275ms)
|
||||
✔ a web cite is a link only for https (0.254363ms)
|
||||
✔ a write notice renders its fixed line (0.201038ms)
|
||||
✔ a body without the reply or notice shape is free text: the whole body, no citations (0.315846ms)
|
||||
✔ an empty citations list is a reply with no cites (0.167552ms)
|
||||
ℹ tests 9
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 70.188818
|
||||
@@ -1,17 +0,0 @@
|
||||
✔ free text renders as itself with no citations (1.211164ms)
|
||||
✔ a cited reply renders its text, then each cite with the badge from the check result (0.457937ms)
|
||||
✔ a parsed body renders as its string does (0.255138ms)
|
||||
✔ without a check result a path cite has no badge, no label and is never changed (0.189285ms)
|
||||
✔ the label and the badge come from the check result only, never from a bad entry (0.162195ms)
|
||||
✔ a web cite is a link only for https (0.231665ms)
|
||||
✔ a write notice renders its fixed line (0.167596ms)
|
||||
✔ a body without the reply or notice shape is free text: the whole body, no citations (0.283804ms)
|
||||
✔ an empty citations list is a reply with no cites (0.145774ms)
|
||||
ℹ tests 9
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 73.226696
|
||||
@@ -1,88 +0,0 @@
|
||||
TAP version 13
|
||||
# Subtest: a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
ok 1 - a signed-in same-origin revert reaches exactly one library.revert and returns the new commit
|
||||
---
|
||||
duration_ms: 18.873338
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
ok 2 - a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata
|
||||
---
|
||||
duration_ms: 7.061714
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
ok 3 - a revert takes a project and a 40-hex commit, nothing else, before the bus is asked
|
||||
---
|
||||
duration_ms: 6.45503
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
ok 4 - a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal
|
||||
---
|
||||
duration_ms: 6.964122
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
ok 5 - a revert whose answer is lost or unusable is reported unknown and is never retried
|
||||
---
|
||||
duration_ms: 5.745801
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read takes one project query and answers the bus result for that project
|
||||
ok 6 - the status read takes one project query and answers the bus result for that project
|
||||
---
|
||||
duration_ms: 2.815134
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
ok 7 - a status answer for another project or of the wrong shape is refused; a refusal keeps its status
|
||||
---
|
||||
duration_ms: 7.105984
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the status read needs no send; a read-only Console still has no revert
|
||||
ok 8 - the status read needs no send; a read-only Console still has no revert
|
||||
---
|
||||
duration_ms: 2.002948
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
ok 9 - library routes are the owner's: a granted user is refused them, and the tester never reverts
|
||||
---
|
||||
duration_ms: 0.708324
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
ok 10 - the page gets the one library renderer, byte for byte, as a JavaScript module
|
||||
---
|
||||
duration_ms: 1.553573
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a revert receipt for another project is unknown and is never retried
|
||||
ok 11 - a revert receipt for another project is unknown and is never retried
|
||||
---
|
||||
duration_ms: 1.371025
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a granted-user HTTP session cannot reach owner library callbacks
|
||||
ok 12 - a granted-user HTTP session cannot reach owner library callbacks
|
||||
---
|
||||
duration_ms: 1.586864
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a configured revert callback remains unreachable when send is off
|
||||
ok 13 - a configured revert callback remains unreachable when send is off
|
||||
---
|
||||
duration_ms: 1.226995
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 13
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 177.384993
|
||||
@@ -1,20 +0,0 @@
|
||||
✔ a signed-in same-origin revert reaches exactly one library.revert and returns the new commit (19.266366ms)
|
||||
✔ a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata (7.415028ms)
|
||||
✔ a revert takes a project and a 40-hex commit, nothing else, before the bus is asked (6.176121ms)
|
||||
✔ a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal (7.325625ms)
|
||||
✔ a revert whose answer is lost or unusable is reported unknown and is never retried (5.0635ms)
|
||||
✔ the status read takes one project query and answers the bus result for that project (3.008847ms)
|
||||
✔ a status answer for another project or of the wrong shape is refused; a refusal keeps its status (6.314096ms)
|
||||
✔ the status read needs no send; a read-only Console still has no revert (1.440122ms)
|
||||
✔ library routes are the owner's: a granted user is refused them, and the tester never reverts (0.563979ms)
|
||||
✔ the page gets the one library renderer, byte for byte, as a JavaScript module (1.180551ms)
|
||||
✔ a revert receipt for another project is unknown and is never retried (1.12255ms)
|
||||
✔ a granted-user HTTP session cannot reach owner library callbacks (1.351581ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 182.202771
|
||||
@@ -1,76 +0,0 @@
|
||||
✔ a signed-in same-origin revert reaches exactly one library.revert and returns the new commit (18.853567ms)
|
||||
✔ a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata (6.491576ms)
|
||||
✔ a revert takes a project and a 40-hex commit, nothing else, before the bus is asked (5.647113ms)
|
||||
✔ a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal (6.08423ms)
|
||||
✔ a revert whose answer is lost or unusable is reported unknown and is never retried (5.445934ms)
|
||||
✖ the status read takes one project query and answers the bus result for that project (3.470803ms)
|
||||
✖ a status answer for another project or of the wrong shape is refused; a refusal keeps its status (5.42219ms)
|
||||
✔ the status read needs no send; a read-only Console still has no revert (1.428304ms)
|
||||
✔ library routes are the owner's: a granted user is refused them, and the tester never reverts (0.509039ms)
|
||||
✔ the page gets the one library renderer, byte for byte, as a JavaScript module (1.123365ms)
|
||||
✖ a revert receipt for another project is unknown and is never retried (1.209291ms)
|
||||
✔ a granted-user HTTP session cannot reach owner library callbacks (1.398731ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 198.061254
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/webui/tests/library-routes.test.mjs:100:1
|
||||
✖ the status read takes one project query and answers the bus result for that project (3.470803ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
403 !== 405
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/library-routes.test.mjs:107:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 403,
|
||||
expected: 405,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/library-routes.test.mjs:111:1
|
||||
✖ a status answer for another project or of the wrong shape is refused; a refusal keeps its status (5.42219ms)
|
||||
AssertionError [ERR_ASSERTION]: project-not-declared
|
||||
|
||||
400 !== 502
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/library-routes.test.mjs:119:12)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 400,
|
||||
expected: 502,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/library-routes.test.mjs:145:1
|
||||
✖ a revert receipt for another project is unknown and is never retried (1.209291ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
200 !== 503
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/fethry-row225/packages/webui/tests/library-routes.test.mjs:148:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 200,
|
||||
expected: 503,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
diff --git a/packages/bus/tests/channel-proof.test.mjs b/packages/bus/tests/channel-proof.test.mjs
|
||||
index ba0179832..435d8fa2d 100644
|
||||
--- a/packages/bus/tests/channel-proof.test.mjs
|
||||
+++ b/packages/bus/tests/channel-proof.test.mjs
|
||||
@@ -11,0 +12,2 @@
|
||||
+// Row 225 (C2-10b) re-pins webui bus.mjs for one STATUS entry, library-revert-not-agent: 409; no proof
|
||||
+// or transport change.
|
||||
@@ -32 +34 @@ const baselines = {
|
||||
- "packages/webui/src/bus.mjs": "604092cb706b152d458f299ff1071f6cd488774629012f96b672f7e9a16c0b2c",
|
||||
+ "packages/webui/src/bus.mjs": "9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658",
|
||||
diff --git a/packages/library/tests/render.test.mjs b/packages/library/tests/render.test.mjs
|
||||
index b07e26d25..9c861bfe9 100644
|
||||
--- a/packages/library/tests/render.test.mjs
|
||||
+++ b/packages/library/tests/render.test.mjs
|
||||
@@ -3 +3 @@ import assert from 'node:assert/strict';
|
||||
-import { renderItems, UNREADABLE } from '../src/index.mjs';
|
||||
+import { renderReply } from '../src/index.mjs';
|
||||
@@ -4,0 +5 @@ import { renderItems, UNREADABLE } from '../src/index.mjs';
|
||||
+// Row 225 (C2-10b): renderReply replaces renderItems (design 7.4, the reply schema of 5.2).
|
||||
@@ -6,6 +7,12 @@ const A = 'a'.repeat(40), B = 'b'.repeat(40);
|
||||
-const cite = (path, lines, quote, commit = A) => ({ path, commit, lines, quote });
|
||||
-const options = {
|
||||
- scopeOf: path => path.includes('/Playground/') ? 'playground' : path.endsWith('.md') ? 'canon' : undefined,
|
||||
- changed: ({ path, commit }) => path === 'Book One/Characters/Mara Vell.md' && commit === A,
|
||||
-};
|
||||
-const render = items => renderItems(JSON.stringify({ items }), options);
|
||||
+const cite = (path, lines, quote, commit = A, extra = {}) => ({ path, commit, lines, quote, ...extra });
|
||||
+const web = (url, quote = 'spring tides') => ({ url, quote });
|
||||
+const reply = (text, citations) => JSON.stringify({ text, citations });
|
||||
+const CITED = reply("Mara's eyes are grey in chapter 3.", [
|
||||
+ cite('Book One/Chapters/Chapter 03.md', [41, 41], 'her grey eyes', A, { as: 'canon' }),
|
||||
+ cite('Book One/Playground/What If.md', [5, 6], 'a storm', B),
|
||||
+ cite('Notes/Eyes.md', [9, 10], 'green eyes'),
|
||||
+ web('https://example.invalid/tides'),
|
||||
+]);
|
||||
+const CHECK = { citations: [{ scope: 'canon', agent: null }, { scope: 'playground', agent: null },
|
||||
+ { scope: 'supporting', agent: 'acme/scribe' }, { scope: 'web', agent: null }] };
|
||||
+const changed = ({ path, commit }) => path === 'Notes/Eyes.md' && commit === A;
|
||||
@@ -13,21 +20,4 @@ const render = items => renderItems(JSON.stringify({ items }), options);
|
||||
-// One snapshot per item kind: the plain text both channels show.
|
||||
-const SNAPSHOTS = {
|
||||
- fact: [{ kind: 'fact', text: 'Tomas is the ferryman.', cites: [cite('Book One/Characters/Tomas.md', [3, 3], 'the ferryman', B)] }],
|
||||
- finding: [{ kind: 'finding', text: 'Eye colour differs.', cites: [cite('Book One/Chapters/Chapter 01.md', [41, 41], 'her grey eyes'),
|
||||
- cite('Book One/Characters/Mara Vell.md', [9, 10], 'green eyes')] }],
|
||||
- correction: [{ kind: 'correction', text: 'Chapter 1 says grey.', from: 'green eyes', to: 'grey eyes',
|
||||
- cites: [cite('Book One/Characters/Mara Vell.md', [9, 9], 'green eyes', B)] }],
|
||||
- question: [{ kind: 'question', text: 'Is the harbor north of the town?', cites: [cite('Book One/Places/Harbor Town.md', [2, 4], 'the harbor', B)] }],
|
||||
- idea: [{ kind: 'idea', text: 'A storm could close the harbor.', cites: [cite('Book One/Playground/What If.md', [5, 5], 'a storm', B),
|
||||
- { url: 'https://example.invalid/tides', quote: 'spring tides' }] }],
|
||||
-};
|
||||
-const EXPECTED = {
|
||||
- fact: '1. Fact: Tomas is the ferryman.\n [canon] Book One/Characters/Tomas.md:3 @bbbbbbb "the ferryman"',
|
||||
- finding: '1. Finding: Eye colour differs.\n [canon] Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
|
||||
- + ' [canon] Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes" (changed since)',
|
||||
- correction: '1. Correction: Chapter 1 says grey.\n From: "green eyes"\n To: "grey eyes"\n'
|
||||
- + ' [canon] Book One/Characters/Mara Vell.md:9 @bbbbbbb "green eyes"',
|
||||
- question: '1. Question: Is the harbor north of the town?\n [canon] Book One/Places/Harbor Town.md:2-4 @bbbbbbb "the harbor"',
|
||||
- idea: '1. Idea: A storm could close the harbor.\n [playground] Book One/Playground/What If.md:5 @bbbbbbb "a storm"\n'
|
||||
- + ' [web] https://example.invalid/tides "spring tides"',
|
||||
-};
|
||||
+test('free text renders as itself with no citations', () => {
|
||||
+ for (const body of ['Grey, in chapter 3.', '', '"a JSON string"', '[1, 2]', '42', 'null'])
|
||||
+ assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body });
|
||||
+});
|
||||
@@ -35,8 +25,15 @@ const EXPECTED = {
|
||||
-for (const kind of Object.keys(SNAPSHOTS)) {
|
||||
- test(`renderItems snapshot: ${kind}`, () => {
|
||||
- const out = render(SNAPSHOTS[kind]);
|
||||
- assert.equal(out.kind, 'items');
|
||||
- assert.equal(out.text, EXPECTED[kind]);
|
||||
- assert.equal(out.items[0].kind, kind);
|
||||
- });
|
||||
-}
|
||||
+test('a cited reply renders its text, then each cite with the badge from the check result', () => {
|
||||
+ const out = renderReply(CITED, { check: CHECK, changed });
|
||||
+ assert.equal(out.kind, 'reply');
|
||||
+ assert.equal(out.text, "Mara's eyes are grey in chapter 3.");
|
||||
+ assert.deepEqual(out.citations.map(c => c.badge), ['canon', 'playground', 'supporting', 'web']);
|
||||
+ assert.equal(out.plain, "Mara's eyes are grey in chapter 3.\n"
|
||||
+ + ' [canon] Book One/Chapters/Chapter 03.md:41 @aaaaaaa "her grey eyes"\n'
|
||||
+ + ' [playground] Book One/Playground/What If.md:5-6 @bbbbbbb "a storm"\n'
|
||||
+ + ' [supporting] Notes/Eyes.md:9-10 @aaaaaaa "green eyes" (agent-written: acme/scribe) (changed since)\n'
|
||||
+ + ' [web] https://example.invalid/tides "spring tides"');
|
||||
+ assert.deepEqual(out.citations[0], { badge: 'canon', path: 'Book One/Chapters/Chapter 03.md', commit: A, lines: [41, 41],
|
||||
+ ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa', quote: 'her grey eyes', canon: true, agent: null, changed: false });
|
||||
+ assert.deepEqual(out.citations[3], { badge: 'web', url: 'https://example.invalid/tides', link: 'https://example.invalid/tides',
|
||||
+ ref: 'https://example.invalid/tides', quote: 'spring tides', agent: null, changed: false });
|
||||
+});
|
||||
@@ -44,6 +41,2 @@ for (const kind of Object.keys(SNAPSHOTS)) {
|
||||
-test('renderItems numbers several items and takes a parsed body as well as a string', () => {
|
||||
- const items = [...SNAPSHOTS.fact, ...SNAPSHOTS.question];
|
||||
- const out = renderItems({ items }, options);
|
||||
- assert.equal(out.text, `${EXPECTED.fact}\n${EXPECTED.question.replace(/^1\./, '2.')}`);
|
||||
- assert.deepEqual(out.items[1].cites[0], { badge: 'canon', path: 'Book One/Places/Harbor Town.md', commit: B, lines: [2, 4],
|
||||
- ref: 'Book One/Places/Harbor Town.md:2-4 @bbbbbbb', quote: 'the harbor', changed: false });
|
||||
+test('a parsed body renders as its string does', () => {
|
||||
+ assert.deepEqual(renderReply(JSON.parse(CITED), { check: CHECK, changed }), renderReply(CITED, { check: CHECK, changed }));
|
||||
@@ -52,4 +45,4 @@ test('renderItems numbers several items and takes a parsed body as well as a str
|
||||
-test('without options a path cite has no badge and is never marked changed', () => {
|
||||
- assert.equal(renderItems({ items: SNAPSHOTS.finding }).text,
|
||||
- '1. Finding: Eye colour differs.\n Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
|
||||
- + ' Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes"');
|
||||
+test('without a check result a path cite has no badge, no label and is never changed', () => {
|
||||
+ const out = renderReply(CITED);
|
||||
+ assert.deepEqual(out.citations.map(c => [c.badge, c.agent, c.changed]),
|
||||
+ [[null, null, false], [null, null, false], [null, null, false], ['web', null, false]]);
|
||||
@@ -58,6 +51,6 @@ test('without options a path cite has no badge and is never marked changed', ()
|
||||
-const NOTICE_SNAPSHOTS = { 'no-cited-answer': 'No cited answer', 'no-answer': 'No answer', 'turn-failed': 'The turn failed' };
|
||||
-for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
|
||||
- test(`renderItems snapshot: notice ${notice}`, () => {
|
||||
- assert.deepEqual(renderItems(JSON.stringify({ items: [], notice }), options), { kind: 'notice', notice, text });
|
||||
- });
|
||||
-}
|
||||
+test('the label and the badge come from the check result only, never from a bad entry', () => {
|
||||
+ const bad = { citations: [{ scope: 'web', agent: '' }, { scope: 'story', agent: 7 }, null, { scope: 'canon', agent: 'x/y' }] };
|
||||
+ const out = renderReply(CITED, { check: bad });
|
||||
+ // A path cite can't be web; a url cite is web by its shape whatever the check says, and never agent-written.
|
||||
+ assert.deepEqual(out.citations.map(c => [c.badge, c.agent]), [[null, null], [null, null], [null, null], ['web', null]]);
|
||||
+});
|
||||
@@ -65,14 +58,8 @@ for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
|
||||
-test('a body without the reply shape shows a fixed line and none of its text', () => {
|
||||
- const secret = 'free prose the guard would refuse';
|
||||
- const bodies = [secret, JSON.stringify(secret), '{"items": [], "notice": "other"}', JSON.stringify({ items: [], notice: 'no-answer', extra: secret }),
|
||||
- JSON.stringify({ items: [] }), JSON.stringify({ items: [{ kind: 'story', text: secret, cites: [] }] }),
|
||||
- JSON.stringify({ items: [{ kind: 'fact', text: secret }] }), JSON.stringify({ items: SNAPSHOTS.fact, prose: secret }),
|
||||
- JSON.stringify({ items: [{ kind: 'correction', text: secret, cites: [] }] }),
|
||||
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [cite('a.md', [2, 1], 'q')] }] }),
|
||||
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }] }] }),
|
||||
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }] }] }),
|
||||
- null, 42, '[]'];
|
||||
- for (const body of bodies) {
|
||||
- const out = renderItems(body, options);
|
||||
- assert.deepEqual(out, { kind: 'unreadable', text: UNREADABLE }, String(body));
|
||||
- assert.ok(!JSON.stringify(out).includes(secret));
|
||||
+test('a web cite is a link only for https', () => {
|
||||
+ const urls = { 'https://example.invalid/a': 'https://example.invalid/a', 'http://example.invalid/a': null,
|
||||
+ 'javascript:alert(1)': null, 'JAVASCRIPT:alert(1)': null, 'data:text/html,x': null, 'not a url': null, '//example.invalid': null };
|
||||
+ for (const [url, link] of Object.entries(urls)) {
|
||||
+ const out = renderReply(reply('t', [web(url)]));
|
||||
+ assert.equal(out.kind, 'reply', url);
|
||||
+ assert.equal(out.citations[0].link, link, url);
|
||||
+ assert.equal(out.citations[0].ref, url);
|
||||
@@ -80,0 +68,27 @@ test('a body without the reply shape shows a fixed line and none of its text', (
|
||||
+
|
||||
+test('a write notice renders its fixed line', () => {
|
||||
+ const body = op => JSON.stringify({ notice: 'library-write', op, path: 'Notes/Eyes.md', commit: B });
|
||||
+ assert.deepEqual(renderReply(body('write'), { name: 'Scribe' }),
|
||||
+ { kind: 'notice', op: 'write', path: 'Notes/Eyes.md', commit: B, text: 'Scribe wrote Notes/Eyes.md @bbbbbbb', plain: 'Scribe wrote Notes/Eyes.md @bbbbbbb' });
|
||||
+ assert.equal(renderReply(body('delete'), { name: 'Scribe' }).text, 'Scribe deleted Notes/Eyes.md @bbbbbbb');
|
||||
+ assert.equal(renderReply(body('write')).text, 'An agent wrote Notes/Eyes.md @bbbbbbb');
|
||||
+});
|
||||
+
|
||||
+test('a body without the reply or notice shape is free text: the whole body, no citations', () => {
|
||||
+ const bodies = [
|
||||
+ JSON.stringify({ text: 't' }), JSON.stringify({ citations: [] }), JSON.stringify({ text: 1, citations: [] }),
|
||||
+ JSON.stringify({ text: 't', citations: [], extra: 1 }), JSON.stringify({ items: [] }),
|
||||
+ reply('t', [cite('a.md', [2, 1], 'q')]), reply('t', [cite('a.md', [0, 1], 'q')]), reply('t', [cite('a.md', [1], 'q')]),
|
||||
+ reply('t', [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }]), reply('t', [cite('a.md', [1, 1], 'q', A, { as: 'playground' })]),
|
||||
+ reply('t', [cite('a.md', [1, 1], 'q', A, { scope: 'canon' })]), reply('t', [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }]),
|
||||
+ reply('t', [{ url: 'https://x.invalid' }]), reply('t', [{ url: 'https://x.invalid', quote: 'q', agent: 'x/y' }]),
|
||||
+ JSON.stringify({ notice: 'library-write', op: 'move', path: 'a.md', commit: A }),
|
||||
+ JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: 'abc' }),
|
||||
+ JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: A, by: 'x' }),
|
||||
+ ];
|
||||
+ for (const body of bodies) assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body }, body);
|
||||
+});
|
||||
+
|
||||
+test('an empty citations list is a reply with no cites', () => {
|
||||
+ assert.deepEqual(renderReply(reply('Grey.', [])), { kind: 'reply', text: 'Grey.', citations: [], plain: 'Grey.' });
|
||||
+});
|
||||
@@ -1,13 +0,0 @@
|
||||
6bdaf870be3d8d03a68eb03f5f6916150bb1b3db134ffbe84018e91d47e6434d packages/bus/tests/channel-proof.test.mjs
|
||||
0385fc49b445ff730021eb11c20b29027631e0d27350e4591d5a0936ffbdccf6 packages/library/README.md
|
||||
653ff444ba66cb73e1ca4df1f6037f15a2ec7d3d009cf614a0dc51deabda4b50 packages/library/src/index.mjs
|
||||
dfcc9f271afc7afdab908395a7f3c402100fa89b4994b20d5bcae3140d5c335e packages/library/src/render.mjs
|
||||
f3c213f3b0bd080d0bc5837f741a29e324acb91cf0c27144dee08a4113262210 packages/library/tests/render.test.mjs
|
||||
9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658 packages/webui/src/bus.mjs
|
||||
f1a0c9a55b1ae933e205c7e2b9753afe0c2f1fc9b0faa2cf18765396f99ac8dd packages/webui/src/cli.mjs
|
||||
bf49a22147be0535076640d20149116a744dad9b0b32dd667ee297a061d56134 packages/webui/src/console-bus.mjs
|
||||
793e7dd9edbe5a55c2deab3c8aef4fbd5b4c838e132e2ac9294043c7176ec6f7 packages/webui/src/public/chat.css
|
||||
af3a6f382e0d40cf2b28c31db7cb58ba775f1e242651ba42c7426bda99dc0d89 packages/webui/src/public/chat.js
|
||||
6412fe34bc640433e9cf7610f65d3574a3415c934354d8cb52e056793a81948f packages/webui/src/serve.mjs
|
||||
f191752f867a112c867ca24268f70aa99ca0903c9208f76bc4bb4c1587cb55df packages/webui/tests/chat-replies-browser.test.mjs
|
||||
df4ff385ac4ca8244ffa00faf82fcca1f4fcdac8da02a4f4d71233d4df8e1157 packages/webui/tests/library-routes.test.mjs
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run row225 mutants in disposable snapshots, only on Sage CLEAR.
|
||||
|
||||
Commit the tested candidate first. Never change the worktree or shared dependencies.
|
||||
A timeout, import failure or process failure is not a mutation kill.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
BROWSER = 'packages/webui/tests/chat-replies-browser.test.mjs'
|
||||
ROUTES = 'packages/webui/tests/library-routes.test.mjs'
|
||||
CHAT = 'packages/webui/src/public/chat.js'
|
||||
SERVE = 'packages/webui/src/serve.mjs'
|
||||
RENDER = 'packages/library/src/render.mjs'
|
||||
MUTANTS = [
|
||||
('M1-badges', RENDER, 'badge: scope, path:', 'badge: null, path:', BROWSER),
|
||||
('M1-agent-label', RENDER, 'canon: c.canon, agent,', 'canon: c.canon, agent: null,', BROWSER),
|
||||
('M2-http-links', RENDER, "new URL(url).protocol === 'https:' ? url : null", 'url', BROWSER),
|
||||
('M3-wrong-commit', CHAT, 'e.file !== e.last', 'cite.commit !== e.last', BROWSER),
|
||||
('M3-missing-file', CHAT, "SHA.test(e?.file ?? '') && SHA.test(e?.last ?? '')", "SHA.test(e?.last ?? '')", BROWSER),
|
||||
('M4-fallback', CHAT, 'Array.isArray(limits.fallback) ? limits.fallback.filter', 'Array.isArray(v.replyLimitsFallback) ? v.replyLimitsFallback.filter', BROWSER),
|
||||
('M4-problem', CHAT, "if (CODE.test(limits.problem ?? '')) parts.push(`<code>${txt(limits.problem)}</code>`);", '', BROWSER),
|
||||
('M5-send', SERVE, 'if (!send || !libraryRevert || !named)', 'if (!libraryRevert || !named)', ROUTES),
|
||||
('M5-project-receipt', SERVE, 'result.project !== value.project || ', '', ROUTES),
|
||||
('M5-commit-input', SERVE, '!COMMIT.test(value.commit)', 'false', ROUTES),
|
||||
('M5-origin', SERVE, "if (one(req, 'origin') !== ownOrigin(preview)) return refuse(403, 'cross-origin request refused', 'a revert needs the Console\\'s own Origin');", '', ROUTES),
|
||||
('M5-fetch-metadata', SERVE, "if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site request refused', 'a revert needs same-origin Fetch Metadata');", '', ROUTES),
|
||||
('M6-duplicate-call', SERVE, 'result = await libraryRevert({ project: value.project, commit: value.commit });', 'result = await libraryRevert({ project: value.project, commit: value.commit }); await libraryRevert({ project: value.project, commit: value.commit });', ROUTES),
|
||||
('M7-user-fence', SERVE, "if (path === '/api/chat' || path.startsWith('/api/chat/')) return true;", "if (path.startsWith('/api/library/')) return true; if (path === '/api/chat' || path.startsWith('/api/chat/')) return true;", ROUTES),
|
||||
]
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--clear', required=True, help='Sage CLEAR authorizing this run')
|
||||
parser.add_argument('--output', required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
repo = Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip())
|
||||
if subprocess.check_output(['git', 'status', '--porcelain', '--', 'packages'], text=True).strip():
|
||||
raise SystemExit('Commit tested packages first: snapshot must match candidate.')
|
||||
head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip()
|
||||
before = {p: digest(repo / p) for p in [CHAT, SERVE, RENDER]}
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
results = []
|
||||
with tempfile.TemporaryDirectory(prefix='mosaic-row225-mutants-') as directory:
|
||||
scratch = Path(directory)
|
||||
archive = scratch / 'candidate.tar'
|
||||
with archive.open('wb') as stream:
|
||||
subprocess.run(['git', 'archive', 'HEAD', 'packages', 'roles', 'contracts', 'tasks', 'missions', 'package.json', 'RELEASE'], stdout=stream, check=True)
|
||||
for name, target, old, new, test in MUTANTS:
|
||||
tree = scratch / name
|
||||
tree.mkdir()
|
||||
with tarfile.open(archive) as bundle:
|
||||
bundle.extractall(tree, filter='data')
|
||||
(tree / 'node_modules').symlink_to((repo / 'node_modules').resolve(), target_is_directory=True)
|
||||
path = tree / target
|
||||
source = path.read_text()
|
||||
hits = source.count(old)
|
||||
if hits != 1:
|
||||
raise SystemExit(f'{name}: expected one anchor, found {hits}')
|
||||
path.write_text(source.replace(old, new, 1))
|
||||
command = ['timeout', '-k', '10', '240' if test == BROWSER else '120', 'node', '--test', '--test-reporter=tap', '--test-concurrency=1', '--test-timeout=30000', test]
|
||||
run = subprocess.run(command, cwd=tree, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=os.environ)
|
||||
log = args.output / f'{name}.tap'
|
||||
log.write_text(run.stdout)
|
||||
path.write_text(source)
|
||||
restored = digest(path) == hashlib.sha256(source.encode()).hexdigest()
|
||||
worktree_clean = before == {p: digest(repo / p) for p in before}
|
||||
if not restored or not worktree_clean:
|
||||
raise SystemExit(f'{name}: restore/hash check failed')
|
||||
killed = run.returncode == 1 and 'ERR_ASSERTION' in run.stdout and 'not ok ' in run.stdout
|
||||
results.append({'mutant': name, 'anchorHits': hits, 'restored': restored, 'worktreeUnchanged': worktree_clean, 'exit': run.returncode, 'killed': killed, 'test': test, 'log': str(log)})
|
||||
print(json.dumps(results[-1]), flush=True)
|
||||
shutil.rmtree(tree)
|
||||
if not killed:
|
||||
break
|
||||
after = {p: digest(repo / p) for p in before}
|
||||
report = {'head': head, 'clear': args.clear, 'worktreeUnchanged': before == after, 'results': results}
|
||||
(args.output / 'summary.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
if before != after or len(results) != len(MUTANTS) or not all(r['killed'] for r in results):
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,211 @@
|
||||
# Row 283 (#1788) C2-01d build notes (Honker)
|
||||
|
||||
Append-only. Design: DESIGN.md d2 16e809457, APPROVE webby-0172. Gate owner Sage, reviewer Webby.
|
||||
|
||||
## 2026-10-23 build on 45dca69f6 (sage-3577)
|
||||
|
||||
Branch honker/row283-build from origin/refactor 45dca69f6e4bce52834587f66f2ff33ba839ab8a. The old branch
|
||||
honker/row283 (16e809457) held only the design, so its two commits were cherry-picked as 398314cf4
|
||||
(d1) and 4f66dccde (d2). The DESIGN.md blob equals 16e809457's.
|
||||
|
||||
Commits:
|
||||
- 331653b38: code, README and tests T1-T8.
|
||||
- 28918824c: the survey.test:73 re-pin, alone (mos-1330 item 4).
|
||||
- this file.
|
||||
|
||||
Nothing has run. No node, test or mutant ran during the build: Darkwing held the slot. Every count
|
||||
below is a plan until Sage's CLEAR.
|
||||
|
||||
### Drift check (sage-3577)
|
||||
|
||||
`git diff --stat 94c33a43c 45dca69f6` (design base to build base) over the paths the tests read or
|
||||
hash-pin:
|
||||
- No change in packages/library, packages/business/src/business.mjs or
|
||||
packages/business/tests/business.test.mjs.
|
||||
- No test hash-pins a library file. bus library.test and library-sync.test import index.mjs, and
|
||||
stack-copy.test only checks that index.mjs exists.
|
||||
- webui library-cli.test changed one line and doesn't read scope. The `classify` in webui reads.mjs is
|
||||
the business one, not the library's.
|
||||
|
||||
I redo the drift check on BASE..refactor before the in-review move.
|
||||
|
||||
### N3 (webby-0169): deepEquals of the mapping output outside packages/library
|
||||
|
||||
`git grep -n playground -- 'packages/*/tests/*' ':!packages/library/*'` and the files naming
|
||||
`universes` give two files:
|
||||
- business.test.mjs:20 compares `validateBusinessDocument(...).projects.books` with
|
||||
`checkMapping(mappingEntry())`. That is output against output, so `supporting: []` shows up on both
|
||||
sides. :27 compares only `.playground`.
|
||||
- library-projects-protected.test.mjs compares file bytes (:31, :32) and change paths (:58), never the
|
||||
mapping output.
|
||||
|
||||
No re-pin is needed outside library. Inside library, only the draft deepEqual at mapping.test:48 gains
|
||||
`supporting: []`, as the design says.
|
||||
|
||||
### Q1 departure (sage-2923) and the :73 re-pin (mos-1330 item 4)
|
||||
|
||||
survey.mjs:65 counts `supporting: 0` and formatSurvey prints it. T8 is the only added test
|
||||
(survey.test "C2-01d: with --mapping the survey counts supporting files").
|
||||
|
||||
mos-1330 item 4, packages/library/tests/survey.test.mjs:73, commit 28918824c:
|
||||
- old: `'scopes canon 5, playground 1, unmapped 3; indexed 6',`
|
||||
- new: `'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6',`
|
||||
|
||||
N2: the pin gains only `supporting 0`. No other text on the line or in that test moved.
|
||||
|
||||
### Q2 tradeoff (webby-0169)
|
||||
|
||||
A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. That goes past
|
||||
the brief, but it is the fail-closed reading, and it uses an existing code: classify checks supporting
|
||||
first, so that playground prefix could never match. The cost is that she can't nest a playground under
|
||||
a supporting folder, though she can nest the other way (a playground holding a supporting folder).
|
||||
|
||||
### Handoffs
|
||||
|
||||
- Q4 (webby-0169, sage-2930): the M12 delete rule ("A delete also needs the path to exist at the tip as
|
||||
a 100644 blob", case2 DESIGN.md :308) is not in supportingPath. It binds row 288 (C2-02) via Gitea
|
||||
#1793 comment 30738, with tests for a missing path and a non-100644 entry.
|
||||
- A save with no `editor` refusing is C2-02's (DESIGN.md section 1).
|
||||
|
||||
### Choices the design left open
|
||||
|
||||
- The book loop is now `againstBooks(owners, list, code)` in mapping.mjs, called once per list. The
|
||||
playground's messages and codes are byte-equal to before.
|
||||
- A non-string path refuses `library-write-story-path` ("the path must be a string") as part of
|
||||
check 1. A non-array `tree` is a TypeError (DESIGN.md section 3).
|
||||
- Check 8 builds the tree's names as every path and every folder above it, keyed by
|
||||
`normalize('NFC').toLowerCase()`. A name on the way refuses if its key holds any name other than
|
||||
itself, so a tree that already holds case twins refuses a write to either (R7).
|
||||
|
||||
### Mutants (exact strings)
|
||||
|
||||
Each applies one replacement to the build tip. Run one at a time, and restore with `git checkout --`
|
||||
on the one file before the next.
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M1 | src/scope.mjs | `const prefix = entry.supporting.find(p => inside(p, path));` | `const prefix = entry.supporting.find(p => inside(p, path) && !entry.playground.some(q => inside(q, path)));` | scope.test "supporting comes before playground", supporting.test "passes at book level" (both-lists path) |
|
||||
| M2 | src/mapping.mjs | `if ((inside \|\| holding) && owner !== id)` | `if ((inside \|\| holding) && owner !== id && list !== 'supporting')` | mapping.test "each supporting prefix rule", business.test C2-04 |
|
||||
| M3 | src/supporting.mjs | `for (const p of own) if (LINKS.has(modes.get(p)))` | `for (const p of own) if (false && LINKS.has(modes.get(p)))` | supporting.test "symlink or gitlink" |
|
||||
| M4 | src/supporting.mjs | `const fold = name => name.normalize('NFC').toLowerCase();` | `const fold = name => name.normalize('NFC');` | supporting.test "case or Unicode form" |
|
||||
| M4b | src/supporting.mjs | `const fold = name => name.normalize('NFC').toLowerCase();` | `const fold = name => name.toLowerCase();` | supporting.test "case or Unicode form" (NFD case) |
|
||||
| M5 | src/supporting.mjs | `Buffer.byteLength(path, 'utf8') > MAX_BYTES` | `path.length > MAX_BYTES` | supporting.test "1025 bytes" |
|
||||
| M6 | src/mapping.mjs | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;` | `const IDENT_BAD = /[<>]/;` | mapping.test "editor is optional" |
|
||||
| M6b | src/mapping.mjs | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;` | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f]/;` | mapping.test "editor is optional" |
|
||||
| M7 | src/supporting.mjs | `if (place.scope !== 'supporting' \|\| place.universe !== universe)` | `if (place.scope !== 'supporting')` | supporting.test "outside the universe's supporting folders" |
|
||||
| M8 | src/mapping.mjs | `if (sid === pid && s !== p && holds(s, p))` | `if (false && sid === pid && s !== p && holds(s, p))` | mapping.test "each supporting prefix rule" (Workshop case) |
|
||||
|
||||
(The `\|` in the table is an escaped `|` in the source text.) Each mutant runs the one test file named,
|
||||
plus business.test for M2, under `timeout -k 10 600`. A mutant passes the gate only when its run is red
|
||||
on the named test and the restored file's sha256 equals the build tip's.
|
||||
|
||||
### Gate plan (on Sage's CLEAR)
|
||||
|
||||
1. The touched files whole, each under `timeout -k 10 600`: library mapping, scope, supporting and
|
||||
survey tests, and business.test.mjs. Counts go below.
|
||||
2. M1-M8 as above.
|
||||
3. The full gate, under mos-1275 timeouts: `node --test packages/library/tests/` and
|
||||
`node --test packages/business/tests/` each under `timeout -k 10 1800`, and every scripts/test-*.sh
|
||||
under `timeout -k 10 300`. pipefail and tee, with skips named (test-task Docker groups).
|
||||
4. Before in-review: grep packages/*/tests for a 64-hex pin on any changed file and for the changed
|
||||
message text, then the drift check on BASE..refactor.
|
||||
|
||||
## 2026-10-23 Webby early look (webby-0227, static, not a verdict)
|
||||
|
||||
- E1: business.test.mjs is an existing test file with a 4-line addition after the playground refusal.
|
||||
The in-review request lists it next to survey.test:73.
|
||||
- E3 fixed: supportingPath now runs the string check before `JSON.stringify`, so a BigInt (or any
|
||||
non-string) path refuses `library-write-story-path` instead of throwing a TypeError.
|
||||
supporting.test's string case now covers 7, null, undefined, 1n and an array.
|
||||
- E2 open: `editor` takes surrounding whitespace, and git trims it at commit, so the saved ident and
|
||||
the commit ident can differ. DESIGN.md section 1 says "non-empty after trim". Refusing
|
||||
`value.trim() !== value` would change that approved rule, so it went to Sage instead of into the
|
||||
build.
|
||||
|
||||
## 2026-10-23 E2 departure (sage-3601)
|
||||
|
||||
A departure from DESIGN.md section 1 (which stays unedited). The design says each `editor` value is
|
||||
"non-empty after trim". Sage ruled that surrounding whitespace refuses too, because git trims an
|
||||
ident at commit, so a padded value would save one name and commit another. mapping.mjs now adds
|
||||
`if (value.trim() !== value) bad(`editor.${key} must not start or end with whitespace`)` after the
|
||||
IDENT_BAD check. `trim()` covers Unicode whitespace, so U+00A0 and U+3000 refuse as well. Inner
|
||||
whitespace is kept ("Her Middle Name" passes).
|
||||
|
||||
T3 gains a case per key for leading, trailing and both-ends spaces, U+00A0 and U+3000, plus one inner
|
||||
whitespace pass.
|
||||
|
||||
M6 grows a third part:
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6c | src/mapping.mjs | `if (value.trim() !== value) bad(` | `if (false && value.trim() !== value) bad(` | mapping.test "editor is optional" (padded cases) |
|
||||
|
||||
Fixture grep: `git grep -nE "\beditor\b|\bauthor\b" -- 'packages/library/tests/*' 'packages/business/tests/*'`,
|
||||
leaving out mapping.test.mjs, finds nothing. packages/business/tests has no fixtures directory. The
|
||||
library fixture mapping.json has no `editor`. The positive control (`vaultPrefix` in mapping.json)
|
||||
matches once. No fixture maps an editor or author with surrounding whitespace.
|
||||
|
||||
## 2026-10-23 E4 (webby-0230, sage-3606), same departure as E2
|
||||
|
||||
Claimed: `queue move 283 in-progress`, op honker-283-start, rev 3642, 19:10:49Z (briefed→in-progress).
|
||||
|
||||
Git's ident.c `crud()` strips any byte <= 0x20 and `, : ; < > " \ '` from both ends of an ident. I
|
||||
checked that with git 2.55.0: `Jane,` `"Jane"` `'Jane'` `Jane;` `Jane:` `Jane\` and `,Jane` all
|
||||
commit as `Jane`, while `Jane.` and `Ja,ne` are kept. mapping.mjs now takes the set from crud()
|
||||
rather than from that list: `IDENT_CRUD_END = /^[\u0000-\u0020,:;<>"\\']|[\u0000-\u0020,:;<>"\\']$/`.
|
||||
The E2 check became `if (value.trim() !== value || IDENT_CRUD_END.test(value))`, which refuses
|
||||
library-mapping-invalid with "editor.<key> must not start or end with whitespace or any of , : ; " ' \".
|
||||
`trim()` still catches Unicode whitespace (U+00A0, U+3000), which crud() leaves alone.
|
||||
|
||||
Inner characters: IDENT_BAD (`/[\u0000-\u001f\u007f-\u009f<>]/`) already refuses `<`, `>`, newline,
|
||||
tab and every other control anywhere in the value. T3 now pins that at the start, the end and the
|
||||
middle. Inner `, : ; " ' \` are kept, since git keeps them, and T3 pins that too.
|
||||
|
||||
Tests:
|
||||
- T3 has one case per character in [space, U+00A0, U+3000, `,`, `:`, `;`, `"`, `'`, `\`], at the start
|
||||
and at the end, for each key. `<`, `>`, newline and tab are pinned at the start, end and middle.
|
||||
Four accepted names have inner crud characters.
|
||||
- New round-trip test, "an accepted editor round-trips through a real git commit unchanged". It
|
||||
commits in a scratch repo with editor `Doe, Jane Q.` / `[email protected].` as author and
|
||||
committer, with GIT_AUTHOR_* and GIT_COMMITTER_* stripped from the env, and asserts `git log`
|
||||
shows exactly the saved values. I preflighted that commit with git 2.55.0 by hand: both values came
|
||||
back unchanged.
|
||||
|
||||
M6c now replaces the whole end check (this supersedes the earlier M6c row):
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6c | src/mapping.mjs | `if (value.trim() !== value \|\| IDENT_CRUD_END.test(value))` | `if (false)` | mapping.test "editor is optional" (end cases) |
|
||||
| M6d | src/mapping.mjs | `if (value.trim() !== value \|\| IDENT_CRUD_END.test(value))` | `if (value.trim() !== value)` | mapping.test "editor is optional" (`,` `:` `;` `"` `'` `\` cases) |
|
||||
|
||||
## 2026-10-23 E5 (webby-0236, not blocking), fixed
|
||||
|
||||
The round-trip test stripped only GIT_AUTHOR_* and GIT_COMMITTER_*. With GIT_DIR, GIT_WORK_TREE or
|
||||
GIT_INDEX_FILE set (inside a hook, say), GIT_DIR beats `-C root` and the commit would land in the
|
||||
outer repo, and the test read global and system config. It now deletes GIT_DIR, GIT_WORK_TREE,
|
||||
GIT_COMMON_DIR and GIT_INDEX_FILE (as queue/tests/helpers.mjs gitEnv does) and sets
|
||||
GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1 (as ledger.test.mjs does). The `-c user.*`
|
||||
ident still applies. Test only; no mutant changes.
|
||||
|
||||
## 2026-10-23 sage-2925: optional editor.user (sage-3649)
|
||||
|
||||
sage-2925 (recorded in ludwig/row286-r2 agents/ludwig/work/queue-286/BUILD.md, Q4) ruled that row 283
|
||||
adds an optional `editor.user`, her Gitea login for the `library-human` slot, which row 288 (C2-02)
|
||||
passes to `humanCredential(context, {user})`. It reached this row after the DESIGN.md d2 approval
|
||||
(ludwig-0068, bubba-0009, sage-3649), so it departs from DESIGN.md section 1 ("exactly the keys
|
||||
`name` and `email`"). DESIGN.md stays unedited.
|
||||
|
||||
- mapping.mjs: `only(entry.editor, ['name', 'email', 'user'], 'editor')`. When present, `user` must be
|
||||
a string matching USER (`/^[A-Za-z0-9_.-]{1,100}$/`, the entry `user` check), else
|
||||
library-mapping-invalid "editor.user must be a plain account name". The output carries `user` only
|
||||
when given. An absent `editor.user` refusing library-human-missing is 288's (bubba-0009).
|
||||
- README: the `editor` line names `user` and the end-character rule from E2/E4.
|
||||
- Tests: T3 (mapping.test "editor is optional") gains one accepted case (`her.login-1`, kept in the
|
||||
output) and refused cases for '', an inner space, '@', 101 characters, 7 and null. They go in
|
||||
mapping.test with the other editor cases, not in the new supporting.test, which tests write paths.
|
||||
- No test outside mapping.test reads `editor` or pins mapping.mjs or README (git grep).
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6e | src/mapping.mjs | `if (Object.hasOwn(entry.editor, 'user') && (typeof entry.editor.user !== 'string' \|\| !USER.test(entry.editor.user)))` | `if (false)` | mapping.test "editor is optional" (editor.user refusals) |
|
||||
| M6f | src/mapping.mjs | `...(Object.hasOwn(entry.editor, 'user') ? { user: entry.editor.user } : {})` | `...({})` | mapping.test "editor is optional" (`her.login-1` kept) |
|
||||
@@ -0,0 +1,191 @@
|
||||
# Row 283 (#1788) C2-01d design, draft 2 (Honker)
|
||||
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md "C2-01d" at refactor 94c33a43c. Sources: the design at
|
||||
agents/gosalyn/work/case2/DESIGN.md b68d79663, the ruling after r11 (:207), the M12 paragraph (:300),
|
||||
1.1 (:755) and 1.2 (:817). Base 94c33a43c. Reviewer Webby, gate owner Sage. Nothing is built yet. The
|
||||
build waits for 282 to land (sage-2906).
|
||||
|
||||
## 1. Mapping (packages/library/src/mapping.mjs)
|
||||
|
||||
`supporting`:
|
||||
- Every universe kind takes an optional `supporting` list: plain `['name','books','playground','supporting']`,
|
||||
hybrid adds it to its seven keys, and a draft gets `['kind','name','playground','supporting']`.
|
||||
- Each value is checked as a playground prefix is: a string ending in `/` whose body passes `folder()`.
|
||||
- The output carries `supporting` on every universe, `[]` when absent, the way a draft carries
|
||||
`books: []`. classify then needs no `?? []`. The business writer writes the raw document
|
||||
(organization.mjs:195), not this output, so no business file changes shape.
|
||||
- The prefix rules, across every entry (ruling after r11):
|
||||
- A supporting prefix holding another supporting prefix, or one listed twice, refuses
|
||||
`library-mapping-invalid`.
|
||||
- A prefix equal to one of its own entry's book folders, or holding one, refuses `library-mapping-invalid`.
|
||||
- A prefix inside or holding another entry's book folder refuses `library-supporting-other-universe`.
|
||||
- Level comes from where the prefix sits, as for the playground: inside one of its own entry's books
|
||||
is book level, and outside every book is universe level. A draft's prefixes are all universe level.
|
||||
- Between the two lists (my choice, Q2):
|
||||
- A supporting prefix may equal a playground prefix of the same entry. That folder is writable.
|
||||
- A playground prefix may hold a supporting prefix of the same entry. The inner folder is supporting
|
||||
and the rest stays playground.
|
||||
- A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. classify
|
||||
checks supporting first, so that playground prefix could never match.
|
||||
- A supporting prefix equal to, holding or inside a playground prefix of another entry refuses
|
||||
`library-mapping-invalid`. Otherwise one path would have two universes.
|
||||
- The book loop at mapping.mjs:128-140 becomes a helper that both lists call with their own code and
|
||||
label. The playground's messages and codes stay byte-equal, so no existing test text moves.
|
||||
|
||||
`editor`:
|
||||
- The entry takes an optional `editor`. When present, it is an object with exactly the keys `name` and
|
||||
`email` (via `only()`). Each value is a string that is non-empty after trim and has no control
|
||||
character (`[\u0000-\u001f\u007f-\u009f]`), `<` or `>` (webby-0169 R1: git drops them from an ident
|
||||
and commits under a different name, so C2-02 would never see a failure). Anything else refuses
|
||||
`library-mapping-invalid`.
|
||||
- The output carries `editor: { name, email }` only when present, as `user` and `vaultPrefix` do. A save
|
||||
with no `editor` refusing is C2-02's job.
|
||||
|
||||
## 2. Scope (scope.mjs)
|
||||
|
||||
- `classify` gets a supporting loop before the playground loop (design 1.2). It returns `place(...)`
|
||||
with `{ scope: 'supporting', universe, book }`. The book is the book folder holding the prefix, or
|
||||
null at universe level and in a draft. The type and folder come from the first folder under the
|
||||
prefix, as for the playground.
|
||||
- The `.md` check stays first, so a `.canvas` or `.base` under a supporting prefix stays unmapped.
|
||||
- The comment above `classify` names the three prefix scopes and the order.
|
||||
|
||||
## 3. supportingPath (new supporting.mjs)
|
||||
|
||||
`supportingPath(mapping, universe, path, tree)` returns `path` or throws
|
||||
`LibraryError('library-write-story-path', ...)`. It is pure and reads no git.
|
||||
|
||||
- `tree` is the array `listTree` returns (reader.mjs:26, `{ mode, type, oid, size, path }`), so the
|
||||
C2-02 broker passes its listing as it is (Q3). A non-array `tree` throws a TypeError, because that is
|
||||
a caller bug, not a path class.
|
||||
- The checks run in this order, each with its own message naming the class:
|
||||
1. The path must be a string of at most 1,024 UTF-8 bytes (`Buffer.byteLength`).
|
||||
2. It must have no control character, using the same control class as `editor` (not `<` or `>`).
|
||||
3. It must have no backslash.
|
||||
4. It must not be absolute (a leading `/`).
|
||||
5. It must have no empty, `.` or `..` segment. That also catches `//` and a trailing `/`.
|
||||
6. `classify(mapping, path)` must give scope `supporting` and that same `universe`. This one check
|
||||
covers an unknown universe, an unmapped path, a `.canvas`, `.base` or attachment, a canon or
|
||||
story path, a playground path that isn't also supporting, and another universe's prefix. The scope
|
||||
rule then has one source, and M1 below hits both.
|
||||
7. Every proper prefix of the path (`a`, `a/b`, ...) and the path itself must not be in `tree` with
|
||||
mode `120000` (symlink) or `160000` (gitlink).
|
||||
8. No prefix of the path, and not the path itself, may equal a tree path or a tree directory
|
||||
(derived from the entries' paths) after `normalize('NFC').toLowerCase()` on both sides, unless
|
||||
it equals that tree path exactly. NFC as well as case, because APFS treats NFC and NFD forms of
|
||||
a name as one file (webby-0169 N1).
|
||||
Directories are checked too, because the folders under a supporting prefix are hers to name. A
|
||||
path in a tree that holds both `a.md` and `A.md` refuses, which matches R7.
|
||||
- The refusal messages quote the path with JSON, except for the control-character class, whose message
|
||||
leaves the path out.
|
||||
- The delete rule ("A delete also needs the path to exist at the tip as a 100644 blob", M12) isn't in
|
||||
the brief's supportingPath list. It stays with C2-02's delete (Q4).
|
||||
The C2-02 brief doesn't name it (only design :308), so BUILD.md carries it to C2-02 under Handoffs
|
||||
(webby-0169 Q4).
|
||||
- index.mjs exports `supportingPath`.
|
||||
|
||||
## 4. README (packages/library/README.md)
|
||||
|
||||
- Mapping: `supporting` and its rules, the cross-list rules, and `editor`.
|
||||
- Refusals: a `library-supporting-other-universe` row, and the new cases in the
|
||||
`library-mapping-invalid` row.
|
||||
- Scope: supporting first, then playground, then canon.
|
||||
- A new section, "Agent write paths", for supportingPath and its classes.
|
||||
|
||||
## 5. Callers of classify outside files owned (Q1 for Sage)
|
||||
|
||||
- survey.mjs:65 builds `{ canon: 0, playground: 0, unmapped: 0 }` and increments
|
||||
`scopes[classify(...).scope]`. A supporting file adds `supporting: NaN`, and formatSurvey (:92) never
|
||||
prints it. I propose a departure: add `supporting: 0` and print it, so the line becomes
|
||||
`scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6`. That re-pins survey.test:73 and
|
||||
adds a count test. Without it, the survey reports garbage once she marks a folder. The departure
|
||||
covers two files, packages/library/src/survey.mjs and packages/library/tests/survey.test.mjs.
|
||||
- No change in the rest, each for a reason:
|
||||
- reader.mjs:183 skips only unmapped, so supporting files are indexed like playground files.
|
||||
- R5 (:207) flags only canon to playground, as design 1.3 R5 says. A canon link to a supporting file
|
||||
raises nothing.
|
||||
- render.mjs:23 gives supporting a null badge. The supporting badge belongs to C2-10b (design :1415).
|
||||
- mirror.mjs:246 refuses only unmapped. Reading supporting files by scope is C2-05c's.
|
||||
- No bus or webui source filters on scope (git grep at 94c33a43c).
|
||||
- M9 already protects the whole `projects` key from agent edits (library-projects-protected.test). No
|
||||
test is added there.
|
||||
|
||||
## 6. Tests
|
||||
|
||||
- mapping.test:
|
||||
- T1: `supporting` at book level, at universe level, in a hybrid and in a draft. The output carries
|
||||
it, with `[]` when absent. The draft `deepEqual` at :48 gains `supporting: []`.
|
||||
- T2: one test per prefix rule.
|
||||
- These refuse `library-mapping-invalid`: no trailing `/`, a bad folder, one holding another, one
|
||||
listed twice, one equal to its own book, one holding its own book, one holding a playground
|
||||
prefix, and one overlapping another entry's playground.
|
||||
- These refuse `library-supporting-other-universe`: one inside another universe's book, and one
|
||||
holding another universe's book.
|
||||
- An equal same-entry playground passes.
|
||||
- T3: `editor` passes, and absent passes. It refuses `library-mapping-invalid` for a non-object, an
|
||||
array, an extra key, a missing key, an empty or blank value, a non-string, and a control character,
|
||||
`<` or `>` in either value.
|
||||
- scope.test, T4:
|
||||
- Supporting at book level, universe level and in a draft each give their universe and book.
|
||||
- A folder in both lists is `supporting`, and a playground-only folder is `playground`.
|
||||
- A supporting `.canvas` or `.base` is unmapped.
|
||||
- supporting.test (new):
|
||||
- T5 passes at book level, universe level, in a draft, in a both-lists folder, and at exactly 1,024 bytes.
|
||||
- T6 has one refusal per class against a fixture tree with a symlink, a gitlink and a case variant.
|
||||
The classes are:
|
||||
- unmapped, `.canvas`, `.base`, an attachment, canon, playground-only, another universe's
|
||||
supporting prefix, and an unknown universe;
|
||||
- absolute, `..`, `.`, `//`, a trailing `/`, a backslash, a C0 control and DEL;
|
||||
- a symlink folder, a gitlink folder, and a symlink at the path itself;
|
||||
- a case-variant file, a case-variant folder, and an NFD-form variant of an NFC tree name;
|
||||
- 1,025 bytes made with a multi-byte character, so bytes and characters differ.
|
||||
- Each asserts the code `library-write-story-path`.
|
||||
- business.test: T7 adds one assertion that `validateBusinessDocument` refuses
|
||||
`library-supporting-other-universe`.
|
||||
- survey.test (only if Q1 is approved): T8 counts one supporting file, and :73 is re-pinned.
|
||||
|
||||
## 7. Mutants (exact strings go in BUILD.md)
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
|---|---|---|
|
||||
| M1 | classify checks playground before supporting (the brief's mutant) | T4 both-lists, T5 both-lists |
|
||||
| M2 | the other-universe fail for supporting is dropped | T2 |
|
||||
| M3 | the symlink and gitlink check is dropped | T6 |
|
||||
| M4 | the case check is dropped | T6 |
|
||||
| M4b | check 8 drops `normalize('NFC')` | T6 NFD variant |
|
||||
| M5 | the byte length uses `path.length` | T6 1,025 bytes |
|
||||
| M6 | the `editor` control-character check is dropped | T3 |
|
||||
| M6b | the `editor` `<` and `>` check is dropped | T3 |
|
||||
| M7 | supportingPath skips the universe match | T6 other universe |
|
||||
| M8 | the supporting-holds-playground check is dropped | T2 |
|
||||
|
||||
## 8. Gate
|
||||
|
||||
- Webby approves the manifest.
|
||||
- The library and business suites and every scripts/test-*.sh are green on Sage's gate.
|
||||
- Timeouts (mos-1275): each package `timeout -k 10 1800`, each script `timeout -k 10 300`, each single
|
||||
file and mutant `timeout -k 10 600`. These come from my row 256 gate at 10:34Z.
|
||||
- Before in-review, grep packages/*/tests for 64-hex pins on the changed files and for the changed
|
||||
message text, then drift-check BASE..refactor.
|
||||
|
||||
## 9. Questions
|
||||
|
||||
- Q1 (Sage): approve the survey.mjs and survey.test.mjs departure in section 5?
|
||||
- Q2 (Webby): the cross-list rules in section 1?
|
||||
- Q3 (Webby): `tree` as the `listTree` array?
|
||||
- Q4 (Webby): the delete's 100644 check stays with C2-02?
|
||||
- Q5 (Webby): `editor` refuses only blank values and control characters, as the brief says. `<` and `>`
|
||||
would break a git ident. I'd leave them to C2-02's commit step unless you want them refused here.
|
||||
|
||||
## 10. Draft 2 (webby-0169)
|
||||
|
||||
- R1 folded: `editor` refuses `<` and `>` too, with T3 cases and mutant M6b.
|
||||
- Q2 accepted. BUILD.md records the tradeoff: refusing a supporting prefix that holds a playground
|
||||
prefix goes past the brief, but it is the fail-closed reading and uses an existing code. The cost
|
||||
is that she can't nest a playground under a supporting folder, though she can nest the other way.
|
||||
- Q3 accepted. Q4: carried to C2-02 under Handoffs in BUILD.md, and Sage was asked to add it to C2-02's brief.
|
||||
- N1 folded into check 8 (NFC and case), with an NFD case in T6 and mutant M4b.
|
||||
- N2: the survey.test:73 pin gains only `supporting`, and the count test is the only addition
|
||||
(mos-1330, its own commit).
|
||||
- N3: before the build, re-grep tests outside packages/library for a `deepEqual` of the mapping output
|
||||
on the build base.
|
||||
@@ -1,177 +0,0 @@
|
||||
# Row 225 (C2-10b) Console chat: replies, citations, write notices, status line
|
||||
|
||||
Owner: Morgana, from sage-3582 (18:53Z 10-23). Base: origin/refactor 45dca69f6. Branch: morgana/row225.
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md, C2-10b. Design: agents/gosalyn/work/case2/DESIGN.md 7.4,
|
||||
5.2 (the reply schema and the check result) and the write notice and `library.revert` (about line 286).
|
||||
|
||||
## Ruling on the dependency gap (sage-3591)
|
||||
|
||||
225 builds against the design contracts. `renderReply` stays pure, and the caller supplies the answers
|
||||
it can't know. The revert route proxies to the bus verb and shows the refusal code until the verb
|
||||
exists. Landing order: 225 lands after 217 (C2-06), not after 288 (C2-02). Sage adds `after 217` to
|
||||
row 225. Before 288 lands, the Revert button gets the bus's refusal and shows its code, as the brief
|
||||
allows.
|
||||
|
||||
## A1. The check result I assume (to 217, Launchpad)
|
||||
|
||||
The broker stores one check result with each agent-to-human message the guard passes. The Console reads
|
||||
it on the message row that `/api/chat/threads/<id>/messages` returns, under the key `check`:
|
||||
|
||||
```json
|
||||
{
|
||||
"check": {
|
||||
"citations": [
|
||||
{ "scope": "canon", "agent": null },
|
||||
{ "scope": "supporting", "agent": "acme/scribe" },
|
||||
{ "scope": "web", "agent": null }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `check` is `null` or absent on a message the guard didn't check: a human's message, a free-text reply,
|
||||
a notice, or a message from before 217.
|
||||
- `check.citations` has one entry per entry of the body's `citations`, in the same order.
|
||||
- `scope` is one of `canon`, `playground`, `supporting` and `web`. A url cite is `web`.
|
||||
- `agent` is the `Mosaic-Agent` trailer value, `<business>/<instance>`, from
|
||||
`git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>`, or `null` when the
|
||||
file's last commit at `commit` carries none. A url cite has `agent: null`. The Console shows the
|
||||
agent-written label when `agent` is a nonempty string, and names the agent.
|
||||
|
||||
The Console trusts nothing in the body for scope or label. A cite with no matching check entry renders
|
||||
with no badge and no label. It doesn't fall back to a scope it worked out itself.
|
||||
|
||||
If 217 uses other names, I change mine to theirs before either of us pushes (sage-3591 item 1).
|
||||
|
||||
## A2. The revert verb I assume (to 288, Honker)
|
||||
|
||||
- Verb: `library.revert`. A human verb (design M4): from an agent session it refuses `human-required`,
|
||||
like `library.status` (`broker.mjs:5133`).
|
||||
- Args: `{ "project": <business project name>, "commit": <40 lowercase hex> }`. The design writes
|
||||
`library.revert {commit}`. `project` is added to match the other library verbs, and like theirs it is the
|
||||
key of `business.projects` (`mirror.mjs:31`), which is the chat project's `repo` link, not its UUID. Both are required, and no other key
|
||||
is allowed (`invalid-request`), as the other library verbs check with `keys(args, ...)`.
|
||||
- Result: `{ "project", "path", "reverted": <the commit given>, "commit": <the new commit> }`.
|
||||
- Refusal codes the Console shows by name:
|
||||
- `library-revert-not-agent`: `commit` has no `Mosaic-Agent` trailer for this business, or isn't
|
||||
reachable from the served commit.
|
||||
- `stale-base`: a later commit changed that path.
|
||||
- `human-required`, `invalid-request`, `project-not-configured`.
|
||||
- Before 288 lands, the bus refuses the verb (expected `unknown-verb`, `broker.mjs:6578`), and the Console
|
||||
shows that code.
|
||||
- HTTP status in the Console (`packages/webui/src/bus.mjs` `busStatus`): `stale-base` and
|
||||
`library-revert-not-agent` map to 409. Every other code keeps the existing mapping.
|
||||
|
||||
## A3. The status line
|
||||
|
||||
`GET /api/library/status?project=<id>` proxies `library.status {project}` through the existing
|
||||
`libraryStatus` option. `cli.mjs:110` already passes that option, but `startServer` doesn't read it.
|
||||
The status line shows:
|
||||
|
||||
- the served commit (7 characters) and its age;
|
||||
- `library-diverged` when the sync state names it;
|
||||
- the fetch error code;
|
||||
- the reply limits in force, and the keys that fell back;
|
||||
- the count of R-problems, linking to the list.
|
||||
|
||||
It reads the fields `readSyncState` returns at the base (`packages/library/src/mirror.mjs:232`). It shows
|
||||
any field 217 adds (the limits) only when present.
|
||||
|
||||
## A4. "Changed since" (ruled sage-3596)
|
||||
|
||||
Sage ruled that 217 supplies `last`. On a `thread.messages` row that carries `check`, each path cite's
|
||||
entry gets `last`, the file's last commit at the served tip, read when the message is read. When `last`
|
||||
is absent the Console marks nothing changed, and a test pins that.
|
||||
|
||||
One more field is needed, raised in morgana-0200. The cited `commit` is a library commit, not the
|
||||
file's own last commit, so a cite made at a commit that didn't touch the file would differ from `last`
|
||||
while the file is unchanged. I asked 217 to also store `file`, the file's last commit at the cited
|
||||
commit. That's `%H` from the same `git log -1 <commit> -- <path>` call that reads the trailer, so it
|
||||
costs no extra git call. The Console marks a cite changed when both `file` and `last` are 40-hex and
|
||||
differ. The cited commit is reachable from the served one (5.2), so `file` is an ancestor of `last`,
|
||||
and differing means newer. A missing or malformed `file` or `last` marks nothing.
|
||||
|
||||
The entry the Console reads, in full: `{ scope, agent, file?, last? }`.
|
||||
|
||||
## A5. `no-cited-answer` retires
|
||||
|
||||
`renderItems` and its `NOTICES` (`no-cited-answer`, `no-answer`, `turn-failed`) go. 217 sends the
|
||||
runner's fixed texts as plain bodies (design 5.2, "Runner texts"), and renderReply shows those as free
|
||||
text. Design 7.4 names no other notice than `library-write`, so nothing is lost (sage-3596 item 3).
|
||||
|
||||
## Files
|
||||
|
||||
Both deviations below were accepted in sage-3596 and are named in the manifest for Dewey. The brief owns these:
|
||||
|
||||
- `packages/library/src/render.mjs`: `renderReply` replaces `renderItems`.
|
||||
- `packages/library/src/index.mjs`: the export.
|
||||
- `packages/library/README.md`: the section.
|
||||
- `packages/library/tests/render.test.mjs`.
|
||||
|
||||
It also owns `bus.js`, its stylesheet, `serve.mjs` (only the two routes) and new webui tests. Two
|
||||
deviations, both recorded here:
|
||||
|
||||
- The chat view's message list is `packages/webui/src/public/chat.js:85` (`window.MosaicChat`), not
|
||||
`bus.js`. The reply, notice and Revert rendering goes in `chat.js` and `chat.css`. The status line
|
||||
goes in the chat thread view, so it is also in `chat.js`.
|
||||
- `packages/webui/src/console-bus.mjs` and `cli.mjs` each get one `libraryRevert` line, beside the
|
||||
existing `libraryStatus` wiring.
|
||||
|
||||
## Overlap with row 220 (C2-10a, Researcher, 3ef6da90d)
|
||||
|
||||
220 changes one line each in the webui tests discord.test.mjs, fixture.mjs, history-fixture.mjs,
|
||||
relaunch.test.mjs and return-flow.test.mjs. Its other paths are in control-board, conversation, discord
|
||||
and business. 225 doesn't touch those five files or those packages. Its webui tests are new files.
|
||||
|
||||
## Handoff to Fethry (mos-1501, Q245)
|
||||
|
||||
The row moved to Fethry (Codex, fethry/row225). Everything below is **untested**: no suite or single file
|
||||
has run on it, since a run needs Sage's CLEAR. Each changed file passes `node --check`.
|
||||
|
||||
Done in the handoff commit:
|
||||
|
||||
- `render.mjs` `renderReply(body, {check, changed, name})`; index export; README section;
|
||||
`render.test.mjs` rewritten. `renderItems` is gone from every package.
|
||||
- `serve.mjs`: `/render.mjs` static entry (owner and user file maps), `GET /api/library/status`
|
||||
(one `project` query, answer must name that project), `POST /api/library/revert` (save's boundary,
|
||||
strict `{project, commit}`, refusal shows the code, lost or unusable answer is `outcome-unknown`,
|
||||
never retried). Both are nulled for a granted user, and `userRoute` keeps them owner-only. The tester
|
||||
listener refuses `libraryRevert`.
|
||||
- `bus.mjs`: `library-revert-not-agent: 409`. That re-pins `packages/webui/src/bus.mjs` in
|
||||
`packages/bus/tests/channel-proof.test.mjs`, with a comment line, as row 254 did. It's a third deviation,
|
||||
in another package's test.
|
||||
- `console-bus.mjs` and `cli.mjs`: `libraryRevert`, beside `libraryStatus`.
|
||||
- `chat.js`: dynamic `import('/render.mjs')` (CSP is `script-src 'self'`); body text until it loads.
|
||||
Cites with a scope badge, the agent-written label, "changed since" from `file`/`last`, links only for
|
||||
https with `rel="noopener noreferrer"`. A notice gets a Revert button behind `confirm()`. The library
|
||||
status line goes under the thread heading.
|
||||
- `chat.css`: cite, badge, notice and status styles.
|
||||
- New tests: `packages/webui/tests/library-routes.test.mjs` and
|
||||
`packages/webui/tests/chat-replies-browser.test.mjs`. 220's five files are untouched.
|
||||
|
||||
Decisions made in the build:
|
||||
|
||||
- The library name is the chat project's `repo`. A project with no repo reads no status and its
|
||||
Revert does nothing.
|
||||
- These codes hide the status line: `project-not-declared`, `library-mapping-invalid`,
|
||||
`project-not-configured`, `not-configured` and `owner-only`.
|
||||
- Otherwise every code-repo thread, and every granted user, would show "unavailable".
|
||||
- Cost: a broken library mapping also hides the line. `library.status` from the CLI still shows it.
|
||||
- "Age" is the time since `lastFetch` ("fetched 5 min ago"), not the served commit's age, which the
|
||||
status doesn't carry.
|
||||
- The limits fields are assumed to be `replyLimits {requireCitation, citationsPerReply}` and
|
||||
`replyLimitsFallback [keys]`, and are shown only when present. **Not agreed with 217.** Confirm with
|
||||
Beakley and rename.
|
||||
- A notice body's shape is all the renderer checks. An agent in a business without a library could
|
||||
send a body that looks like a notice. The page would show it, but `library.revert` still refuses it.
|
||||
|
||||
Left:
|
||||
|
||||
1. Get CLEAR and run the webui and library suites, `channel-proof.test.mjs`, and the two new files whole.
|
||||
The browser test's selectors and waits have never run.
|
||||
2. Agree the limits field names with 217 (Beakley).
|
||||
3. Granted-user R6 G5 (`granted-user.test.mjs:252`) lists owner verbs by hand. Add `library.revert`
|
||||
once 288 lands.
|
||||
4. BUILD.md: the mutant list and the mos-1275 timeout plan.
|
||||
5. The manifest names the three deviations: chat.js and chat.css in place of bus.js, the console-bus and
|
||||
cli lines, and the channel-proof re-pin.
|
||||
@@ -0,0 +1,184 @@
|
||||
# Row 216 build notes (C2-05a, lookup index and queries, #1717)
|
||||
|
||||
Phase A only: code, tests and this file. No test, suite or mutant has been run. `node --check` passed on every changed `.mjs`.
|
||||
|
||||
- Branch: `researcher/row216`
|
||||
- Base: `d372102882d6858fa77af4aee453f866c886ad5c` (origin/refactor)
|
||||
- Design: `agents/gosalyn/work/case2/DESIGN.md` on origin/gosalyn/case2-design, section 3 (table at lines 625-631, bullets 633-641), 1.2-1.4, B12, D2
|
||||
- Brief: `docs/plans/2026-10-20_case2-rows.md`, C2-05a at lines 320-357
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Status |
|
||||
|---|---|
|
||||
| `packages/library/src/index-build.mjs` | new |
|
||||
| `packages/library/src/query.mjs` | new |
|
||||
| `packages/library/src/index.mjs` | edit: 2 export lines added |
|
||||
| `packages/library/tests/query-fixture.mjs` | new, test helper |
|
||||
| `packages/library/tests/query.test.mjs` | new |
|
||||
| `packages/library/tests/index-build.test.mjs` | new |
|
||||
| `agents/researcher/work/queue-216/BUILD.md` | new (this file) |
|
||||
|
||||
No existing test or source file changed beyond `index.mjs`.
|
||||
|
||||
## Design rows and where each is tested
|
||||
|
||||
| Design row | Code | Tests (query.test.mjs unless noted) |
|
||||
|---|---|---|
|
||||
| `library_find` | `find` | find is bound to its universe; every result carries its scope; long list pages; large items page under the cap |
|
||||
| `library_get` | `get` | hybrid gets a source file; get lists visible links both ways; get pages its lines |
|
||||
| `library_search` | `search` | search returns scoped line hits; item that cannot fit alone is refused |
|
||||
| `library_manuscript` | `manuscript` | one chapter at most 400 lines; refused without chapter; hybrid reads a source chapter |
|
||||
| `library_universe` | `universeInfo` | universe info reports a hybrid's sources, constraints, notes; unknown universe refused |
|
||||
| Hybrid semantics (1.4) | `sees` | a hybrid sees its sources canon minus exclusions, no playground |
|
||||
| Unmapped never appears | index build + `sees` | unmapped files never appear (query.test); index holds mapped files only (index-build.test) |
|
||||
| Per-commit cache (3) | `indexFor` | same commit, path, mapping return one index; different path, mapping or commit get their own; eight-entry bound; failed read not kept; unchecked mapping refused (index-build.test) |
|
||||
| Rebuild time (3, C2-05a gate) | `indexFor` | 3,000-file rebuild, time reported with `t.diagnostic`, no threshold asserted |
|
||||
| REPLY_CAP parity | `QUERY_REPLY_CAP` | equals `REPLY_CAP` from harness tools.mjs |
|
||||
|
||||
## Choices where the design table is silent or differs
|
||||
|
||||
These are departures or gaps. Each is built as stated, so Sage can rule before Phase B. A ruling that changes one of them is a small edit.
|
||||
|
||||
1. **Paging arguments (gap a).** The table has no offset. `find` and `search` take an optional `offset` (a position in the filtered list, default 0). A reply carries `next`: the position of the next item, or `null`.
|
||||
2. **`library_get` has no universe (gap b).** Built with a required `universe`. Without it the reply could not be isolated. A missing file and a file the universe cannot see both return `null`, so a caller cannot tell them apart. `get` also takes an optional `from` line.
|
||||
3. **`get` over the cap (gap c).** Lines page by `from` with `next`. `links_out` and `links_in` come back whole on every page. If the frontmatter and links alone exceed the cap, the call is refused with `query-item-too-large`. A hub note with many backlinks may be refused. This needs a ruling: link paging, or accept the refusal.
|
||||
4. **`manuscript` without a chapter (gap d).** Refused with `query-chapter-required`. The design names no list form. A chapter name that matches two visible files is refused with `query-chapter-ambiguous`.
|
||||
5. **`search` excerpt (gap e).** The excerpt is the whole line, not a truncation. A line that alone exceeds the cap is refused. Truncating it would cut an item.
|
||||
6. **`exclude` form (gap f).** `checkMapping` accepts only a folder path for `exclude` (`folder()`), so an exclusion is a path prefix. There is no type or source form to build. Confirm.
|
||||
7. **Pin constraints.** `pin` does not hide any file. It is reported by `universeInfo` only. Confirm.
|
||||
8. **Playground through `get`.** `scope` applies to lists. `get` by an explicit path returns a playground file of the caller's own universe. Confirm.
|
||||
9. **Links.** A link into a file the universe cannot see is dropped from `links_out` and `links_in`, in line with N1. Links from a visible file that point into another universe are kept in the index (the reader keeps them under R6), and the query layer drops them for universes that cannot see them.
|
||||
10. **`find` matching.** Case-insensitive substring over the front matter `name` and the file name. An empty `query` matches every file in scope.
|
||||
11. **Cache key.** `repo`, `commit` and a sha256 of the canonical mapping. Keying by commit alone would return the wrong scopes after a mapping change. The cache holds eight entries, least recently used out. Failed reads are not kept.
|
||||
12. **`universeInfo`.** Reads only the checked mapping and the served commit. It builds no index.
|
||||
13. **Reply commit.** Every reply carries the index commit, which is the served commit passed in.
|
||||
|
||||
## Provisional refusal codes (pending Sage approval)
|
||||
|
||||
The brief says new refusal codes need Sage's approval first. These are provisional and must not be treated as settled:
|
||||
|
||||
- `query-argument-invalid`
|
||||
- `query-universe-unknown`
|
||||
- `query-chapter-required`
|
||||
- `query-chapter-ambiguous`
|
||||
- `query-item-too-large`
|
||||
|
||||
Reused, existing codes: `library-mapping-invalid`, plus the reader's `library-git`, `library-commit` through `indexFor`.
|
||||
|
||||
## Grep results (grep only, not a test run)
|
||||
|
||||
- Test references to the changed paths: `packages/library/tests/*.mjs` import `../src/index.mjs` and `../src/mapping.mjs`. `packages/bus/tests/library.test.mjs` and `library-sync.test.mjs` import the library index as a namespace. Neither enumerates its exports, so the two new export lines break no assertion.
|
||||
- `packages/cli/tests/stack-copy.test.mjs:52` checks that `packages/library/src/index.mjs` exists, and it still does.
|
||||
- 64-hex pins: none in `packages/library/tests`, none on `index.mjs`, `query.mjs` or `index-build.mjs`.
|
||||
- `scripts/test-*.sh`: no reference to the new modules.
|
||||
|
||||
## Flag for Sage
|
||||
|
||||
- `packages/library/tests/helpers.mjs` `repo()` builds its fixtures under `tmpdir()`, which is `/tmp`. The launcher rules forbid that. The new tests do not use it; they use `tests/query-fixture.mjs`, which builds under `/mnt/storage/scratch/tmp`. The existing helper should be fixed in a separate row, not here.
|
||||
|
||||
## Phase B commands (not run)
|
||||
|
||||
Run only after Sage sends "slot clear". Set the scratch TMPDIR first, per the launcher rule.
|
||||
|
||||
```
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-aa3cc98787bc5152c/packages/library
|
||||
export TMPDIR=/mnt/storage/scratch/tmp
|
||||
node --test tests/query.test.mjs tests/index-build.test.mjs
|
||||
node --test tests/*.test.mjs
|
||||
```
|
||||
|
||||
Also run `packages/bus/tests/library.test.mjs` and `library-sync.test.mjs`, since they import the index.
|
||||
|
||||
## Status
|
||||
|
||||
Not yet verified. Nothing has been tested and nothing is committed. The working tree holds the Phase A changes. AGENTS.md invariant 8 allows a commit only after the applicable suites are green, so the commit waits for Phase B. The dispatch asked for a local commit in Phase A; Sage to confirm that the commit waits for Phase B.
|
||||
|
||||
## Round 2 (2026-10-22, sage-2708, sage-2716): rulings applied, rebased onto aca16404c
|
||||
|
||||
Added after the text above; nothing above is edited. "Nothing is committed", "Provisional refusal codes",
|
||||
`query-chapter-required` and the tmpdir flag above were true when written and are superseded here.
|
||||
|
||||
- Branch `researcher/row216-r2`, one local commit on origin/refactor `aca16404c`. Row 268 (C2-01c) is in
|
||||
that base. Row 253 has not landed; the work rebases onto 253's tip before any gate. Not pushed, not run.
|
||||
- Rulings: `agents/sage/work/queue-216/RULINGS.md` at da9555ca2, rulings 1-13 and the tmpdir flag.
|
||||
- Drift check, d37210288..aca16404c against my paths: the library files that changed are `mapping.mjs`,
|
||||
`scope.mjs`, `README.md` and the mapping and scope tests. None is a file I add or edit. `query.mjs` reads
|
||||
the checked mapping through them, so the whole library package runs in phase B.
|
||||
|
||||
### What changed from phase A
|
||||
|
||||
| Ruling | Change in `query.mjs` | Test |
|
||||
|---|---|---|
|
||||
| 3 `get` over the cap | `links_out` and `links_in` page together with `links_from`; every reply carries `links_next`, `next` and the front matter. A page with lines left carries at least one line. Links take at most half the cap while lines remain. `query-item-too-large` is left for front matter alone or one line alone (or one link alone) | hub note with 900 backlinks pages and adds up; front matter alone and a line alone refused; bad `links_from` |
|
||||
| 4 `manuscript` no chapter | `chapter` optional: the first visible chapter in path order. Every reply carries `next_chapter`. `query-chapter-required` removed. Ambiguity still refuses | first chapter and `next_chapter`; ambiguous name refused |
|
||||
| 5 `search` long line | hit comes back `{path, line, excerpt: null, excerpt_too_long: true, scope}`, other hits still return | marked hit and unmarked hits in one reply |
|
||||
| 6 `exclude` | path-prefix reading only. The test names `EXCLUSION` and `EXCLUDED_PATHS` in `query-fixture.mjs`, so C2-15 (row 267) swaps two constants | exclusion hides from the hybrid and not from the source |
|
||||
| 8 hybrid `get` | unchanged code; test added | hybrid gets a source's playground file as `null`; the source gets it with `scope: "playground"` |
|
||||
| PRD 1.14 (row 268) | `universeInfo` reports `kind` from the mapping (`draft` included) | four tests below |
|
||||
|
||||
PRD 1.14 tests, one each: a lookup naming a `book` leaves `book: null` files out (find and search);
|
||||
`library_manuscript` never reads a `book: null` file; a draft universe has no canon, so its lookups return
|
||||
files only with `scope: "playground"`; `library_universe` on a draft returns kind, name and prefixes.
|
||||
The levels fixture is `WITH_LEVELS` in `query.test.mjs`: harbor gets `Notes/Harbor/` as a universe-level
|
||||
playground prefix, and `idea` is a draft over `Drafts/New World/`.
|
||||
|
||||
`query-fixture.mjs` builds under `tmpdir()`, as `helpers.mjs` does (ruling "tmpdir flag"). The earlier
|
||||
flag about `helpers.mjs` building under `/tmp` is closed by that ruling: the gate sets `TMPDIR`.
|
||||
|
||||
Refusal codes now: `query-argument-invalid`, `query-universe-unknown`, `query-chapter-ambiguous`,
|
||||
`query-item-too-large`. All four approved in the rulings.
|
||||
|
||||
### Grep results (grep only, not a test run)
|
||||
|
||||
- `git grep` over `packages/*/tests/*` and `scripts/*` for `query.mjs` and `index-build`: no hit outside
|
||||
`packages/library/tests/query*.test.mjs` and `index-build.test.mjs`.
|
||||
- 64-hex pins naming a library path: none. Positive control: the same pathspec finds the sha256 lines in
|
||||
`packages/cli/tests/case1-hostcheck.test.mjs` and the `packages/library/src/index.mjs` existence check at
|
||||
`packages/cli/tests/stack-copy.test.mjs:52`.
|
||||
- Tests that read `packages/library/src` as a directory: `packages/cli/tests/stack-release-fixture.mjs:309`
|
||||
copies `packages/library/src` whole into a release fixture. It carries no file list, so two new files break
|
||||
no assertion. `stack-release.test.mjs` is not in the gate unless Sage wants it.
|
||||
- Tests that import the library index as a namespace: `packages/bus/tests/library.test.mjs` and
|
||||
`library-sync.test.mjs`. Neither enumerates exports, so the two export lines add no break. Both run
|
||||
whole in phase B.
|
||||
|
||||
### Run limits (fixed before any run)
|
||||
|
||||
Every node file runs as its own `setsid -w timeout -k 10 LIMIT node --test FILE`; nothing runs unbounded.
|
||||
|
||||
- Node test files: LIMIT 900 s. The library test files have no recorded time. 900 s is above the longest
|
||||
recorded whole-package run I know of (cli, 591 s, row 281 BUILD.md "Runs" step 3), so it holds the 3x rule
|
||||
for any single file. The first run records each file's time, and a file past 300 s gets a limit re-derived
|
||||
from it before the next run. `query.test.mjs` builds a 900-file repo and a 3,000-file index rebuild is in
|
||||
`index-build.test.mjs`; both are the files most likely to need it.
|
||||
- No shell script is in this row's gate, so no script limit applies.
|
||||
|
||||
Any timeout goes to Sage, then Mos, unplaced (mos-1275).
|
||||
|
||||
### Phase B (not run; Sage's CLEAR first, then a rebase onto 253's tip)
|
||||
|
||||
```
|
||||
export TMPDIR=/mnt/storage/scratch/tmp
|
||||
cd <worktree>/packages/library
|
||||
for f in tests/*.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
|
||||
cd ../bus
|
||||
for f in tests/library.test.mjs tests/library-sync.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
|
||||
cd ../cli
|
||||
setsid -w timeout -k 10 900 node --test tests/stack-copy.test.mjs
|
||||
```
|
||||
|
||||
Mutants I propose (my list, not read off the brief; each is meant to be killed by the named test, and none
|
||||
has been thrown; the kill claims are unverified until CLEAR):
|
||||
|
||||
- M-a: `sees()` returns true for a file of another universe: killed by "find is bound to its universe".
|
||||
- M-b: the playground filter dropped: killed by "playground appears only when asked for".
|
||||
- M-c: the hybrid reads a source's playground: killed by "a hybrid gets a source playground file as null".
|
||||
- M-d: `links_next` always null: killed by the hub paging test.
|
||||
- M-e: the book filter lets `book: null` through: killed by the PRD 1.14 book test.
|
||||
|
||||
### Status
|
||||
|
||||
Built, not run. `node --check` has not been run on this round's files (no node before the CLEAR); the
|
||||
phase A pass above covered the earlier text only. The commit is local and waits for no gate under Sage's
|
||||
sage-2716 instruction, which allows git and docs work only; push and claim wait for Sage's go.
|
||||
@@ -1593,5 +1593,3 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-23T17:48:43Z | Scrooge (629b6cd3) | Row 252 Console inbox Approve and Deny (#1753) | Rebased onto 306 landing as 580037105 (range-diff all =), slot run bus 907/0 webui 410/0 cli 943/0, in-review revs 3608/3609, Webby r1 APPROVE, J1-J19 specs to Sage; Sage landed 45ea3fa0d (done rev 3616). Part 1 mutant results were never in BUILD.md; they went to Sage by T3 (scrooge-0252-mutant-specs).
|
||||
2026-10-23 | Sage (1ef1e4f8) | Rows 308 and 252 landings, 571 Connections design amendment, Vikunja 597 | 308 (PRD 1.17, #1815) landed 30e6bb680 and 252 (#1753) landed 45ea3fa0d (replay of 580037105 onto 30e6bb680, range-diff all =), each after a full gate on the landed tree (252 also J1-J19, J11c and J18 equivalent); Gyro's 571 amendment (Fenton APPROVE) landed e8bb13df8; Vikunja 570, 435 and 597 done, canonical webui esbuild 0.28.2; queue revs 3589-3616 committed 59f013247
|
||||
2026-10-23 | Sage (1ef1e4f8) | Vikunja 571 W build rows; rows 13, 42, 257 and 289 check | Filed the design's ten W rows as queue rows 309-318 (revs 3617-3626, owners set to unassigned 3627-3636, queue commit 293b42fee pushed); Vikunja 571 done (comment 994). Row 13's candidate fd72d2689 is on refactor, gate is Monday's row 7 ledger run; 42 paused under mos-1184; 257 post-v1; 289 waits on 214 and 288. Slot released 17:56Z.
|
||||
2026-10-23 | Fethry (Codex gpt-6.1-sol, 14f80e3c) | Row 225 C2-10b (#1726), Q245 handoff | Recovered Morgana's 9a131e956 by fast-forward on fethry/row225 in the assigned T3 worktree. Static review and contract confirmation in progress; all Node/tests require Sage CLEAR. No acceptance claim.
|
||||
2026-10-23 | Fethry (14f80e3c) | Row225 T1 lane A sage-3641/3647 (#1726) | Continued Morgana handoff, fixed nested status limits and wrong-project revert receipt after RED, kept invalid-mapping diagnosis visible with RED/GREEN. Renderer9/0, routes13/0, browser9/0, full library81/0, channel-proof2/0; isolated mutants14/14, restored SHA256 checks and clean worktree sources. Old-test approval mos1510 preserved80/66 and3/1. Slot returned with no processes around19:29Z. Dewey source-review packet prepared; full webui/build and integration gate remain pending.
|
||||
@@ -96,7 +96,16 @@ export const TASK_PAGE_MAX = 200;
|
||||
// request allowed past the ordinary 64 KiB envelope (server.mjs and client.mjs), up to its content cap.
|
||||
export const PROJECT_FILE_VERBS = Object.freeze(['project.files', 'project.file', 'project.file.save']);
|
||||
// C2-03a: library.sync is the human's fetch, and with `accept` the one way past library-diverged.
|
||||
export const LIBRARY_VERBS = Object.freeze(['library.file', 'library.status', 'library.sync']);
|
||||
// Row 224: pure agent lookups are separate from the three human-only verbs. Listing them
|
||||
// here also makes server.mjs await their host effects instead of serializing a Promise as {}.
|
||||
const LIBRARY_LOOKUPS = Object.freeze({
|
||||
'library.find': { method: 'find', fields: ['universe', 'query', 'type', 'book', 'scope', 'offset'], required: ['universe', 'query'] },
|
||||
'library.get': { method: 'get', fields: ['universe', 'path', 'scope', 'from', 'links_from'], required: ['universe', 'path'] },
|
||||
'library.search': { method: 'search', fields: ['universe', 'text', 'book', 'scope', 'offset'], required: ['universe', 'text'] },
|
||||
'library.manuscript': { method: 'manuscript', fields: ['universe', 'book', 'scope', 'chapter', 'from', 'to'], required: ['universe', 'book'] },
|
||||
'library.universe': { method: 'universeInfo', fields: ['universe'], required: ['universe'] },
|
||||
});
|
||||
export const LIBRARY_VERBS = Object.freeze(['library.file', 'library.status', 'library.sync', ...Object.keys(LIBRARY_LOOKUPS)]);
|
||||
export const PROJECT_SAVE_VERB = 'project.file.save';
|
||||
export const PROJECT_SAVE_REQUEST_MAX = 1024 * 1024 + 64 * 1024;
|
||||
export const WEB_VERBS =Object.freeze(['web.search', 'web.fetch']);
|
||||
@@ -5125,10 +5134,42 @@ export class Broker {
|
||||
throw this.#refused(s, ProjectFailure && e instanceof ProjectFailure ? new BusError(e.code) : e, request);
|
||||
}
|
||||
}
|
||||
// Row 224 C2-05b: the agent never supplies a host repo, commit or business. Its current
|
||||
// holder and business entry are rechecked before/after awaits; runtime owns Git and queries.
|
||||
async #requestLibraryLookup(s, { verb, args }) {
|
||||
try {
|
||||
this.#agent(s);
|
||||
const business = this.#business(s.business);
|
||||
const projects = Object.entries(business.projects ?? {}).filter(([, entry]) => entry.kind === 'library');
|
||||
if (!projects.length || typeof this.#library?.lookup !== 'function') fail('library-not-configured');
|
||||
const spec = LIBRARY_LOOKUPS[verb];
|
||||
keys(args, ['project', ...spec.fields], spec.required);
|
||||
if (!text(args.universe, 160)) fail('invalid-request');
|
||||
if (args.project === undefined && projects.length !== 1) fail('invalid-request');
|
||||
const project = args.project ?? projects[0][0];
|
||||
if (!text(project, 160) || !projects.some(([name]) => name === project)) fail('library-not-configured');
|
||||
const revision = JSON.stringify(business.projects[project]);
|
||||
const authorize = () => {
|
||||
this.#agent(s);
|
||||
this.#processingGate(s, verb);
|
||||
if (JSON.stringify(this.#business(s.business).projects?.[project]) !== revision) fail('library-not-configured');
|
||||
};
|
||||
const { project: ignored, ...query } = args;
|
||||
authorize();
|
||||
const result = await this.#library.lookup({ business, project }, { method: spec.method, args: query, authorize });
|
||||
authorize();
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const Failure = this.#library?.LibraryError;
|
||||
throw this.#refused(s, Failure && error instanceof Failure ? new BusError(error.code) : error, { verb, args });
|
||||
}
|
||||
}
|
||||
// C2-04 M4: only a verified human reads library text or status. Host Git
|
||||
// functions stay injected, preserving this module's sandbox import closure.
|
||||
async #requestLibrary(s, request) {
|
||||
const { verb, args } = request;
|
||||
if (Object.hasOwn(LIBRARY_LOOKUPS, verb)) return this.#requestLibraryLookup(s, request);
|
||||
try {
|
||||
this.#human(s);
|
||||
const fields = verb === 'library.file' ? ['project', 'path'] : verb === 'library.sync' ? ['project', 'accept'] : ['project'];
|
||||
|
||||
@@ -87,6 +87,38 @@ export async function harnessLevels({ log = (e) => process.stderr.write(`plugins
|
||||
}
|
||||
return (name) => levels.get(name) ?? null;
|
||||
}
|
||||
// Row 224 C2-05b: a host-only adapter. A failed fetch keeps serving immutable old
|
||||
// objects, as C2-03a requires; no caller may select a repo, commit or sync --accept.
|
||||
export function createLibraryLookup({ dataRoot, sync, now = () => Date.now() }) {
|
||||
const methods = { find: libraryModule.find, get: libraryModule.get, search: libraryModule.search,
|
||||
manuscript: libraryModule.manuscript, universeInfo: libraryModule.universeInfo };
|
||||
return async (context, { method, args, authorize }) => {
|
||||
if (!Object.hasOwn(methods, method)) throw new libraryModule.LibraryError('query-argument-invalid', 'unknown lookup');
|
||||
authorize();
|
||||
const source = { ...context, dataRoot };
|
||||
let state = await libraryModule.libraryStatus(source);
|
||||
authorize();
|
||||
// sage-2819 / dewey-0852: failed attempts also pace inline retries. Otherwise an
|
||||
// outage makes every lookup refetch while lastFetch remains old or null.
|
||||
const recentAttempt = Math.max(state.lastFetch === null ? -Infinity : Date.parse(state.lastFetch),
|
||||
state.lastError === null ? -Infinity : Date.parse(state.lastError.at));
|
||||
const age = now() - recentAttempt;
|
||||
if (age > libraryModule.SYNC_INTERVAL_MS || age < 0) {
|
||||
try { await sync(source); }
|
||||
catch (error) {
|
||||
// A timer may already be fetching this mirror. Its current served state is still safe.
|
||||
if (!(error instanceof libraryModule.LibraryError) || error.code !== 'library-sync-busy') throw error;
|
||||
}
|
||||
authorize();
|
||||
state = await libraryModule.libraryStatus(source);
|
||||
}
|
||||
if (!state.served) throw new libraryModule.LibraryError('library-commit', 'library has never been synced');
|
||||
authorize();
|
||||
return methods[method]({ repo: libraryModule.mirrorPath(source), commit: state.served,
|
||||
mapping: libraryModule.checkMapping(context.business.projects[context.project]) }, args);
|
||||
};
|
||||
}
|
||||
|
||||
export async function startBroker({
|
||||
dataRoot,
|
||||
businesses,
|
||||
@@ -301,6 +333,8 @@ export async function startBroker({
|
||||
store,
|
||||
businesses,
|
||||
library: {
|
||||
lookup: createLibraryLookup({ dataRoot, sync: context =>
|
||||
processing.work(() => libraryModule.sync(context, syncOptions(context.business))) }),
|
||||
readFile: context => libraryModule.readLibraryFile({ ...context, dataRoot }),
|
||||
status: context => libraryModule.libraryStatus({ ...context, dataRoot }),
|
||||
sync: (context, { accept }) => processing.work(() => libraryModule.sync({ ...context, dataRoot }, { ...syncOptions(context.business), accept })),
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
// Row 252 (#1753, Webby N5) re-pins human.mjs, whose readProcess also keeps the five
|
||||
// MOSAIC_BUS_CONSOLE_* keys for the Console wrapper's proof, and webui bus.mjs for consoleCall,
|
||||
// which shares humanCall's spawn as transport(). The human proof and its spawn are unchanged.
|
||||
// Row 225 (C2-10b) re-pins webui bus.mjs for one STATUS entry, library-revert-not-agent: 409; no proof
|
||||
// or transport change.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -31,7 +29,7 @@ const baselines = {
|
||||
// Row 194 (#1692, brief item 7, sage-1610 a): organization-tracker-required 400 and
|
||||
// organization-tracker-bot-taken 409; no proof or transport change.
|
||||
// Row 301 (#1808, mos-1449): the user transport moved from user-bus.mjs into bus.mjs; humanCall unchanged.
|
||||
"packages/webui/src/bus.mjs": "9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658",
|
||||
"packages/webui/src/bus.mjs": "604092cb706b152d458f299ff1071f6cd488774629012f96b672f7e9a16c0b2c",
|
||||
};
|
||||
const repoFile = path => new URL("../../../" + path, import.meta.url);
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
// Row 224: exercise broker authorization, real C2-05a queries and host served snapshots.
|
||||
// All bodies and remotes are made up. External sync is replaced only at its trusted host seam.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { Broker } from '../src/broker.mjs';
|
||||
import { createLibraryLookup, startBroker } from '../src/runtime.mjs';
|
||||
import { serve } from '../src/server.mjs';
|
||||
import { Client } from '../src/client.mjs';
|
||||
import * as library from '../../library/src/index.mjs';
|
||||
import { scratchRepo, note } from '../../library/tests/query-fixture.mjs';
|
||||
import { mappingEntry, git } from '../../library/tests/helpers.mjs';
|
||||
|
||||
const NOW = Date.parse('2026-10-22T16:00:00.000Z');
|
||||
const VERBS = ['library.find', 'library.get', 'library.search', 'library.manuscript', 'library.universe'];
|
||||
const GET = { universe: 'harbor', path: 'Book One/Characters/Tomas.md' };
|
||||
const ARGS = [
|
||||
{ universe: 'harbor', query: 'Tomas' }, GET,
|
||||
{ universe: 'harbor', text: 'ferry' }, { universe: 'harbor', book: 'Book One' }, { universe: 'harbor' },
|
||||
];
|
||||
const TEXTS = {
|
||||
'Book One/Characters/Tomas.md': note('character', 'Tomas', 'He runs the ferry. [[Elsewhere]]'),
|
||||
'Book One/Chapters/01.md': '# A ferry\nOne line.\n',
|
||||
'Book One/Playground/What If.md': note('character', 'Maybe Tomas', 'Unwritten ferry.'),
|
||||
'Second World/Characters/Elsewhere.md': note('character', 'Elsewhere', 'Other ferry.'),
|
||||
};
|
||||
|
||||
async function world(t, { projects, adapter, claimed = true, now = () => NOW, files = TEXTS } = {}) {
|
||||
const f = scratchRepo(t, files), dataRoot = join(f.root, 'data');
|
||||
mkdirSync(dataRoot);
|
||||
const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true },
|
||||
projects: projects ?? { books: mappingEntry() }, arbiters: { technical: 'coder', delivery: 'coder' },
|
||||
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
|
||||
const context = { business, project: 'books', dataRoot };
|
||||
let mirror = null, state = null, fetches = 0;
|
||||
if (business.projects.books?.kind === 'library') {
|
||||
mirror = await library.ensureMirror(context);
|
||||
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', mirror, 'HEAD:refs/heads/served');
|
||||
state = { version: 1, branch: 'main', served: f.commit, lastFetch: new Date(NOW).toISOString(),
|
||||
lastError: null, diverged: null, problems: [] };
|
||||
writeFileSync(join(mirror, library.SYNC_STATE_FILE), JSON.stringify(state), { mode: 0o600 });
|
||||
}
|
||||
const setState = patch => {
|
||||
state = { ...state, ...patch };
|
||||
writeFileSync(join(mirror, library.SYNC_STATE_FILE), JSON.stringify(state), { mode: 0o600 });
|
||||
};
|
||||
const lookup = createLibraryLookup({ dataRoot, now, sync: async () => {
|
||||
fetches++;
|
||||
setState({ lastFetch: new Date(now()).toISOString() });
|
||||
return { state, code: null };
|
||||
} });
|
||||
const store = new Store(dataRoot);
|
||||
t.after(() => store.close());
|
||||
const broker = new Broker({ store, businesses: { acme: business },
|
||||
library: adapter ?? { lookup, LibraryError: library.LibraryError } });
|
||||
const agent = broker.bindLaunch({ business: 'acme', role: 'coder', run: 'r224', harness: 'pi' });
|
||||
if (claimed) broker.request(agent, { verb: 'role.claim' });
|
||||
const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const reader = broker.bindReader({ business: 'acme' }), tester = broker.bindTester({ business: 'acme' });
|
||||
const call = (verb, args = {}, cap = agent) => Promise.resolve().then(() => broker.request(cap, { verb, args }));
|
||||
return { ...f, dataRoot, business, context, mirror, store, broker, agent, human, reader, tester,
|
||||
call, lookup, setState, fetches: () => fetches };
|
||||
}
|
||||
|
||||
test('all five agent verbs read the configured served commit through real queries', async t => {
|
||||
const f = await world(t);
|
||||
const [find, get, search, manuscript, universe] = await Promise.all(VERBS.map((verb, i) => f.call(verb, ARGS[i])));
|
||||
for (const result of [find, get, search, manuscript, universe]) assert.equal(result.commit, f.commit);
|
||||
assert.deepEqual(find.results.map(x => x.path), ['Book One/Characters/Tomas.md']);
|
||||
assert.match(JSON.stringify(get.lines), /He runs the ferry/);
|
||||
assert.deepEqual(get.links_out, []); // The only target is in another universe.
|
||||
assert.ok(search.results.length > 0);
|
||||
assert.ok(search.results.every(x => !x.path.startsWith('Second World/') && x.scope === 'canon'));
|
||||
assert.equal(manuscript.path, 'Book One/Chapters/01.md');
|
||||
assert.deepEqual(universe.books, ['Book One']);
|
||||
assert.equal(f.fetches(), 0);
|
||||
const playground = await f.call('library.find', { universe: 'harbor', query: '', scope: 'playground' });
|
||||
assert.deepEqual(playground.results.map(x => x.path), ['Book One/Playground/What If.md']);
|
||||
assert.equal(await f.call('library.get', { universe: 'harbor', path: 'Second World/Characters/Elsewhere.md' }), null);
|
||||
});
|
||||
|
||||
test('each verb refuses missing universe and unknown keys before host work', async t => {
|
||||
let calls = 0;
|
||||
const f = await world(t, { adapter: { lookup: () => { calls++; } } });
|
||||
for (const [i, verb] of VERBS.entries()) {
|
||||
const { universe, ...missing } = ARGS[i];
|
||||
await assert.rejects(f.call(verb, missing), { code: 'invalid-request' });
|
||||
for (const extra of [{ repo: '/elsewhere' }, { commit: 'b'.repeat(40) }, { business: 'other' }, { accept: 'b'.repeat(40) }])
|
||||
await assert.rejects(f.call(verb, { ...ARGS[i], ...extra }), { code: 'invalid-request' });
|
||||
await assert.rejects(f.call(verb, { ...ARGS[i], universe: '' }), { code: 'invalid-request' });
|
||||
}
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test('no library, missing host adapter and undeclared project refuse library-not-configured', async t => {
|
||||
const noLibrary = await world(t, { projects: {} });
|
||||
const noAdapter = await world(t, { adapter: {} });
|
||||
const f = await world(t);
|
||||
for (const [i, verb] of VERBS.entries()) {
|
||||
await assert.rejects(noLibrary.call(verb, ARGS[i]), { code: 'library-not-configured' });
|
||||
await assert.rejects(noAdapter.call(verb, ARGS[i]), { code: 'library-not-configured' });
|
||||
await assert.rejects(f.call(verb, { ...ARGS[i], project: 'other' }), { code: 'library-not-configured' });
|
||||
}
|
||||
f.business.projects.second = mappingEntry();
|
||||
await assert.rejects(f.call('library.get', GET), { code: 'invalid-request' });
|
||||
assert.equal((await f.call('library.get', { ...GET, project: 'books' })).commit, f.commit);
|
||||
});
|
||||
|
||||
test('reads need the current holder on each call; human/tester/reader are not agents', async t => {
|
||||
const f = await world(t, { claimed: false });
|
||||
for (const [i, verb] of VERBS.entries()) {
|
||||
await assert.rejects(f.call(verb, ARGS[i]), { code: 'not-holder' });
|
||||
await assert.rejects(f.call(verb, ARGS[i], f.human), { code: 'agent-required' });
|
||||
await assert.rejects(f.call(verb, ARGS[i], f.tester), { code: 'tester-refused' });
|
||||
await assert.rejects(f.call(verb, ARGS[i], f.reader), { code: 'read-only' });
|
||||
}
|
||||
f.broker.request(f.agent, { verb: 'role.claim' });
|
||||
assert.equal((await f.call('library.get', GET)).commit, f.commit);
|
||||
f.broker.request(f.agent, { verb: 'role.release' });
|
||||
await assert.rejects(f.call('library.get', GET), { code: 'not-holder' });
|
||||
});
|
||||
|
||||
test('releasing a claim during an async lookup fences the response', { timeout: 30_000 }, async t => {
|
||||
let release, entered;
|
||||
const started = new Promise(resolve => { entered = resolve; });
|
||||
const f = await world(t, { adapter: { lookup: async () => {
|
||||
entered(); await new Promise(resolve => { release = resolve; }); return { lines: [[1, 'not returned']] };
|
||||
} } });
|
||||
const pending = f.call('library.get', GET);
|
||||
await started;
|
||||
f.broker.request(f.agent, { verb: 'role.release' });
|
||||
release();
|
||||
await assert.rejects(pending, { code: 'not-holder' });
|
||||
});
|
||||
|
||||
test('a business library removed during an async read cannot leak its result', { timeout: 30_000 }, async t => {
|
||||
let release, entered;
|
||||
const started = new Promise(resolve => { entered = resolve; });
|
||||
const f = await world(t, { adapter: { lookup: async () => {
|
||||
entered(); await new Promise(resolve => { release = resolve; }); return { lines: [[1, 'not returned']] };
|
||||
} } });
|
||||
const pending = f.call('library.get', GET);
|
||||
await started;
|
||||
delete f.business.projects.books;
|
||||
release();
|
||||
await assert.rejects(pending, { code: 'library-not-configured' });
|
||||
});
|
||||
|
||||
test('freshness boundary, unknown last fetch and failed fetch still use immutable served state', async t => {
|
||||
const f = await world(t);
|
||||
f.setState({ lastFetch: new Date(NOW - 60_000).toISOString() });
|
||||
await f.call('library.get', GET);
|
||||
assert.equal(f.fetches(), 0);
|
||||
f.setState({ lastFetch: new Date(NOW - 60_001).toISOString() });
|
||||
await f.call('library.get', GET);
|
||||
assert.equal(f.fetches(), 1);
|
||||
f.setState({ lastFetch: null });
|
||||
await f.call('library.universe', { universe: 'harbor' });
|
||||
assert.equal(f.fetches(), 2);
|
||||
let attempted = 0, clock = NOW;
|
||||
const failing = createLibraryLookup({ dataRoot: f.dataRoot, now: () => clock, sync: async () => {
|
||||
attempted++;
|
||||
f.setState({ lastError: { code: 'library-fetch-failed', at: new Date(clock).toISOString() } });
|
||||
return { code: 'library-fetch-failed' };
|
||||
} });
|
||||
f.setState({ lastFetch: null });
|
||||
const result = await failing(f.context, { method: 'get', args: GET, authorize() {} });
|
||||
assert.equal(attempted, 1);
|
||||
assert.equal(result.commit, f.commit);
|
||||
for (const elapsed of [1, 59_999, 60_000]) {
|
||||
clock = NOW + elapsed;
|
||||
const retry = await failing(f.context, { method: 'get', args: GET, authorize() {} });
|
||||
assert.equal(retry.commit, f.commit, 'serve the old pin during the failure cooldown');
|
||||
assert.equal(attempted, 1, 'recent lastError prevents another sync even though lastFetch is null');
|
||||
}
|
||||
clock = NOW + 60_001;
|
||||
assert.equal((await failing(f.context, { method: 'get', args: GET, authorize() {} })).commit, f.commit);
|
||||
assert.equal(attempted, 2, 'retry only after the failed-attempt interval expires');
|
||||
});
|
||||
|
||||
test('an unreadable sync state fails closed; a busy sync keeps serving the existing pin', async t => {
|
||||
const f = await world(t);
|
||||
const lookup = createLibraryLookup({ dataRoot: f.dataRoot, now: () => NOW, sync: async () => {
|
||||
throw new library.LibraryError('library-sync-busy', 'another fetch');
|
||||
} });
|
||||
f.setState({ lastFetch: null });
|
||||
assert.equal((await lookup(f.context, { method: 'get', args: GET, authorize() {} })).commit, f.commit);
|
||||
writeFileSync(join(f.mirror, library.SYNC_STATE_FILE), '{bad json', { mode: 0o600 });
|
||||
await assert.rejects(f.call('library.get', GET), { code: 'library-state-invalid' });
|
||||
});
|
||||
|
||||
test('a stale lookup sees the commit published by its sync, not the prior served commit', async t => {
|
||||
const f = await world(t);
|
||||
writeFileSync(join(f.repo, GET.path), note('character', 'Tomas', 'Changed ferry.'));
|
||||
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'second fixture');
|
||||
const next = git(f.repo, 'rev-parse', 'HEAD');
|
||||
const lookup = createLibraryLookup({ dataRoot: f.dataRoot, now: () => NOW, sync: async () => {
|
||||
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', f.mirror, 'HEAD:refs/heads/served');
|
||||
f.setState({ served: next, lastFetch: new Date(NOW).toISOString() });
|
||||
return { code: null };
|
||||
} });
|
||||
f.setState({ lastFetch: null });
|
||||
const result = await lookup(f.context, { method: 'get', args: GET, authorize() {} });
|
||||
assert.equal(result.commit, next);
|
||||
assert.match(JSON.stringify(result.lines), /Changed ferry/);
|
||||
});
|
||||
|
||||
test('real socket awaits lookup results and rechecks rejected query arguments', { timeout: 30_000 }, async t => {
|
||||
const f = await world(t);
|
||||
const path = join(f.root, 'lookup.sock');
|
||||
const server = await serve({ broker: f.broker, path }); t.after(() => server.close());
|
||||
const client = new Client({ path, cap: f.agent });
|
||||
const result = await client.call('library.get', GET);
|
||||
assert.equal(result.commit, f.commit);
|
||||
await assert.rejects(client.call('library.find', { universe: 'harbor', query: '', scope: 'anything' }), /query-argument-invalid/);
|
||||
});
|
||||
|
||||
test('runtime actually injects the lookup adapter, without exposing human library.file', { timeout: 30_000 }, async t => {
|
||||
const f = scratchRepo(t, TEXTS), dataRoot = join(f.root, 'runtime'); mkdirSync(dataRoot);
|
||||
const business = { id: 'acme', human: 'jason', projects: { books: mappingEntry() },
|
||||
arbiters: { technical: 'coder', delivery: 'coder' },
|
||||
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
|
||||
const runtime = await startBroker({ dataRoot, businesses: { acme: business } }); t.after(() => runtime.close());
|
||||
const mirror = library.mirrorPath({ business, project: 'books', dataRoot });
|
||||
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', mirror, 'HEAD:refs/heads/served');
|
||||
writeFileSync(join(mirror, library.SYNC_STATE_FILE), JSON.stringify({ version: 1, branch: 'main', served: f.commit,
|
||||
lastFetch: new Date().toISOString(), lastError: null, diverged: null, problems: [] }), { mode: 0o600 });
|
||||
const cap = runtime.broker.bindLaunch({ business: 'acme', role: 'coder', run: 'r224-runtime', harness: 'pi' });
|
||||
runtime.broker.request(cap, { verb: 'role.claim' });
|
||||
assert.equal((await runtime.broker.request(cap, { verb: 'library.get', args: GET })).commit, f.commit);
|
||||
await assert.rejects(runtime.broker.request(cap, { verb: 'library.file', args: { project: 'books', path: GET.path } }),
|
||||
{ code: 'human-required' });
|
||||
});
|
||||
|
||||
test('C2-05c broker forwards supporting scope and returns the served blob without adding write verbs', async t => {
|
||||
const projects = { books: mappingEntry() };
|
||||
projects.books.universes.harbor.supporting = ['Book One/Support/'];
|
||||
const path = 'Book One/Support/Helper.md';
|
||||
const f = await world(t, { projects, files: { ...TEXTS, [path]: note('summary', 'Helper', 'Supporting fixture.') } });
|
||||
assert.deepEqual((await f.call('library.find', { universe: 'harbor', query: '', scope: 'supporting' })).results.map(x => x.path), [path]);
|
||||
assert.deepEqual((await f.call('library.search', { universe: 'harbor', text: 'Supporting fixture', scope: 'supporting' })).results.map(x => x.path), [path]);
|
||||
assert.equal(await f.call('library.get', { universe: 'harbor', path }), null);
|
||||
const result = await f.call('library.get', { universe: 'harbor', path, scope: 'supporting' });
|
||||
assert.equal(result.scope, 'supporting');
|
||||
assert.equal(result.commit, f.commit);
|
||||
assert.equal(result.blob, git(f.mirror, 'rev-parse', `${f.commit}:${path}`));
|
||||
assert.equal(await f.call('library.get', { universe: 'second', path, scope: 'supporting' }), null);
|
||||
await assert.rejects(f.call('library.manuscript', { universe: 'harbor', book: 'Book One', scope: 'supporting' }), { code: 'query-argument-invalid' });
|
||||
});
|
||||
@@ -27,6 +27,10 @@ test('C2-04: library mappings load without roots, remain frozen, and retain mapp
|
||||
assert.deepEqual(validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }).projects.books.universes.harbor.playground, ['Elsewhere/Playground/']);
|
||||
doc.projects.books.universes.harbor.playground = ['Second World/Playground/'];
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-playground-other-universe/);
|
||||
// C2-01d: a supporting prefix inside another universe's book refuses with its own code.
|
||||
doc.projects.books = mappingEntry();
|
||||
doc.projects.books.universes.harbor.supporting = ['Second World/Notes/'];
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-supporting-other-universe/);
|
||||
doc.projects.books = { ...mappingEntry(), root: '/forbidden' };
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-mapping-invalid/);
|
||||
});
|
||||
|
||||
@@ -528,6 +528,7 @@ export function createLauncher({
|
||||
contract: readFileSync(resolved.contract, "utf8"),
|
||||
arbiter: Object.values(b.arbiters).includes(instance),
|
||||
tracker,
|
||||
library: Object.values(b.projects ?? {}).some(project => project.kind === "library"),
|
||||
credentials,
|
||||
session: { run, launchedBy, workspace: inSandbox, toolSocket, turnMarker, checkouts: inSandbox === workspace ? checkouts : checkouts.map((c) => ({ ...c, path: join(inSandbox, relative(workspace, c.path)) })) },
|
||||
harness,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { bootConfig, loadSystem } from "../src/config.mjs";
|
||||
import { startTimeOf } from "../src/host.mjs";
|
||||
import { createLauncher } from "../src/launcher.mjs";
|
||||
import { fakeHarnesses, fixture, tmp } from "./helpers.mjs";
|
||||
import { mappingEntry } from "../../library/tests/helpers.mjs";
|
||||
// Row 264a (#1769): a thread made by hand gets the default session the 3k-3l migration gives a
|
||||
// legacy thread, with the thread's id.
|
||||
const DEFAULT_SESSION = [
|
||||
@@ -529,3 +530,24 @@ test('C2-09a: terminating one creative thread does not signal an unselected peer
|
||||
await until(() => !readSessions(f.dataRoot).some(row => row.run === runs[0]));
|
||||
assert.ok(readSessions(f.dataRoot).some(row => row.run === runs[1]));
|
||||
});
|
||||
|
||||
// Row224: the launcher must derive library presence from business projects, not
|
||||
// from session checkouts (a library never has an agent clone).
|
||||
test("C2-05b a real launched bundle gets lookups for a library project without a library checkout", { skip, timeout: 60000 }, async t => {
|
||||
const prepared = prepare(t, { idle: 10, edit: doc => {
|
||||
doc.projects.books = mappingEntry();
|
||||
return doc;
|
||||
} });
|
||||
const { f } = prepared;
|
||||
const host = await start(t, prepared);
|
||||
const project = host.call("project.create", { name: "library work", repo: "stack" }).id;
|
||||
const thread = host.call("thread.create", { project, name: "lookup" }).id;
|
||||
await ask(host.call, thread, "inspect bundle");
|
||||
const run = launchLog(f).find(e => e.event === "launch" && e.thread === thread).run;
|
||||
const tools = JSON.parse(readFileSync(join(f.dataRoot, "launches", "acme", run, "bundle", "tools.json"), "utf8"));
|
||||
assert.deepEqual(tools.filter(tool => tool.name.startsWith("library_")).map(tool => tool.name),
|
||||
["library_find", "library_get", "library_search", "library_manuscript", "library_universe"]);
|
||||
const [session] = threadRuns(f, thread);
|
||||
assert.ok(session);
|
||||
assert.ok(!session.checkouts?.some(checkout => checkout.project === "books"));
|
||||
});
|
||||
@@ -292,6 +292,7 @@ function prompt({ contract, resolved, session, tools, untyped, builtins, persona
|
||||
// resolved resolveInstance(...) output
|
||||
// contract the role contract's text
|
||||
// arbiter true when the business names this instance as an arbiter
|
||||
// library true when the trusted business configuration has a library project
|
||||
// tracker true when the broker has task verbs for the business
|
||||
// credentials the broker's credentialStatus(...) for the instance (metadata only)
|
||||
// session { run, launchedBy, workspace, toolSocket, turnMarker, checkouts }
|
||||
@@ -309,11 +310,11 @@ function prompt({ contract, resolved, session, tools, untyped, builtins, persona
|
||||
// (row 213); an instance with persona.sha256 and no personaDir refuses
|
||||
// Returns { manifest, files, policy, tools, env } with absolute paths; env is the module's
|
||||
// adapter variables, which the host writes into session.json.
|
||||
export function buildBundle({ dir, resolved, contract, arbiter = false, tracker = false, credentials = [], session, harness, route = "primary", routeReason = null, harnessVersion = null, models = null, land = false, skillsDir = null, personaDir = null }) {
|
||||
export function buildBundle({ dir, resolved, contract, arbiter = false, tracker = false, library = false, credentials = [], session, harness, route = "primary", routeReason = null, harnessVersion = null, models = null, land = false, skillsDir = null, personaDir = null }) {
|
||||
if (route !== "primary" && route !== "fallback") throw new BundleError(`route ${JSON.stringify(route)} isn't primary or fallback`);
|
||||
const model = sessionModel(resolved, models, route);
|
||||
const web = webPlan(resolved, model.harness, harness.builtinWeb());
|
||||
const tools = typedTools({ resolved, arbiter, tracker, projects: (session.checkouts ?? []).map((c) => c.project), webTyped: !web.builtin, land });
|
||||
const tools = typedTools({ resolved, arbiter, tracker, library, projects: (session.checkouts ?? []).map((c) => c.project), webTyped: !web.builtin, land });
|
||||
const untyped = untypedActions(resolved, tools);
|
||||
const persona = readPersona(resolved, personaDir);
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
|
||||
@@ -78,9 +78,10 @@ const TASK_TOOLS = {
|
||||
|
||||
const MESSAGE_CLASSES = ["REQUEST", "ASSIGNMENT", "REVIEW-REQUEST", "REVIEW-RESULT", "RESULT", "INFO", "DECISION", "REACTION", "BLOCK"];
|
||||
|
||||
// typedTools({ resolved, arbiter, tracker, projects, land }) -> [{ name, verb, action, description, parameters }]
|
||||
// typedTools({ resolved, arbiter, tracker, projects, land, library }) -> [{ name, verb, action, description, parameters }]
|
||||
// resolved resolveInstance(...) output for the session's instance
|
||||
// arbiter true when the business names this instance as an arbiter
|
||||
// library true when the trusted business configuration has a library project
|
||||
// tracker true when the broker has task verbs for the business
|
||||
// projects the project ids the session has a clone of
|
||||
// land true when the role and thread hold a merge decision a landing could use (landHeld)
|
||||
@@ -88,7 +89,7 @@ const MESSAGE_CLASSES = ["REQUEST", "ASSIGNMENT", "REVIEW-REQUEST", "REVIEW-RESU
|
||||
// `verb` is the broker verb; the `launch` tool has verb "launch" and goes
|
||||
// to the host instead, and "working.push", "working.fetch" and
|
||||
// "working.land" go to the host's git socket.
|
||||
export function typedTools({ resolved, arbiter = false, tracker = false, projects = [], webTyped = true, land = false }) {
|
||||
export function typedTools({ resolved, arbiter = false, tracker = false, projects = [], webTyped = true, land = false, library = false }) {
|
||||
const held = new Set([...resolved.limits.authority.withinRole, ...resolved.limits.authority.crossRole]);
|
||||
const tools = [];
|
||||
const add = (name, verb, action, description, parameters) => tools.push({ name, verb, action, description, parameters });
|
||||
@@ -222,6 +223,33 @@ export function typedTools({ resolved, arbiter = false, tracker = false, project
|
||||
url: str("The https url to fetch", { maxLength: 2048 }),
|
||||
}, ["url"]));
|
||||
}
|
||||
// Row224: no library clone or token reaches a session. The broker chooses the served
|
||||
// commit and requires a universe on every call; cursors page without cutting replies.
|
||||
if (library) {
|
||||
const universe = str("Universe ID in the configured library", { minLength: 1 });
|
||||
const project = str("Library project ID; omit only when the business has one library");
|
||||
const scope = str("canon by default; playground and supporting are opt-in", { enum: ["canon", "playground", "supporting"] });
|
||||
const manuscriptScope = str("canon by default; playground is opt-in", { enum: ["canon", "playground"] });
|
||||
const book = str("Book folder from library_universe");
|
||||
const offset = { type: "integer", minimum: 0, description: "next from the previous page" };
|
||||
const line = { type: "integer", minimum: 1, description: "One-based line number" };
|
||||
const base = { universe, project };
|
||||
add("library_find", "library.find", null, "Find names in one universe, at the served commit. At most 50 results; continue with next as offset. Playground and supporting are opt-in.", obj({
|
||||
...base, query: str("Case-insensitive name or filename substring; empty matches all"),
|
||||
type: str("Mapped type", { enum: ["character", "place", "research", "tracker", "summary"] }),
|
||||
book, scope, offset,
|
||||
}, ["universe", "query"]));
|
||||
add("library_get", "library.get", null, "Read a note and its blob at the served commit, canon by default. Playground/supporting paths need matching scope; mismatch returns null. next pages lines; links_next pages visible links in supporting/playground/canon order.", obj({
|
||||
...base, path: str("Mapped note path relative to the library"), scope, from: line, links_from: offset,
|
||||
}, ["universe", "path"]));
|
||||
add("library_search", "library.search", null, "Search text in one universe at the served commit. At most 50 hits; use next as offset. Playground and supporting are opt-in.", obj({
|
||||
...base, text: str("Nonempty case-insensitive substring", { minLength: 1 }), book, scope, offset,
|
||||
}, ["universe", "text"]));
|
||||
add("library_manuscript", "library.manuscript", null, "Read one chapter of a book at the served commit, at most 400 lines. Omit chapter for the first; continue with next or next_chapter.", obj({
|
||||
...base, book, scope: manuscriptScope, chapter: str("Chapter filename without extension"), from: line, to: line,
|
||||
}, ["universe", "book"]));
|
||||
add("library_universe", "library.universe", null, "Read a universe's mapping at the served commit: books, types, playground prefixes, and hybrid sources/constraints/notes or draft metadata.", obj(base, ["universe"]));
|
||||
}
|
||||
add("read_spec", "spec.read", null, "Read a project's committed north-star spec and its file history. commit is an optional full reachable commit ID. The file commit, rather than HEAD, is what a decision cites. confirmed says the file commit was authored by the business's human.", obj({
|
||||
project: str("Project ID from the business file"),
|
||||
commit: str("Optional full historical commit ID", { pattern: "^[0-9a-f]{40}$" }),
|
||||
@@ -277,6 +305,11 @@ export function typedTools({ resolved, arbiter = false, tracker = false, project
|
||||
if (held.has("task.create"))
|
||||
add("list_requirements", "requirements", null, "List the approved PRD's requirement ids, each with a one-line title. task_create takes only these. When none fits a request, raise a prd.approve decision instead of creating the task.", obj({}));
|
||||
}
|
||||
// mos-1308 / M3: no generic spec, evidence, tracker, decision or write tools. The
|
||||
// C2-08 web grant still supplies broker web reads to companion and research only.
|
||||
if (CREATIVE_DEFINITIONS.includes(resolved.definition))
|
||||
return tools.filter(tool => tool.name === "send_message" || tool.name.startsWith("library_") ||
|
||||
tool.name === "web_search" || tool.name === "web_fetch");
|
||||
return tools;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Row224 M3: exact creative tool sets preserve the existing C2-08 web grants.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { typedTools } from '../src/tools.mjs';
|
||||
import { buildBundle } from '../src/bundle.mjs';
|
||||
import { TYPES } from '../../library/src/mapping.mjs';
|
||||
import { HARNESS, REPO_SKILLS, resolvedFor, scratch } from './helpers.mjs';
|
||||
|
||||
const LOOKUPS = ['library_find', 'library_get', 'library_search', 'library_manuscript', 'library_universe'];
|
||||
const CREATIVE = ['companion', 'research', 'ideas', 'proofreader', 'fact-checker', 'continuity'];
|
||||
const role = (t, definition, harness = 'pi') => resolvedFor(t, 'bean', d => {
|
||||
d.roles.bean = { definition, vars: { harness, model: 'claude-sonnet-5-5', thinking: 'high' } };
|
||||
});
|
||||
const expected = definition => ['send_message', ...(['companion', 'research'].includes(definition)
|
||||
? ['web_search', 'web_fetch'] : []), ...LOOKUPS];
|
||||
|
||||
test('M3: per-role snapshots contain six names, or eight with the C2-08 web grant', t => {
|
||||
for (const definition of CREATIVE) {
|
||||
const tools = typedTools({ resolved: role(t, definition), library: true, tracker: true, arbiter: true });
|
||||
assert.deepEqual(tools.map(t => t.name), expected(definition), definition);
|
||||
assert.ok(tools.every(t => t.parameters.additionalProperties === false));
|
||||
}
|
||||
});
|
||||
|
||||
test('every noncreative role gets lookups when a library is configured, not otherwise', t => {
|
||||
for (const instance of ['coder', 'reviewer', 'pm', 'chief']) {
|
||||
const resolved = resolvedFor(t, instance);
|
||||
const ordinary = typedTools({ resolved }).map(t => t.name);
|
||||
assert.ok(!ordinary.some(name => name.startsWith('library_')));
|
||||
const tools = typedTools({ resolved, library: true });
|
||||
assert.deepEqual(tools.filter(t => t.name.startsWith('library_')).map(t => t.name), LOOKUPS);
|
||||
assert.deepEqual(tools.filter(t => !t.name.startsWith('library_')).map(t => t.name), ordinary);
|
||||
}
|
||||
});
|
||||
|
||||
test('lookup schemas require a universe even for get, include paging, and cannot select repo or commit', t => {
|
||||
const tools = typedTools({ resolved: role(t, 'ideas'), library: true });
|
||||
const required = [ ['universe', 'query'], ['universe', 'path'], ['universe', 'text'], ['universe', 'book'], ['universe'] ];
|
||||
for (const [i, tool] of tools.filter(t => t.name.startsWith('library_')).entries()) {
|
||||
assert.deepEqual(tool.parameters.required, required[i]);
|
||||
assert.equal(tool.verb, LOOKUPS[i].replace('_', '.'));
|
||||
assert.equal(tool.action, null);
|
||||
assert.equal(tool.parameters.additionalProperties, false);
|
||||
for (const key of ['repo', 'commit', 'business', 'accept']) assert.equal(Object.hasOwn(tool.parameters.properties, key), false);
|
||||
}
|
||||
const byName = Object.fromEntries(tools.map(t => [t.name, t]));
|
||||
assert.deepEqual(byName.library_find.parameters.properties.scope.enum, ['canon', 'playground', 'supporting']);
|
||||
assert.deepEqual(byName.library_find.parameters.properties.type.enum, [...TYPES]);
|
||||
assert.equal(byName.library_find.parameters.properties.offset.minimum, 0);
|
||||
assert.equal(byName.library_get.parameters.properties.from.minimum, 1);
|
||||
assert.equal(byName.library_get.parameters.properties.links_from.minimum, 0);
|
||||
assert.equal(byName.library_manuscript.parameters.properties.chapter.type, 'string');
|
||||
});
|
||||
|
||||
test('bundle flag reaches both harness tool manifests and creative prompt without generic tools', t => {
|
||||
for (const definition of CREATIVE) for (const harness of ['pi', 'claude-code']) {
|
||||
const dir = scratch(t), workspace = join(dir, 'ws'); mkdirSync(workspace);
|
||||
const b = buildBundle({ dir: join(dir, 'bundle'), resolved: role(t, definition, harness), library: true,
|
||||
contract: '# Creative\n', tracker: true, arbiter: true, skillsDir: REPO_SKILLS,
|
||||
session: { run: 'r224', launchedBy: 'chief', workspace, toolSocket: join(dir, 't.sock'), turnMarker: join(dir, 'turn.done') },
|
||||
harness: HARNESS[harness] });
|
||||
assert.deepEqual(b.tools.map(t => t.name), expected(definition), `${definition} ${harness}`);
|
||||
const serialized = JSON.parse(readFileSync(b.files.tools, 'utf8'));
|
||||
assert.deepEqual(serialized.map(t => t.name), expected(definition));
|
||||
const prompt = readFileSync(b.files.prompt, 'utf8');
|
||||
assert.match(prompt, /library_find/);
|
||||
assert.doesNotMatch(prompt, /Bus tools: [^\n]*(read_spec|list_agents|inbox|trail|write_record|raise_decision|list_tasks)/);
|
||||
}
|
||||
});
|
||||
|
||||
test('C2-05c scope schemas expand only find/search/get, keeping manuscript and tool names unchanged', t => {
|
||||
const tools = typedTools({ resolved: role(t, 'ideas'), library: true });
|
||||
assert.deepEqual(tools.map(t => t.name), ['send_message', ...LOOKUPS]);
|
||||
for (const name of ['library_find', 'library_search', 'library_get']) {
|
||||
const tool = tools.find(t => t.name === name);
|
||||
assert.deepEqual(tool.parameters.properties.scope.enum, ['canon', 'playground', 'supporting']);
|
||||
assert.ok(!tool.parameters.required.includes('scope'));
|
||||
}
|
||||
assert.deepEqual(tools.find(t => t.name === 'library_manuscript').parameters.properties.scope.enum, ['canon', 'playground']);
|
||||
});
|
||||
+55
-23
@@ -31,33 +31,72 @@ deep-frozen copy. `where` names the entry in messages. `fail(code, message)` def
|
||||
- `user` is the bot account name from design step 3, required for sync. It is optional in the
|
||||
mapping and checked with row 199's account name pattern, `[A-Za-z0-9_.-]`, 1 to 100 characters.
|
||||
- `vaultPrefix` is optional, one folder name.
|
||||
- `editor` is optional, `{ "name", "email" }` plus an optional `"user"`: the ident her Console saves commit under
|
||||
(C2-01d). Each value is text that is non-empty after trim, with no control character
|
||||
(`U+0000`-`U+001F`, `U+007F`-`U+009F`), `<` or `>`, and no whitespace or git-stripped character at
|
||||
either end. `user` is her Gitea login for the `library-human` slot (sage-2925), checked as the
|
||||
entry's `user` is. `editor` and its `user` come back only when given; a save without them refuses
|
||||
in C2-02.
|
||||
- `types` maps a folder name to `character`, `place`, `research`, `tracker` or `summary`.
|
||||
`manuscript` lists folder names and shares none with `types`.
|
||||
- A universe has `name`, `books` and `playground`. A hybrid adds `kind: "hybrid"`, two or more
|
||||
plain-universe `sources`, `constraints` (each one `{ "exclude": ... }` or
|
||||
`{ "pin": { "entity", "field", "value" } }`) and `notes`, and may have `books: []`. A draft
|
||||
universe has only `kind: "draft"`, `name` and one or more `playground` prefixes, and comes back as
|
||||
`{ kind: "draft", name, books: [], playground }`. `kind` is absent, `"hybrid"` or `"draft"`.
|
||||
`{ kind: "draft", name, books: [], playground, supporting }`. `kind` is absent, `"hybrid"` or `"draft"`.
|
||||
- A playground prefix ends in `/` and has one of three levels (C2-01c). Book level lies inside one of
|
||||
its own entry's book folders. Universe level, in a plain universe or a hybrid, lies outside every
|
||||
book folder. A draft's prefixes all lie outside every book folder. No prefix holds another or is
|
||||
listed twice, across every entry.
|
||||
- Every universe kind may also have `supporting`, a list of prefixes where agents may write (C2-01d,
|
||||
M12). It comes back on every universe, `[]` when absent. A supporting prefix follows the playground's
|
||||
rules: it ends in `/`, has the same three levels, holds no other supporting prefix and is listed
|
||||
once, across every entry.
|
||||
- Between the two lists: a supporting prefix may equal a playground prefix of its own entry, and a
|
||||
playground prefix may hold one of its own entry's supporting prefixes. A supporting prefix may not
|
||||
hold a playground prefix, which scope would then never reach, and may not equal, hold or lie inside
|
||||
another entry's playground prefix.
|
||||
|
||||
Refusals:
|
||||
|
||||
| Code | When |
|
||||
|---|---|
|
||||
| `library-playground-other-universe` | a playground prefix lies inside, or holds, a book folder of another entry |
|
||||
| `library-mapping-invalid` | any other shape fault: unknown key, bad remote or branch, an unknown `kind`, a book folder in two universes, a book folder that holds another, a playground prefix that holds another or is listed twice, a prefix equal to a book folder or holding its own entry's book, a draft with `books` or with no prefix, a hybrid source that is unknown, a hybrid or a draft |
|
||||
| `library-supporting-other-universe` | a supporting prefix lies inside, or holds, a book folder of another entry |
|
||||
| `library-mapping-invalid` | any other shape fault: unknown key, bad remote or branch, an unknown `kind`, a book folder in two universes, a book folder that holds another, a playground or supporting prefix that holds another of its list or is listed twice, a prefix equal to a book folder or holding its own entry's book, a supporting prefix that holds a playground prefix or overlaps another entry's, a bad `editor`, a draft with `books` or with no prefix, a hybrid source that is unknown, a hybrid or a draft |
|
||||
|
||||
## Scope
|
||||
|
||||
`classify(mapping, path)` decides scope from the path alone. Only paths ending in lowercase `.md` are
|
||||
mapped. It checks prefixes first. A path under any entry's playground prefix is `playground`, with that
|
||||
entry as `universe` and the book folder holding the prefix as `book`, or `null` at universe level and
|
||||
mapped. It checks prefixes first, supporting before playground. A path under any entry's supporting
|
||||
prefix is `supporting`, and otherwise one under a playground prefix is `playground`. Either way that
|
||||
entry is `universe` and the book folder holding the prefix is `book`, or `null` at universe level and
|
||||
in a draft. Any other path under a book folder is `canon`. Everything else is `unmapped`. The type is
|
||||
the first folder after the playground prefix, or after the book: a `types` name, `manuscript`, or
|
||||
`null`.
|
||||
the first folder after the prefix, or after the book: a `types` name, `manuscript`, or `null`.
|
||||
|
||||
## Agent write paths
|
||||
|
||||
`supportingPath(mapping, universe, path, tree)` returns `path` when an agent may write it in that
|
||||
universe, else it refuses `library-write-story-path` (C2-01d, M12). `tree` is the array `listTree`
|
||||
returns for the commit the write lands on; anything else is a TypeError. It reads no git. The checks
|
||||
run in this order, so a path with two faults always names the same one:
|
||||
|
||||
1. a string of at most 1,024 UTF-8 bytes;
|
||||
2. no control character (the `editor` class), and the message leaves the path out;
|
||||
3. no backslash;
|
||||
4. not absolute;
|
||||
5. no empty, `.` or `..` segment, which also refuses `//` and a trailing `/`;
|
||||
6. `classify` gives `supporting` in that same universe, which refuses unmapped, `.canvas`, `.base`
|
||||
and attachment paths, canon, a playground-only folder and another universe's prefix;
|
||||
7. no folder on the way, and not the path itself, is a symlink (`120000`) or gitlink (`160000`) in
|
||||
`tree`;
|
||||
8. no folder on the way, and not the path itself, matches a tree path or tree folder after
|
||||
`normalize('NFC').toLowerCase()` unless it is that exact name. APFS folds case and Unicode form,
|
||||
so her checkout would put the file somewhere else. A tree that already holds case twins refuses a
|
||||
write to either (R7).
|
||||
|
||||
A delete's further rule, that the path exists at the tip as a `100644` blob, belongs to the C2-02
|
||||
broker.
|
||||
|
||||
## Reader
|
||||
|
||||
@@ -104,28 +143,21 @@ in an extension other than `.md`, in any case, embeds an attachment, and it is n
|
||||
a dot and up to 8 letters and digits, at least one a letter. Any other unresolved embed is R4, and so is
|
||||
an unresolved front matter value written as an embed (C2-01b N3, sage-1900).
|
||||
|
||||
## renderReply
|
||||
## renderItems
|
||||
|
||||
`renderReply(body, { check, changed, name })` turns a stored reply into what both channels show. It
|
||||
replaces C2-01's items renderer and is pure. A body is free text, or the design 5.2 object
|
||||
`{ "text", "citations" }` with library cites `{ path, commit, lines, quote, as? }` and web cites
|
||||
`{ url, quote }`. Any other body, a JSON object with another key or a bad cite included, renders as
|
||||
free text: the whole body, with no citations.
|
||||
|
||||
`check` is the stored check result (C2-06), `{ citations: [{ scope, agent }] }` in body order. A path
|
||||
cite's badge (`canon`, `playground` or `supporting`) and its agent-written label come only from it. A
|
||||
url cite is always `web`, and is a link only when its scheme is `https`. `changed({ path, commit }, i)`
|
||||
says whether the file's last commit is newer than the cited one. `plain` is the rendering as text:
|
||||
`renderItems(body, { scopeOf, changed })` turns a reply body into the plain text both channels show.
|
||||
It is pure. Item kinds are `fact`, `finding`, `correction` (with `from` and `to`), `question` and
|
||||
`idea`. Each cite is a path with commit, line range and quote, or a url with a quote.
|
||||
|
||||
```
|
||||
Mara's eyes are grey in chapter 3.
|
||||
[canon] Book One/Chapters/Chapter 03.md:41 @aaaaaaa "her grey eyes"
|
||||
[supporting] Notes/Eyes.md:9-10 @aaaaaaa "green eyes" (agent-written: acme/scribe) (changed since)
|
||||
[web] https://example.invalid/tides "spring tides"
|
||||
1. Correction: Chapter 1 says grey.
|
||||
From: "green eyes"
|
||||
To: "grey eyes"
|
||||
[canon] Book One/Characters/Mara Vell.md:9 @bbbbbbb "green eyes" (changed since)
|
||||
```
|
||||
|
||||
The broker's write notice, `{ "notice": "library-write", op, path, commit }`, gives the line
|
||||
`<name> wrote <path> @<commit>` or `<name> deleted <path> @<commit>`.
|
||||
A body of `{ "items": [], "notice": ... }` gives `No cited answer`, `No answer` or `The turn failed`.
|
||||
Any other shape gives the fixed line `This reply could not be shown.` and none of the body's text.
|
||||
|
||||
## Survey
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// The lookup index of design 3 (C2-05a): types, names, links and backlinks for one commit of her repo.
|
||||
// It is built from the reader's output, so it holds only mapped, readable files: R1, R2 and R7 leave a
|
||||
// file out, and an unresolved link is not in it. It is cached in memory by commit id. The key also
|
||||
// carries the repo path and a digest of the mapping, because one commit reads differently under another
|
||||
// mapping. The index is not universe-aware; query.mjs decides what a universe may see.
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readLibrary } from './reader.mjs';
|
||||
import { raise } from './problems.mjs';
|
||||
|
||||
const CACHE_LIMIT = 8;
|
||||
const cache = new Map();
|
||||
|
||||
const push = (map, key, value) => {
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key).push(value);
|
||||
};
|
||||
// A file's name for lookups: its front matter `name` when it is text, else its file name.
|
||||
const displayName = file => {
|
||||
const name = file.frontmatter?.name;
|
||||
return typeof name === 'string' && name.trim() ? name.trim() : file.name;
|
||||
};
|
||||
// A link as the index keeps it: the file at the other end, its line, and the field when front matter holds it.
|
||||
const linkAt = (path, link) => ({ path, line: link.line, ...(link.field === undefined ? {} : { field: link.field }) });
|
||||
|
||||
// Builds the index from readLibrary's output. Pure: no git, no clock.
|
||||
export function buildIndex(library, mapping) {
|
||||
const files = new Map(), types = new Map(), names = new Map(), linksOut = new Map(), linksIn = new Map();
|
||||
for (const file of library.files) {
|
||||
const name = displayName(file);
|
||||
files.set(file.path, {
|
||||
path: file.path, scope: file.scope, universe: file.universe, book: file.book, type: file.type,
|
||||
folder: file.folder, stem: file.name, name, frontmatter: file.frontmatter, lines: file.lines,
|
||||
});
|
||||
if (file.type !== null) push(types, file.type, file.path);
|
||||
push(names, name.toLowerCase(), file.path);
|
||||
linksOut.set(file.path, file.links.map(link => linkAt(link.path, link)));
|
||||
for (const link of file.links) push(linksIn, link.path, linkAt(file.path, link));
|
||||
}
|
||||
return Object.freeze({
|
||||
commit: library.commit, mapping, files, paths: [...files.keys()], types, names, linksOut, linksIn,
|
||||
problems: library.problems,
|
||||
});
|
||||
}
|
||||
|
||||
const digest = mapping => createHash('sha256').update(JSON.stringify(mapping)).digest('hex');
|
||||
|
||||
// The index for one commit under one mapping, read once and kept in memory. A repeat call returns the same
|
||||
// object. Past CACHE_LIMIT entries the oldest goes. A failed read is not kept.
|
||||
export async function indexFor({ repo, commit, mapping }) {
|
||||
if (!mapping || mapping.kind !== 'library') raise('library-mapping-invalid', 'expected a checked library mapping');
|
||||
const key = JSON.stringify([repo, commit, digest(mapping)]);
|
||||
if (cache.has(key)) {
|
||||
const hit = cache.get(key);
|
||||
cache.delete(key);
|
||||
cache.set(key, hit);
|
||||
return hit;
|
||||
}
|
||||
const pending = readLibrary({ repo, commit, mapping }).then(library => buildIndex(library, mapping));
|
||||
cache.set(key, pending);
|
||||
while (cache.size > CACHE_LIMIT) cache.delete(cache.keys().next().value);
|
||||
try {
|
||||
return await pending;
|
||||
} catch (error) {
|
||||
if (cache.get(key) === pending) cache.delete(key);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearIndexCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
export function indexCacheSize() {
|
||||
return cache.size;
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
export { LibraryError, RULES, problem, sortProblems } from './problems.mjs';
|
||||
export { checkMapping, TYPES } from './mapping.mjs';
|
||||
export { classify } from './scope.mjs';
|
||||
export { supportingPath } from './supporting.mjs';
|
||||
export { extractLinks, parseWikilink, resolver, formOf, isAttachment } from './links.mjs';
|
||||
export { readLibrary, listTree, readTexts, parseFile, parseFrontMatter, headCommit, MAX_BLOB, LINK_FIELDS } from './reader.mjs';
|
||||
export { renderReply, SCOPES, NOTICE_OPS } from './render.mjs';
|
||||
export { renderItems, KINDS, NOTICES, UNREADABLE } from './render.mjs';
|
||||
export { survey, formatSurvey, surveyMain } from './survey.mjs';
|
||||
export { SYNC_STATE_FILE, mirrorPath, ensureMirror, withLibraryLock, servedCommit, readSyncState, libraryStatus, readLibraryFile } from './mirror.mjs';
|
||||
export { syncCommand, sync, ASKPASS, SLOT, SYNC_INTERVAL_MS } from './sync.mjs';
|
||||
export { buildIndex, indexFor, clearIndexCache, indexCacheSize } from './index-build.mjs';
|
||||
export { sees, find, get, search, manuscript, universeInfo, QUERY_REPLY_CAP, LOOKUP_LIMIT, MANUSCRIPT_LINES } from './query.mjs';
|
||||
@@ -1,8 +1,9 @@
|
||||
// The library project entry of a business file (design 1.1): her folders mapped onto universes,
|
||||
// books, type folders, manuscript folders and playground prefixes at three levels (ruling after r11,
|
||||
// C2-01c): inside one of the entry's books, outside every book, or in a draft universe. A pure validator. Business file
|
||||
// loading wires it in C2-04, which passes its own `fail` so the refusal reads as a business file
|
||||
// problem. Agents can't write this entry (M9, C2-04).
|
||||
// C2-01c): inside one of the entry's books, outside every book, or in a draft universe. Supporting
|
||||
// prefixes (C2-01d) follow the same rules and are where agents may write (M12). A pure validator.
|
||||
// Business file loading wires it in C2-04, which passes its own `fail` so the refusal reads as a
|
||||
// business file problem. Agents can't write this entry (M9, C2-04).
|
||||
import { raise } from './problems.mjs';
|
||||
|
||||
export const TYPES = Object.freeze(['character', 'place', 'research', 'tracker', 'summary']);
|
||||
@@ -12,6 +13,12 @@ const BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
||||
const USER = /^[A-Za-z0-9_.-]{1,100}$/;
|
||||
const CONSTRAINT_KEYS = ['exclude', 'pin'];
|
||||
const KINDS = ['hybrid', 'draft'];
|
||||
// C2-01d (F41): the editor ident her Console saves commit under. Git drops `<` and `>` from an ident
|
||||
// and commits under a different name, so they refuse here (webby-0169 R1).
|
||||
const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;
|
||||
// Git's ident.c crud() strips any byte <= 0x20 and , : ; < > " \ ' from both ends of an ident, so a
|
||||
// value that starts or ends with one would save one name and commit another (sage-3601, sage-3606).
|
||||
const IDENT_CRUD_END = /^[\u0000-\u0020,:;<>"\\']|[\u0000-\u0020,:;<>"\\']$/;
|
||||
|
||||
// A folder segment from her vault: spaces and capitals are fine, separators, control characters
|
||||
// and dot names are not.
|
||||
@@ -40,7 +47,7 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
};
|
||||
|
||||
object(entry, '');
|
||||
only(entry, ['kind', 'remote', 'branch', 'user', 'vaultPrefix', 'universes', 'types', 'manuscript'], 'entry');
|
||||
only(entry, ['kind', 'remote', 'branch', 'user', 'vaultPrefix', 'editor', 'universes', 'types', 'manuscript'], 'entry');
|
||||
if (entry.kind !== 'library') bad('kind must be "library"');
|
||||
let url;
|
||||
try { url = new URL(entry.remote); } catch { bad('remote must be an https:// URL'); }
|
||||
@@ -52,6 +59,20 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
if (Object.hasOwn(entry, 'user') && (typeof entry.user !== 'string' || !USER.test(entry.user)))
|
||||
bad('user must be a plain account name');
|
||||
if (Object.hasOwn(entry, 'vaultPrefix') && !segment(entry.vaultPrefix)) bad('vaultPrefix must be one folder name');
|
||||
if (Object.hasOwn(entry, 'editor')) {
|
||||
object(entry.editor, 'editor');
|
||||
only(entry.editor, ['name', 'email', 'user'], 'editor');
|
||||
// Her Gitea login for the library-human slot, which C2-02 passes in (sage-2925). Optional here.
|
||||
if (Object.hasOwn(entry.editor, 'user') && (typeof entry.editor.user !== 'string' || !USER.test(entry.editor.user)))
|
||||
bad('editor.user must be a plain account name');
|
||||
for (const key of ['name', 'email']) {
|
||||
const value = entry.editor[key];
|
||||
if (typeof value !== 'string' || !value.trim() || IDENT_BAD.test(value))
|
||||
bad(`editor.${key} must be non-empty text with no control character, < or >`);
|
||||
if (value.trim() !== value || IDENT_CRUD_END.test(value))
|
||||
bad(`editor.${key} must not start or end with whitespace or any of , : ; " ' \\`);
|
||||
}
|
||||
}
|
||||
|
||||
object(entry.types, 'types');
|
||||
for (const [name, type] of Object.entries(entry.types)) {
|
||||
@@ -64,6 +85,7 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
object(entry.universes, 'universes');
|
||||
if (!Object.keys(entry.universes).length) bad('universes must name at least one universe');
|
||||
const universes = {}, bookOwner = new Map(), prefixes = [], prefixOwner = new Map();
|
||||
const supports = [], supportOwner = new Map();
|
||||
for (const [id, universe] of Object.entries(entry.universes)) {
|
||||
const name = `universes.${id}`;
|
||||
if (!ID.test(id)) bad(`universe id ${JSON.stringify(id)} is not a plain id`);
|
||||
@@ -71,8 +93,8 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
if (Object.hasOwn(universe, 'kind') && !KINDS.includes(universe.kind)) bad(`${name}.kind must be "hybrid" or "draft"`);
|
||||
const hybrid = universe.kind === 'hybrid', draft = universe.kind === 'draft';
|
||||
if (draft && Object.hasOwn(universe, 'books')) bad(`${name} is a draft and takes no books`);
|
||||
only(universe, hybrid ? ['kind', 'name', 'sources', 'books', 'playground', 'constraints', 'notes']
|
||||
: draft ? ['kind', 'name', 'playground'] : ['name', 'books', 'playground'], name);
|
||||
only(universe, hybrid ? ['kind', 'name', 'sources', 'books', 'playground', 'supporting', 'constraints', 'notes']
|
||||
: draft ? ['kind', 'name', 'playground', 'supporting'] : ['name', 'books', 'playground', 'supporting'], name);
|
||||
if (typeof universe.name !== 'string' || !universe.name.trim() || universe.name.length > 128 || /[\u0000-\u001f]/.test(universe.name))
|
||||
bad(`${name}.name must be a short display name`);
|
||||
const books = draft ? [] : list(universe.books, `${name}.books`, folder, 'a folder path');
|
||||
@@ -81,12 +103,14 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
if (bookOwner.has(book)) bad(`book folder ${JSON.stringify(book)} is in two universes`);
|
||||
bookOwner.set(book, id);
|
||||
}
|
||||
const playground = list(universe.playground ?? [], `${name}.playground`,
|
||||
value => typeof value === 'string' && value.endsWith('/') && folder(value.slice(0, -1)), 'a folder path ending in /');
|
||||
const prefix = value => typeof value === 'string' && value.endsWith('/') && folder(value.slice(0, -1));
|
||||
const playground = list(universe.playground ?? [], `${name}.playground`, prefix, 'a folder path ending in /');
|
||||
if (draft && !playground.length) bad(`${name}.playground must name at least one prefix`);
|
||||
for (const prefix of playground) { prefixes.push(prefix); prefixOwner.set(prefix, id); }
|
||||
if (draft) { universes[id] = { kind: 'draft', name: universe.name, books, playground }; continue; }
|
||||
const out = { name: universe.name, books, playground };
|
||||
for (const p of playground) { prefixes.push(p); prefixOwner.set(p, id); }
|
||||
const supporting = list(universe.supporting ?? [], `${name}.supporting`, prefix, 'a folder path ending in /');
|
||||
for (const p of supporting) { supports.push(p); supportOwner.set(p, id); }
|
||||
if (draft) { universes[id] = { kind: 'draft', name: universe.name, books, playground, supporting }; continue; }
|
||||
const out = { name: universe.name, books, playground, supporting };
|
||||
if (hybrid) {
|
||||
out.kind = 'hybrid';
|
||||
out.sources = list(universe.sources, `${name}.sources`, value => typeof value === 'string' && ID.test(value), 'a universe id');
|
||||
@@ -124,18 +148,36 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
for (const a of books) for (const b of books) if (a !== b && holds(a, b)) bad(`book folder ${JSON.stringify(a)} holds book folder ${JSON.stringify(b)}`);
|
||||
for (const a of prefixes) for (const b of prefixes) if (a !== b && holds(a, b)) bad(`playground ${JSON.stringify(a)} holds playground ${JSON.stringify(b)}`);
|
||||
if (new Set(prefixes).size !== prefixes.length) bad('a playground prefix is listed twice');
|
||||
for (const a of supports) for (const b of supports) if (a !== b && holds(a, b)) bad(`supporting ${JSON.stringify(a)} holds supporting ${JSON.stringify(b)}`);
|
||||
if (new Set(supports).size !== supports.length) bad('a supporting prefix is listed twice');
|
||||
// A prefix's level comes from where it sits (ruling after r11). Inside one of its own entry's books
|
||||
// is book level, outside every book is universe level, and a draft's prefixes are all outside. Scope
|
||||
// checks prefixes first (scope.mjs), so a prefix equal to or holding a book would turn that book
|
||||
// into playground.
|
||||
for (const [prefix, id] of prefixOwner) {
|
||||
for (const [book, owner] of bookOwner) {
|
||||
const at = `universes.${id}.playground ${JSON.stringify(prefix)}`;
|
||||
if (prefix === `${book}/`) bad(`${at} is the whole book folder ${JSON.stringify(book)}`);
|
||||
const inside = holds(book, prefix), holding = holds(prefix, book);
|
||||
if ((inside || holding) && owner !== id)
|
||||
fail('library-playground-other-universe', `${where} ${at} ${inside ? 'lies inside' : 'holds'} book folder ${JSON.stringify(book)} of universes.${owner}`);
|
||||
if (holding) bad(`${at} holds its own book folder ${JSON.stringify(book)}`);
|
||||
// into playground or supporting.
|
||||
const againstBooks = (owners, list, code) => {
|
||||
for (const [prefix, id] of owners) {
|
||||
for (const [book, owner] of bookOwner) {
|
||||
const at = `universes.${id}.${list} ${JSON.stringify(prefix)}`;
|
||||
if (prefix === `${book}/`) bad(`${at} is the whole book folder ${JSON.stringify(book)}`);
|
||||
const inside = holds(book, prefix), holding = holds(prefix, book);
|
||||
if ((inside || holding) && owner !== id)
|
||||
fail(code, `${where} ${at} ${inside ? 'lies inside' : 'holds'} book folder ${JSON.stringify(book)} of universes.${owner}`);
|
||||
if (holding) bad(`${at} holds its own book folder ${JSON.stringify(book)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
againstBooks(prefixOwner, 'playground', 'library-playground-other-universe');
|
||||
againstBooks(supportOwner, 'supporting', 'library-supporting-other-universe');
|
||||
// C2-01d, between the lists. A supporting prefix may equal a playground prefix of its own entry, and
|
||||
// a playground may hold one of its own entry's supporting prefixes. A supporting prefix that holds a
|
||||
// playground would hide it, since scope checks supporting first. Any overlap across entries would
|
||||
// give one path two universes.
|
||||
for (const [s, sid] of supportOwner) {
|
||||
for (const [p, pid] of prefixOwner) {
|
||||
const at = `universes.${sid}.supporting ${JSON.stringify(s)}`;
|
||||
if (sid !== pid && (s === p || holds(s, p) || holds(p, s)))
|
||||
bad(`${at} overlaps playground ${JSON.stringify(p)} of universes.${pid}`);
|
||||
if (sid === pid && s !== p && holds(s, p)) bad(`${at} holds playground ${JSON.stringify(p)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +185,8 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
kind: 'library', remote: entry.remote, branch: entry.branch,
|
||||
...(Object.hasOwn(entry, 'user') ? { user: entry.user } : {}),
|
||||
...(Object.hasOwn(entry, 'vaultPrefix') ? { vaultPrefix: entry.vaultPrefix } : {}),
|
||||
...(Object.hasOwn(entry, 'editor') ? { editor: { name: entry.editor.name, email: entry.editor.email,
|
||||
...(Object.hasOwn(entry.editor, 'user') ? { user: entry.editor.user } : {}) } } : {}),
|
||||
universes, types: { ...entry.types }, manuscript,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// The lookup queries of design 3 (C2-05a): find, get, search, manuscript and universeInfo. Each takes a
|
||||
// source {repo, commit, mapping} and an argument object, and returns a reply that fits QUERY_REPLY_CAP.
|
||||
// Universe isolation is here, in sees(): a file reaches a reply only when sees() allows it. Scope comes
|
||||
// from the path (D2). Unmapped files are not in the index, so they never appear. Lists page by position
|
||||
// and never cut an item. A search line too long to fit alone comes back as a marked hit with no excerpt;
|
||||
// a find item, a file's front matter or one of its lines that cannot fit alone is refused.
|
||||
import { raise } from './problems.mjs';
|
||||
import { TYPES } from './mapping.mjs';
|
||||
import { indexFor } from './index-build.mjs';
|
||||
import { listTree } from './reader.mjs';
|
||||
|
||||
// The same cap as packages/harness/src/tools.mjs REPLY_CAP. The query tests assert the two stay equal.
|
||||
export const QUERY_REPLY_CAP = 40_000;
|
||||
export const LOOKUP_LIMIT = 50;
|
||||
export const MANUSCRIPT_LINES = 400;
|
||||
const SCOPES = Object.freeze(['canon', 'playground', 'supporting']);
|
||||
const MANUSCRIPT_SCOPES = Object.freeze(['canon', 'playground']);
|
||||
const SCOPE_RANK = Object.freeze({ supporting: 0, playground: 1, canon: 2 });
|
||||
|
||||
const bytes = value => JSON.stringify(value).length;
|
||||
const invalid = message => raise('query-argument-invalid', message);
|
||||
const inside = (folder, path) => path.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
|
||||
|
||||
function textArg(value, name) {
|
||||
if (typeof value !== 'string') invalid(`${name} must be text`);
|
||||
return value;
|
||||
}
|
||||
function scopeArg(value, allowed = SCOPES) {
|
||||
if (value === undefined) return 'canon';
|
||||
if (!allowed.includes(value)) invalid(`scope must be one of ${allowed.join(", ")}`);
|
||||
return value;
|
||||
}
|
||||
function positionArg(value, name, fallback) {
|
||||
if (value === undefined) return fallback;
|
||||
if (!Number.isSafeInteger(value) || value < 1) invalid(`${name} must be a line number of 1 or more`);
|
||||
return value;
|
||||
}
|
||||
function offsetArg(value) {
|
||||
if (value === undefined) return 0;
|
||||
if (!Number.isSafeInteger(value) || value < 0) invalid('offset must be a whole number of 0 or more');
|
||||
return value;
|
||||
}
|
||||
function universeArg(mapping, value) {
|
||||
if (typeof value !== 'string' || !value) invalid('universe is required');
|
||||
if (!Object.hasOwn(mapping.universes, value)) raise('query-universe-unknown', `this library has no universe ${JSON.stringify(value)}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Whether universe U may see a file. Its own files, in either scope. A hybrid also sees the canon of
|
||||
// its declared sources, minus the folders its exclude constraints name. A hybrid never sees a source's
|
||||
// playground. Pin constraints do not hide anything; they are reported by universeInfo only.
|
||||
export function sees(mapping, universe, file) {
|
||||
if (file.universe === universe) return true;
|
||||
const entry = mapping.universes[universe];
|
||||
if (entry.kind !== 'hybrid' || !entry.sources.includes(file.universe) || file.scope !== 'canon') return false;
|
||||
return !entry.constraints.some(c => c.exclude !== undefined && inside(c.exclude, file.path));
|
||||
}
|
||||
|
||||
// The files one universe may see in one scope, with optional book and type filters, in path order.
|
||||
async function visible(source, universe, { scope, book, type }) {
|
||||
const index = await indexFor(source);
|
||||
const files = index.paths
|
||||
.map(path => index.files.get(path))
|
||||
.filter(file => file.scope === scope && sees(index.mapping, universe, file)
|
||||
&& (book === undefined || file.book === book) && (type === undefined || file.type === type));
|
||||
return { index, files };
|
||||
}
|
||||
|
||||
// A reply whose size is over the cap is refused, never cut.
|
||||
function checked(reply) {
|
||||
if (bytes(reply) > QUERY_REPLY_CAP) raise('query-item-too-large', `the reply is over the ${QUERY_REPLY_CAP}-character cap`);
|
||||
return reply;
|
||||
}
|
||||
|
||||
// Picks items from `offset`, at most `limit`, while the reply still fits. build(picked, next) returns
|
||||
// the reply; `next` is the position of the first item not picked, or null when none is left. An item
|
||||
// that cannot fit even alone is refused: skipping it would hide it, and cutting it would break it.
|
||||
function fitPage(items, { offset = 0, limit = LOOKUP_LIMIT, build }) {
|
||||
const picked = [];
|
||||
let at = offset;
|
||||
while (at < items.length && picked.length < limit) {
|
||||
// The size check uses items.length for `next`, the widest number a cursor can take, so a page never runs over.
|
||||
if (bytes(build([...picked, items[at]], items.length)) > QUERY_REPLY_CAP) {
|
||||
if (!picked.length) raise('query-item-too-large', `the item at position ${at} alone is over the ${QUERY_REPLY_CAP}-character cap`);
|
||||
break;
|
||||
}
|
||||
picked.push(items[at]);
|
||||
at += 1;
|
||||
}
|
||||
return checked(build(picked, at < items.length ? at : null));
|
||||
}
|
||||
|
||||
// find: the files whose name or file name holds `query`, case-insensitive. Up to 50 per page.
|
||||
export async function find(source, { universe: name, query, type, book, scope, offset } = {}) {
|
||||
const universe = universeArg(source.mapping, name);
|
||||
const needle = textArg(query, 'query').toLowerCase();
|
||||
if (type !== undefined && !TYPES.includes(type)) invalid(`type must be one of ${TYPES.join(', ')}`);
|
||||
const { index, files } = await visible(source, universe, {
|
||||
scope: scopeArg(scope), book: book === undefined ? undefined : textArg(book, 'book'), type,
|
||||
});
|
||||
const hits = files.filter(file => file.name.toLowerCase().includes(needle) || file.stem.toLowerCase().includes(needle));
|
||||
return fitPage(hits, {
|
||||
offset: offsetArg(offset),
|
||||
build: (picked, next) => ({
|
||||
commit: index.commit,
|
||||
results: picked.map(file => ({
|
||||
path: file.path, type: file.type, name: file.name, appears_in: file.frontmatter?.appears_in ?? null,
|
||||
scope: file.scope, universe: file.universe, book: file.book,
|
||||
})),
|
||||
next,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// get: one file in the requested scope (canon default), its served-tree blob, front matter,
|
||||
// lines and visible links both ways. Links keep all visible scopes, ordered supporting/playground/canon.
|
||||
// Returns null when the file is missing or not visible, so a caller cannot tell the two apart. Lines page
|
||||
// by `from` (reply `next`), links page together by `links_from` (reply `links_next`); `links_out` come
|
||||
// before `links_in`. Every page carries the front matter. Links take at most half the cap, so a hub note
|
||||
// with many backlinks stays readable and still leaves room for lines. A page with lines left carries at
|
||||
// least one line, so links shrink when a line needs the room; read on with `from: next` and
|
||||
// `links_from: links_next`. A `from` past the last line returns links alone. Only front matter, or a
|
||||
// line that cannot fit with no links beside it, is refused.
|
||||
export async function get(source, { universe: name, path, scope, from, links_from: linksFrom } = {}) {
|
||||
const universe = universeArg(source.mapping, name);
|
||||
const requestedScope = scopeArg(scope);
|
||||
const startLine = positionArg(from, 'from', 1);
|
||||
const linkStart = offsetArg(linksFrom);
|
||||
if (typeof path !== 'string') invalid('path must be text');
|
||||
const index = await indexFor(source);
|
||||
const file = index.files.get(path);
|
||||
if (!file || file.scope !== requestedScope || !sees(index.mapping, universe, file)) return null;
|
||||
const reachable = links => links.filter(link => {
|
||||
const other = index.files.get(link.path);
|
||||
return other !== undefined && sees(index.mapping, universe, other);
|
||||
}).sort((a, b) => SCOPE_RANK[index.files.get(a.path).scope] - SCOPE_RANK[index.files.get(b.path).scope]);
|
||||
const outs = reachable(index.linksOut.get(file.path) ?? []);
|
||||
const ins = reachable(index.linksIn.get(file.path) ?? []);
|
||||
const links = [...outs.map(link => ({ out: true, link })), ...ins.map(link => ({ out: false, link }))];
|
||||
// The reader/index does not keep tree oids. Resolve only in this immutable served
|
||||
// snapshot, never HEAD; listTree already checks/bounds the tree and its object IDs.
|
||||
const entry = (await listTree(source.repo, index.commit)).find(entry => entry.path === file.path);
|
||||
if (!entry || entry.mode !== '100644' || entry.type !== 'blob') raise('library-commit', 'indexed file is absent from the served tree');
|
||||
const blob = entry.oid;
|
||||
const lines = file.lines.slice(startLine - 1);
|
||||
const linkRest = Math.max(0, links.length - linkStart);
|
||||
const page = (nLines, nLinks) => {
|
||||
const taken = links.slice(linkStart, linkStart + nLinks);
|
||||
return {
|
||||
path: file.path, commit: index.commit, blob, scope: file.scope, frontmatter: file.frontmatter,
|
||||
lines: lines.slice(0, nLines),
|
||||
links_out: taken.filter(item => item.out).map(item => item.link),
|
||||
links_in: taken.filter(item => !item.out).map(item => item.link),
|
||||
next: nLines < lines.length ? startLine + nLines : null,
|
||||
links_next: nLinks < linkRest ? linkStart + nLinks : null,
|
||||
};
|
||||
};
|
||||
// How many leading items fit in `room`, by each item's own size.
|
||||
const count = (items, room) => {
|
||||
let used = 0, n = 0;
|
||||
while (n < items.length && used + bytes(items[n]) + 1 <= room) used += bytes(items[n++]) + 1;
|
||||
return n;
|
||||
};
|
||||
const base = bytes(page(0, 0));
|
||||
if (base > QUERY_REPLY_CAP) raise('query-item-too-large', `the front matter of ${file.path} alone is over the ${QUERY_REPLY_CAP}-character cap`);
|
||||
if (lines.length && bytes(page(1, 0)) > QUERY_REPLY_CAP) {
|
||||
raise('query-item-too-large', `line ${lines[0][0]} of ${file.path} alone is over the ${QUERY_REPLY_CAP}-character cap`);
|
||||
}
|
||||
const rest = links.slice(linkStart);
|
||||
let nLinks = count(rest, (lines.length ? Math.floor(QUERY_REPLY_CAP / 2) : QUERY_REPLY_CAP) - base);
|
||||
if (!nLinks && rest.length) nLinks = count(rest, QUERY_REPLY_CAP - base);
|
||||
if (!nLinks && rest.length) raise('query-item-too-large', `a link of ${file.path} alone is over the ${QUERY_REPLY_CAP}-character cap`);
|
||||
while (lines.length && nLinks > 0 && bytes(page(1, nLinks)) > QUERY_REPLY_CAP) nLinks = Math.floor(nLinks / 2);
|
||||
let nLines = Math.max(lines.length ? 1 : 0, count(lines, QUERY_REPLY_CAP - bytes(page(0, nLinks))));
|
||||
while (nLines > 1 && bytes(page(nLines, nLinks)) > QUERY_REPLY_CAP) nLines -= 1;
|
||||
return checked(page(nLines, nLinks));
|
||||
}
|
||||
|
||||
// search: lines whose text holds `text`, case-insensitive. The excerpt is the whole line. Up to 50 per page.
|
||||
export async function search(source, { universe: name, text, book, scope, offset } = {}) {
|
||||
const universe = universeArg(source.mapping, name);
|
||||
const needle = textArg(text, 'text').toLowerCase();
|
||||
if (!needle) invalid('text must not be empty');
|
||||
const { index, files } = await visible(source, universe, {
|
||||
scope: scopeArg(scope), book: book === undefined ? undefined : textArg(book, 'book'),
|
||||
});
|
||||
const hits = [];
|
||||
for (const file of files) {
|
||||
for (const [line, body] of file.lines) {
|
||||
if (!body.toLowerCase().includes(needle)) continue;
|
||||
const hit = { path: file.path, line, excerpt: body, scope: file.scope };
|
||||
// The excerpt is the whole line. A line too long to fit alone is marked, not cut and not refused.
|
||||
hits.push(bytes({ commit: index.commit, results: [hit], next: Number.MAX_SAFE_INTEGER }) > QUERY_REPLY_CAP
|
||||
? { path: file.path, line, excerpt: null, excerpt_too_long: true, scope: file.scope } : hit);
|
||||
}
|
||||
}
|
||||
return fitPage(hits, {
|
||||
offset: offsetArg(offset),
|
||||
build: (picked, next) => ({ commit: index.commit, results: picked, next }),
|
||||
});
|
||||
}
|
||||
|
||||
// manuscript: one chapter file of a book, by its file name, up to 400 lines per call from `from` to `to`.
|
||||
// Without a chapter it returns the book's first chapter file in path order. Every reply carries
|
||||
// `next_chapter`, the next chapter file's name in path order or null. A name that matches two visible
|
||||
// files is refused. Files outside every book (book null) are never chapters of a book.
|
||||
export async function manuscript(source, { universe: name, book, scope, chapter, from, to } = {}) {
|
||||
const universe = universeArg(source.mapping, name);
|
||||
const bookName = textArg(book, 'book');
|
||||
const chapterName = chapter === undefined ? undefined : textArg(chapter, 'chapter');
|
||||
const start = positionArg(from, 'from', 1);
|
||||
const end = positionArg(to, 'to', Infinity);
|
||||
if (end < start) invalid('to must not be before from');
|
||||
const { index, files } = await visible(source, universe, { scope: scopeArg(scope, MANUSCRIPT_SCOPES), book: bookName, type: 'manuscript' });
|
||||
let at = 0;
|
||||
if (chapterName !== undefined) {
|
||||
const matches = files.filter(file => file.stem === chapterName);
|
||||
if (matches.length > 1) raise('query-chapter-ambiguous', `${matches.length} visible files are named ${JSON.stringify(chapterName)}`);
|
||||
if (!matches.length) return null;
|
||||
at = files.indexOf(matches[0]);
|
||||
}
|
||||
const file = files[at];
|
||||
if (!file) return null;
|
||||
const nextChapter = files[at + 1]?.stem ?? null;
|
||||
return fitPage(file.lines.slice(start - 1, end), {
|
||||
limit: MANUSCRIPT_LINES,
|
||||
build: (picked, next) => ({
|
||||
path: file.path, commit: index.commit, scope: file.scope, lines: picked,
|
||||
next: next === null ? null : start + next, next_chapter: nextChapter,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// universeInfo: a universe's kind, name, books, playground prefixes, type folders and manuscript folders.
|
||||
// A hybrid adds its sources, constraints and notes. A draft has no books and no canon: its kind, name
|
||||
// and prefixes are the whole answer beside the mapping's types. It reads only the checked mapping and the
|
||||
// served commit.
|
||||
export async function universeInfo(source, { universe: name } = {}) {
|
||||
const universe = universeArg(source.mapping, name);
|
||||
const entry = source.mapping.universes[universe];
|
||||
const hybrid = entry.kind === 'hybrid';
|
||||
return checked({
|
||||
commit: source.commit, universe, name: entry.name, kind: entry.kind ?? 'universe',
|
||||
books: entry.books, playground: entry.playground, types: source.mapping.types, manuscript: source.mapping.manuscript,
|
||||
...(hybrid ? {
|
||||
sources: entry.sources, constraints: entry.constraints,
|
||||
...(entry.notes === undefined ? {} : { notes: entry.notes }),
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
@@ -1,72 +1,59 @@
|
||||
// renderReply(body): how a stored reply looks (design 7.4), shared by the Console chat (C2-10b) and the
|
||||
// Discord relay (C2-18). It replaces C2-01's items renderer. Pure: no git, no clock, no DOM. The caller
|
||||
// passes what only the bus knows: the stored check result (C2-06) and whether a cited file changed.
|
||||
// The guard checks a reply before it is stored. This only decides how a body reads; it trusts nothing
|
||||
// in the body for a scope or the agent-written label.
|
||||
export const SCOPES = Object.freeze(['canon', 'playground', 'supporting', 'web']);
|
||||
export const NOTICE_OPS = Object.freeze({ write: 'wrote', delete: 'deleted' });
|
||||
// renderItems(body): how a stored cited reply looks (design 7.4), shared by the Console chat (C2-10b)
|
||||
// and the Discord relay (C2-18). Pure: no git, no clock. The caller passes what it knows about cites.
|
||||
// The guard (C2-06) checks limits before a reply is stored. This only refuses to show a body that
|
||||
// doesn't have the reply shape, and then it shows a fixed line, never the body's text.
|
||||
export const KINDS = Object.freeze({ fact: 'Fact', finding: 'Finding', correction: 'Correction', question: 'Question', idea: 'Idea' });
|
||||
export const NOTICES = Object.freeze({ 'no-cited-answer': 'No cited answer', 'no-answer': 'No answer', 'turn-failed': 'The turn failed' });
|
||||
export const UNREADABLE = 'This reply could not be shown.';
|
||||
const SHA = /^[0-9a-f]{40}$/;
|
||||
|
||||
const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
const line = n => Number.isSafeInteger(n) && n > 0;
|
||||
const only = (value, keys) => Object.keys(value).every(k => keys.includes(k));
|
||||
const quoted = text => JSON.stringify(text);
|
||||
// webby-0041 N5: only an https url is ever a link. Anything else, javascript: and http: included, is text.
|
||||
function https(url) {
|
||||
try { return new URL(url).protocol === 'https:' ? url : null; } catch { return null; }
|
||||
}
|
||||
|
||||
// The design 5.2 cite shapes. null when the value isn't one, and then the body is not a cited reply.
|
||||
function cite(value) {
|
||||
function cite(value, { scopeOf, changed }) {
|
||||
if (!plain(value) || typeof value.quote !== 'string') return null;
|
||||
if (Object.hasOwn(value, 'url')) return typeof value.url === 'string' && only(value, ['url', 'quote']) ? { url: value.url, quote: value.quote } : null;
|
||||
if (Object.hasOwn(value, 'url')) {
|
||||
if (typeof value.url !== 'string' || Object.hasOwn(value, 'path')) return null;
|
||||
return { badge: 'web', url: value.url, ref: value.url, quote: value.quote, changed: false };
|
||||
}
|
||||
const { path, commit, lines } = value;
|
||||
if (!only(value, ['path', 'commit', 'lines', 'quote', 'as']) || typeof path !== 'string' || !SHA.test(commit ?? '')
|
||||
|| !Array.isArray(lines) || lines.length !== 2 || !line(lines[0]) || !line(lines[1]) || lines[1] < lines[0]
|
||||
|| (Object.hasOwn(value, 'as') && value.as !== 'canon')) return null;
|
||||
return { path, commit, lines: [...lines], quote: value.quote, canon: value.as === 'canon' };
|
||||
if (typeof path !== 'string' || !SHA.test(commit ?? '') || !Array.isArray(lines) || lines.length !== 2
|
||||
|| !line(lines[0]) || !line(lines[1]) || lines[1] < lines[0]) return null;
|
||||
const badge = scopeOf?.(path);
|
||||
const ref = `${path}:${lines[0] === lines[1] ? lines[0] : `${lines[0]}-${lines[1]}`} @${commit.slice(0, 7)}`;
|
||||
return { badge: badge === 'canon' || badge === 'playground' ? badge : null, path, commit, lines: [...lines], ref,
|
||||
quote: value.quote, changed: changed?.({ path, commit }) === true };
|
||||
}
|
||||
const quoted = text => JSON.stringify(text);
|
||||
|
||||
// check: the stored entry for this cite, { scope, agent }, or undefined. A path cite takes its badge only
|
||||
// from it; a url cite is web by its shape. agent is the Mosaic-Agent trailer value, never the body's.
|
||||
function shown(c, check, changed, i) {
|
||||
if (c.url !== undefined) return { badge: 'web', url: c.url, link: https(c.url), ref: c.url, quote: c.quote, agent: null, changed: false };
|
||||
const scope = plain(check) && SCOPES.includes(check.scope) && check.scope !== 'web' ? check.scope : null;
|
||||
const agent = plain(check) && typeof check.agent === 'string' && check.agent !== '' ? check.agent : null;
|
||||
const ref = `${c.path}:${c.lines[0] === c.lines[1] ? c.lines[0] : `${c.lines[0]}-${c.lines[1]}`} @${c.commit.slice(0, 7)}`;
|
||||
return { badge: scope, path: c.path, commit: c.commit, lines: c.lines, ref, quote: c.quote, canon: c.canon, agent,
|
||||
changed: changed?.({ path: c.path, commit: c.commit }, i) === true };
|
||||
}
|
||||
|
||||
// The broker's write notice (design, writes): {"notice": "library-write", op, path, commit}, no agent text.
|
||||
function notice(value, name) {
|
||||
if (!plain(value) || value.notice !== 'library-write' || !only(value, ['notice', 'op', 'path', 'commit'])
|
||||
|| !Object.hasOwn(NOTICE_OPS, value.op) || typeof value.path !== 'string' || !SHA.test(value.commit ?? '')) return null;
|
||||
const who = typeof name === 'string' && name !== '' ? name : 'An agent';
|
||||
return { kind: 'notice', op: value.op, path: value.path, commit: value.commit,
|
||||
text: `${who} ${NOTICE_OPS[value.op]} ${value.path} @${value.commit.slice(0, 7)}` };
|
||||
}
|
||||
|
||||
// body: the stored message body, a string (or an already parsed object).
|
||||
// options.check: the stored check result, { citations: [{ scope, agent }] } in body order, or null.
|
||||
// options.changed({path, commit}, index) -> true when the file's last commit is newer than the cited one.
|
||||
// options.name: the sender's name for a notice line.
|
||||
// Returns { kind: 'text', text, plain } | { kind: 'reply', text, citations, plain } | { kind: 'notice', op, path, commit, text, plain }.
|
||||
// plain is the whole rendering as text, for a channel that shows no markup.
|
||||
export function renderReply(body, options = {}) {
|
||||
const raw = typeof body === 'string' ? body : JSON.stringify(body) ?? String(body);
|
||||
// body: the stored message body (a JSON string or the parsed object).
|
||||
// options.scopeOf(path) -> 'canon' | 'playground' | undefined, for the scope badge.
|
||||
// options.changed({path, commit}) -> true when the file's last commit is newer than the cited one.
|
||||
// Returns { kind: 'items', items, text } | { kind: 'notice', notice, text } | { kind: 'unreadable', text }.
|
||||
export function renderItems(body, options = {}) {
|
||||
const unreadable = { kind: 'unreadable', text: UNREADABLE };
|
||||
let value = body;
|
||||
if (typeof body === 'string') { try { value = JSON.parse(body); } catch { value = undefined; } }
|
||||
const n = notice(value, options.name);
|
||||
if (n) return { ...n, plain: n.text };
|
||||
// Design 5.2: a JSON object must be {text, citations} and nothing else. Any other body is a reply whose
|
||||
// text is the whole body and which carries no citation.
|
||||
const free = { kind: 'text', text: raw, plain: raw };
|
||||
if (!plain(value) || typeof value.text !== 'string' || !Array.isArray(value.citations) || !only(value, ['text', 'citations'])) return free;
|
||||
const cites = value.citations.map(cite);
|
||||
if (cites.some(c => c === null)) return free;
|
||||
const checks = plain(options.check) && Array.isArray(options.check.citations) ? options.check.citations : [];
|
||||
const citations = cites.map((c, i) => shown(c, checks[i], options.changed, i));
|
||||
const lines = citations.map(c => ` ${c.badge ? `[${c.badge}] ` : ''}${c.ref} ${quoted(c.quote)}${c.agent ? ` (agent-written: ${c.agent})` : ''}${c.changed ? ' (changed since)' : ''}`);
|
||||
return { kind: 'reply', text: value.text, citations, plain: [value.text, ...lines].join('\n') };
|
||||
if (typeof body === 'string') { try { value = JSON.parse(body); } catch { return unreadable; } }
|
||||
if (!plain(value) || !Array.isArray(value.items)) return unreadable;
|
||||
const keys = Object.keys(value);
|
||||
if (value.items.length === 0) {
|
||||
if (keys.length !== 2 || !Object.hasOwn(NOTICES, value.notice)) return unreadable;
|
||||
return { kind: 'notice', notice: value.notice, text: NOTICES[value.notice] };
|
||||
}
|
||||
if (keys.length !== 1) return unreadable;
|
||||
const items = [];
|
||||
for (const item of value.items) {
|
||||
if (!plain(item) || !Object.hasOwn(KINDS, item.kind) || typeof item.text !== 'string' || !Array.isArray(item.cites)) return unreadable;
|
||||
const correction = item.kind === 'correction';
|
||||
if (correction && (typeof item.from !== 'string' || typeof item.to !== 'string')) return unreadable;
|
||||
const cites = item.cites.map(c => cite(c, options));
|
||||
if (cites.some(c => c === null)) return unreadable;
|
||||
items.push({ kind: item.kind, label: KINDS[item.kind], text: item.text,
|
||||
...(correction ? { from: item.from, to: item.to } : {}), cites });
|
||||
}
|
||||
const text = items.map((item, i) => [
|
||||
`${i + 1}. ${item.label}: ${item.text}`,
|
||||
...(item.kind === 'correction' ? [` From: ${quoted(item.from)}`, ` To: ${quoted(item.to)}`] : []),
|
||||
...item.cites.map(c => ` ${c.badge ? `[${c.badge}] ` : ''}${c.ref} ${quoted(c.quote)}${c.changed ? ' (changed since)' : ''}`),
|
||||
].join('\n')).join('\n');
|
||||
return { kind: 'items', items, text };
|
||||
}
|
||||
@@ -2,16 +2,22 @@
|
||||
// Paths are repo-relative and compared exactly, case included (R7 handles case-only twins).
|
||||
const inside = (folder, path) => path.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
|
||||
|
||||
// { scope: 'playground' | 'canon', universe, book, type, folder } or { scope: 'unmapped' }.
|
||||
// Prefixes come first (ruling after r11, C2-01c): a `.md` file under any entry's playground prefix is
|
||||
// playground, with that entry as `universe` and the book folder that holds the prefix as `book`, or
|
||||
// null at universe level and in a draft. Otherwise a `.md` file in a book folder is canon. `type` is
|
||||
// the mapped type of the first folder under the prefix (playground) or under the book (canon),
|
||||
// { scope: 'supporting' | 'playground' | 'canon', universe, book, type, folder } or { scope: 'unmapped' }.
|
||||
// Prefixes come first (ruling after r11, C2-01c), supporting before playground (C2-01d, design 1.2):
|
||||
// a `.md` file under any entry's supporting prefix is supporting, and otherwise one under a playground
|
||||
// prefix is playground. Either way `universe` is that entry and `book` is the book folder that holds
|
||||
// the prefix, or null at universe level and in a draft. Otherwise a `.md` file in a book folder is
|
||||
// canon. `type` is the mapped type of the first folder under the prefix or under the book (canon),
|
||||
// 'manuscript' for a manuscript folder, else null. `folder` is that folder name. checkMapping keeps
|
||||
// prefixes from overlapping, so at most one matches.
|
||||
// prefixes of one list from overlapping, and keeps a supporting prefix from holding a playground
|
||||
// prefix, so the first match is the only one that counts.
|
||||
export function classify(mapping, path) {
|
||||
if (typeof path !== 'string' || !path.endsWith('.md')) return { scope: 'unmapped' };
|
||||
const entries = Object.entries(mapping.universes);
|
||||
for (const [universe, entry] of entries) {
|
||||
const prefix = entry.supporting.find(p => inside(p, path));
|
||||
if (prefix) return place(mapping, path, prefix, { scope: 'supporting', universe, book: entry.books.find(b => inside(b, prefix)) ?? null });
|
||||
}
|
||||
for (const [universe, entry] of entries) {
|
||||
const prefix = entry.playground.find(p => inside(p, path));
|
||||
if (prefix) return place(mapping, path, prefix, { scope: 'playground', universe, book: entry.books.find(b => inside(b, prefix)) ?? null });
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// C2-01d (M12): the paths an agent may write. A path passes only under a supporting prefix of the
|
||||
// named universe, and only if nothing in the tree could turn the write into something else: a
|
||||
// symlink or gitlink on the way, or a name that differs from a tree name only in case or Unicode
|
||||
// form (APFS folds both, so her checkout would put the file somewhere else). Pure; reads no git.
|
||||
import { LibraryError } from './problems.mjs';
|
||||
import { classify } from './scope.mjs';
|
||||
|
||||
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
||||
const MAX_BYTES = 1024;
|
||||
const LINKS = new Set(['120000', '160000']);
|
||||
const fold = name => name.normalize('NFC').toLowerCase();
|
||||
|
||||
// `tree` is the array listTree returns ({ mode, type, oid, size, path }). Returns `path` or throws
|
||||
// library-write-story-path naming the class. Checks run in a fixed order, so a path with two faults
|
||||
// always names the same one.
|
||||
export function supportingPath(mapping, universe, path, tree) {
|
||||
if (!Array.isArray(tree)) throw new TypeError('supportingPath needs the listTree array');
|
||||
const refuse = message => { throw new LibraryError('library-write-story-path', message); };
|
||||
if (typeof path !== 'string') refuse('the path must be a string');
|
||||
const at = JSON.stringify(path);
|
||||
if (Buffer.byteLength(path, 'utf8') > MAX_BYTES) refuse(`${at} is longer than ${MAX_BYTES} bytes`);
|
||||
if (CONTROL.test(path)) refuse('the path holds a control character');
|
||||
if (path.includes('\\')) refuse(`${at} holds a backslash`);
|
||||
if (path.startsWith('/')) refuse(`${at} is absolute`);
|
||||
const parts = path.split('/');
|
||||
if (parts.some(part => part === '' || part === '.' || part === '..')) refuse(`${at} has an empty, . or .. segment`);
|
||||
const place = classify(mapping, path);
|
||||
if (place.scope !== 'supporting' || place.universe !== universe)
|
||||
refuse(`${at} is not under a supporting folder of universe ${JSON.stringify(universe)}`);
|
||||
const own = parts.map((_, i) => parts.slice(0, i + 1).join('/'));
|
||||
const modes = new Map(tree.map(entry => [entry.path, entry.mode]));
|
||||
for (const p of own) if (LINKS.has(modes.get(p))) refuse(`${at} goes through the symlink or gitlink ${JSON.stringify(p)}`);
|
||||
const names = new Map();
|
||||
for (const entry of tree) {
|
||||
const segs = entry.path.split('/');
|
||||
for (let i = 1; i <= segs.length; i++) {
|
||||
const name = segs.slice(0, i).join('/');
|
||||
const key = fold(name);
|
||||
if (!names.has(key)) names.set(key, new Set());
|
||||
names.get(key).add(name);
|
||||
}
|
||||
}
|
||||
for (const p of own) {
|
||||
const seen = names.get(fold(p));
|
||||
if (seen && [...seen].some(name => name !== p))
|
||||
refuse(`${at} differs only in case or Unicode form from ${JSON.stringify([...seen].find(name => name !== p))}`);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export async function survey({ repo, commit, mapping }) {
|
||||
forms, attachments, fields,
|
||||
};
|
||||
if (mapping) {
|
||||
const scopes = { canon: 0, playground: 0, unmapped: 0 };
|
||||
const scopes = { canon: 0, playground: 0, supporting: 0, unmapped: 0 };
|
||||
for (const entry of entries) scopes[classify(mapping, entry.path).scope]++;
|
||||
const library = await readLibrary({ repo, commit: head, mapping });
|
||||
const steps = Object.fromEntries(STEPS.map(s => [s, 0]));
|
||||
@@ -89,7 +89,7 @@ export function formatSurvey(s) {
|
||||
out.push(`body links ${FORMS.map(f => `${f} ${s.forms[f]}`).join(', ')}; attachment ${s.attachments}`);
|
||||
for (const [field, c] of Object.entries(s.fields)) out.push(`field ${field} wikilink ${c.wikilink}, name ${c.name}`);
|
||||
if (s.scopes) {
|
||||
out.push(`scopes canon ${s.scopes.canon}, playground ${s.scopes.playground}, unmapped ${s.scopes.unmapped}; indexed ${s.indexed}`);
|
||||
out.push(`scopes canon ${s.scopes.canon}, playground ${s.scopes.playground}, supporting ${s.scopes.supporting}, unmapped ${s.scopes.unmapped}; indexed ${s.indexed}`);
|
||||
out.push(`resolved ${STEPS.map(st => `${st} ${s.steps[st]}`).join(', ')}, unresolved ${s.steps.unresolved}`);
|
||||
out.push(`problems ${Object.entries(s.problems).map(([code, n]) => `${code} ${n}`).join(', ')}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Index tests (C2-05a): the cache key and its bound, failed reads, mapped-only contents, and the rebuild
|
||||
// time on a 3,000-file fixture. Fixtures are made up and built under the scratch area.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { clearIndexCache, indexCacheSize, indexFor } from '../src/index-build.mjs';
|
||||
import { git, mapping, MARKER } from './helpers.mjs';
|
||||
import { note, scratchRepo } from './query-fixture.mjs';
|
||||
|
||||
const FILES = {
|
||||
'Book One/Characters/Mara Vell.md': note('character', 'Mara Vell', MARKER),
|
||||
'Book One/Chapters/Chapter 01.md': '# One\n[[Mara Vell]]\n',
|
||||
'Inbox Note.md': `${MARKER}\n`,
|
||||
};
|
||||
|
||||
// A mapping whose harbor universe has the given display name. Each name gives a new cache key.
|
||||
const named = name => mapping(e => ({ ...e, universes: { ...e.universes, harbor: { ...e.universes.harbor, name } } }));
|
||||
|
||||
test('the same commit, repo path and mapping return the same index, and only mapped files are in it', async t => {
|
||||
clearIndexCache();
|
||||
const { repo, commit } = scratchRepo(t, FILES);
|
||||
const a = await indexFor({ repo, commit, mapping: mapping() });
|
||||
const b = await indexFor({ repo, commit, mapping: mapping() });
|
||||
assert.equal(a, b);
|
||||
assert.equal(indexCacheSize(), 1);
|
||||
assert.equal(a.commit, commit);
|
||||
assert.ok(a.files.has('Book One/Characters/Mara Vell.md'));
|
||||
assert.ok(!a.files.has('Inbox Note.md'));
|
||||
assert.deepEqual(a.names.get('mara vell'), ['Book One/Characters/Mara Vell.md']);
|
||||
assert.deepEqual(a.linksIn.get('Book One/Characters/Mara Vell.md').map(l => l.path), ['Book One/Chapters/Chapter 01.md']);
|
||||
});
|
||||
|
||||
test('a different repo path, mapping or commit gets its own index', async t => {
|
||||
clearIndexCache();
|
||||
const { root, repo, commit } = scratchRepo(t, FILES);
|
||||
const base = await indexFor({ repo, commit, mapping: mapping() });
|
||||
const alias = join(root, 'alias');
|
||||
symlinkSync(repo, alias);
|
||||
assert.notEqual(await indexFor({ repo: alias, commit, mapping: mapping() }), base);
|
||||
assert.notEqual(await indexFor({ repo, commit, mapping: named('Renamed Harbor') }), base);
|
||||
writeFileSync(join(repo, 'Book One/Places/New.md'), note('place', 'New'));
|
||||
git(repo, 'add', '-A');
|
||||
git(repo, 'commit', '-qm', 'second');
|
||||
const next = git(repo, 'rev-parse', 'HEAD');
|
||||
const later = await indexFor({ repo, commit: next, mapping: mapping() });
|
||||
assert.notEqual(later, base);
|
||||
assert.equal(later.commit, next);
|
||||
assert.ok(later.files.has('Book One/Places/New.md'));
|
||||
});
|
||||
|
||||
test('the cache holds at most eight indexes and evicts the least recently used', async t => {
|
||||
clearIndexCache();
|
||||
const { repo, commit } = scratchRepo(t, FILES);
|
||||
const maps = Array.from({ length: 9 }, (_, i) => named(`Harbor ${i}`));
|
||||
const first = await indexFor({ repo, commit, mapping: maps[0] });
|
||||
for (const m of maps.slice(1)) await indexFor({ repo, commit, mapping: m });
|
||||
assert.equal(indexCacheSize(), 8);
|
||||
assert.notEqual(await indexFor({ repo, commit, mapping: maps[0] }), first);
|
||||
assert.equal(indexCacheSize(), 8);
|
||||
});
|
||||
|
||||
test('a failed read is not kept in the cache', async t => {
|
||||
clearIndexCache();
|
||||
const { repo } = scratchRepo(t, FILES);
|
||||
await assert.rejects(indexFor({ repo, commit: '0'.repeat(40), mapping: mapping() }));
|
||||
assert.equal(indexCacheSize(), 0);
|
||||
});
|
||||
|
||||
test('a missing or unchecked mapping is refused before any read', async t => {
|
||||
clearIndexCache();
|
||||
const { repo, commit } = scratchRepo(t, FILES);
|
||||
await assert.rejects(indexFor({ repo, commit, mapping: undefined }), { code: 'library-mapping-invalid' });
|
||||
await assert.rejects(indexFor({ repo, commit, mapping: { kind: 'hybrid' } }), { code: 'library-mapping-invalid' });
|
||||
});
|
||||
|
||||
test('a 3,000-file rebuild completes and its time is reported', { timeout: 300_000 }, async t => {
|
||||
clearIndexCache();
|
||||
const folders = ['Characters', 'Places', 'Research'];
|
||||
const files = {};
|
||||
for (let i = 0; i < 3000; i++) {
|
||||
files[`Book One/${folders[i % 3]}/Note ${i}.md`] = note('character', `Note ${i}`, `Body ${i} [[Note ${(i + 1) % 3000}]]`);
|
||||
}
|
||||
const { repo, commit } = scratchRepo(t, files);
|
||||
const started = performance.now();
|
||||
const index = await indexFor({ repo, commit, mapping: mapping() });
|
||||
const ms = performance.now() - started;
|
||||
t.diagnostic(`3,000-file index rebuild: ${ms.toFixed(0)} ms`);
|
||||
assert.equal(index.paths.length, 3000);
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { checkMapping, classify, LibraryError } from '../src/index.mjs';
|
||||
import { mapping, mappingEntry } from './helpers.mjs';
|
||||
|
||||
@@ -45,7 +49,7 @@ test('C2-01c: universe-level prefixes lie outside every book, in a plain univers
|
||||
|
||||
test('C2-01c: a draft takes kind, name and playground only, and comes back frozen with books []', () => {
|
||||
const m = mapping(e => { e.universes.idea = draft(); return e; });
|
||||
assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'] });
|
||||
assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'], supporting: [] });
|
||||
assert.ok(Object.isFrozen(m.universes.idea) && Object.isFrozen(m.universes.idea.books) && Object.isFrozen(m.universes.idea.playground));
|
||||
refuses(e => { e.universes.idea = { ...draft(), books: ['New World'] }; return e; }, 'library-mapping-invalid', /universes.idea is a draft and takes no books/);
|
||||
refuses(e => { e.universes.idea = { ...draft(), books: [] }; return e; }, 'library-mapping-invalid', /takes no books/);
|
||||
@@ -56,6 +60,107 @@ test('C2-01c: a draft takes kind, name and playground only, and comes back froze
|
||||
refuses(e => { e.universes.harbor.kind = kind; return e; }, 'library-mapping-invalid', /universes.harbor.kind must be "hybrid" or "draft"/);
|
||||
});
|
||||
|
||||
test('C2-01d: supporting prefixes at book level, universe level, in a hybrid and in a draft, [] when absent', () => {
|
||||
const m = mapping(e => {
|
||||
e.universes.harbor.supporting = ['Book One/Notes/', 'Harbor Notes/'];
|
||||
e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: [], supporting: ['Crossover Notes/'] };
|
||||
e.universes.idea = { ...draft(), supporting: ['Drafts/New World Notes/'] };
|
||||
return e;
|
||||
});
|
||||
assert.deepEqual(m.universes.harbor.supporting, ['Book One/Notes/', 'Harbor Notes/']);
|
||||
assert.deepEqual(m.universes.cross.supporting, ['Crossover Notes/']);
|
||||
assert.deepEqual(m.universes.idea.supporting, ['Drafts/New World Notes/']);
|
||||
assert.deepEqual(m.universes.second.supporting, []);
|
||||
assert.ok(Object.isFrozen(m.universes.harbor.supporting) && Object.isFrozen(m.universes.second.supporting));
|
||||
assert.deepEqual(mapping().universes.harbor.supporting, []);
|
||||
});
|
||||
|
||||
test('C2-01d: each supporting prefix rule refuses with its code', () => {
|
||||
const invalid = (change, pattern) => refuses(change, 'library-mapping-invalid', pattern);
|
||||
invalid(e => { e.universes.harbor.supporting = ['Book One/Notes']; return e; }, /universes.harbor.supporting holds "Book One\/Notes", which is not a folder path ending in \//);
|
||||
invalid(e => { e.universes.harbor.supporting = ['Book One/../Notes/']; return e; }, /which is not a folder path ending in \//);
|
||||
invalid(e => { e.universes.harbor.supporting = 'Book One/Notes/'; return e; }, /universes.harbor.supporting must be a list/);
|
||||
invalid(e => { e.universes.harbor.supporting = ['Book One/Notes/', 'Book One/Notes/Deep/']; return e; },
|
||||
/supporting "Book One\/Notes\/" holds supporting "Book One\/Notes\/Deep\/"/);
|
||||
invalid(e => { e.universes.harbor.supporting = ['Ideas/']; e.universes.idea = { ...draft(), supporting: ['Ideas/'] }; return e; },
|
||||
/a supporting prefix is listed twice/);
|
||||
invalid(e => { e.universes.harbor.supporting = ['Book One/']; return e; }, /universes.harbor.supporting "Book One\/" is the whole book folder "Book One"/);
|
||||
invalid(e => { e.universes.harbor.books = ['Shelf/Book One']; e.universes.harbor.playground = []; e.universes.harbor.supporting = ['Shelf/']; return e; },
|
||||
/universes.harbor.supporting "Shelf\/" holds its own book folder "Shelf\/Book One"/);
|
||||
// Supporting is checked first in scope, so a supporting prefix holding a playground would hide it.
|
||||
invalid(e => { e.universes.harbor.supporting = ['Book One/Workshop/']; e.universes.harbor.playground = ['Book One/Workshop/Playground/']; return e; },
|
||||
/universes.harbor.supporting "Book One\/Workshop\/" holds playground "Book One\/Workshop\/Playground\/"/);
|
||||
// Across entries any overlap would give one path two universes.
|
||||
for (const [supporting, playground] of [['Ideas/', 'Ideas/'], ['Ideas/Deep/', 'Ideas/'], ['Ideas/', 'Ideas/Deep/']])
|
||||
invalid(e => { e.universes.harbor.supporting = [supporting]; e.universes.idea = draft([playground]); return e; },
|
||||
new RegExp(`universes.harbor.supporting "${supporting}" overlaps playground "${playground}" of universes.idea`));
|
||||
refuses(e => { e.universes.harbor.supporting = ['Second World/Notes/']; return e; },
|
||||
'library-supporting-other-universe', /universes.harbor.supporting "Second World\/Notes\/" lies inside book folder "Second World" of universes.second/);
|
||||
refuses(e => { e.universes.second.books = ['Worlds/Second']; e.universes.harbor.supporting = ['Worlds/']; return e; },
|
||||
'library-supporting-other-universe', /universes.harbor.supporting "Worlds\/" holds book folder "Worlds\/Second" of universes.second/);
|
||||
refuses(e => { e.universes.idea = { ...draft(), supporting: ['Book One/Idea Notes/'] }; return e; },
|
||||
'library-supporting-other-universe', /universes.idea.supporting "Book One\/Idea Notes\/" lies inside book folder "Book One" of universes.harbor/);
|
||||
});
|
||||
|
||||
test('C2-01d: between the lists, a supporting prefix may equal its own playground, and a playground may hold its own supporting', () => {
|
||||
const same = mapping(e => { e.universes.harbor.supporting = ['Book One/Playground/']; return e; });
|
||||
assert.deepEqual([same.universes.harbor.playground, same.universes.harbor.supporting], [['Book One/Playground/'], ['Book One/Playground/']]);
|
||||
const nested = mapping(e => { e.universes.harbor.supporting = ['Book One/Playground/Notes/']; return e; });
|
||||
assert.deepEqual(nested.universes.harbor.supporting, ['Book One/Playground/Notes/']);
|
||||
});
|
||||
|
||||
test('C2-01d: editor is optional, kept when given, and refuses anything but a plain name and email', () => {
|
||||
assert.equal(Object.hasOwn(mapping(), 'editor'), false);
|
||||
const editor = { name: 'Her Name', email: '[email protected]' };
|
||||
const m = mapping(e => ({ ...e, editor }));
|
||||
assert.deepEqual(m.editor, editor);
|
||||
assert.notEqual(m.editor, editor);
|
||||
assert.ok(Object.isFrozen(m.editor));
|
||||
const invalid = (value, pattern) => refuses(e => ({ ...e, editor: value }), 'library-mapping-invalid', pattern);
|
||||
for (const value of [null, 'Her Name', 7, ['Her Name', '[email protected]']]) invalid(value, /editor must be an object/);
|
||||
invalid({ ...editor, signingKey: 'x' }, /editor has unknown key "signingKey"/);
|
||||
// sage-2925: an optional Gitea login for her save, checked as the entry's user is.
|
||||
assert.deepEqual(mapping(e => ({ ...e, editor: { ...editor, user: 'her.login-1' } })).editor, { ...editor, user: 'her.login-1' });
|
||||
for (const user of ['', 'her login', 'her@host', 'a'.repeat(101), 7, null])
|
||||
invalid({ ...editor, user }, /editor.user must be a plain account name/);
|
||||
invalid({ name: 'Her Name' }, /editor.email must be non-empty text with no control character, < or >/);
|
||||
invalid({ email: '[email protected]' }, /editor.name must be non-empty text/);
|
||||
for (const bad of ['', ' ', 7, null, 'a\u0000b', 'a\nb', 'a\u001fb', 'a\u007fb', 'a\u0085b', 'a\u009fb', '<her>', 'a<b', 'a>b']) {
|
||||
invalid({ ...editor, name: bad }, /editor.name must be non-empty text with no control character, < or >/);
|
||||
invalid({ ...editor, email: bad }, /editor.email must be non-empty text with no control character, < or >/);
|
||||
}
|
||||
// sage-3601, sage-3606: git's crud() strips whitespace and , : ; < > " \ ' from both ends of an
|
||||
// ident, so a value starting or ending with one refuses. Inner < > and controls refuse above.
|
||||
const ends = [' ', '\u00a0', '\u3000', ',', ':', ';', '"', "'", '\\'];
|
||||
for (const c of ends) for (const padded of [`${c}Her Name`, `Her Name${c}`]) {
|
||||
invalid({ ...editor, name: padded }, /editor.name must not start or end with whitespace or any of , : ; " ' \\/);
|
||||
invalid({ ...editor, email: padded }, /editor.email must not start or end with whitespace or any of , : ; " ' \\/);
|
||||
}
|
||||
for (const c of ['<', '>', '\n', '\t']) for (const padded of [`${c}Her Name`, `Her Name${c}`, `Her${c}Name`])
|
||||
invalid({ ...editor, name: padded }, /editor.name must be non-empty text with no control character, < or >/);
|
||||
assert.equal(mapping(e => ({ ...e, editor: { name: 'Her Middle Name', email: '[email protected]' } })).editor.name, 'Her Middle Name');
|
||||
for (const name of ['Doe, Jane Q.', 'Jane: "Q" Doe', "O'Neil", 'Back\\slash Name'])
|
||||
assert.equal(mapping(e => ({ ...e, editor: { name, email: '[email protected]' } })).editor.name, name);
|
||||
});
|
||||
|
||||
// sage-3606: an accepted editor commits under exactly the saved ident, as author and committer. A
|
||||
// future git that strips more from an ident fails here.
|
||||
test('C2-01d: an accepted editor round-trips through a real git commit unchanged', t => {
|
||||
const editor = mapping(e => ({ ...e, editor: { name: 'Doe, Jane Q.', email: '[email protected].' } })).editor;
|
||||
const root = mkdtempSync(join(tmpdir(), 'library-ident-'));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
// An ident in the environment would win over -c, so the commit runs without one. GIT_DIR and its
|
||||
// family would beat -C (inside a hook, say), and git reads no global or system config (webby-0236 E5).
|
||||
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_(AUTHOR|COMMITTER)_/.test(k)));
|
||||
for (const k of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete env[k];
|
||||
Object.assign(env, { GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1' });
|
||||
const git = (...args) => execFileSync('git', ['-c', `user.name=${editor.name}`, '-c', `user.email=${editor.email}`,
|
||||
'-c', 'commit.gpgsign=false', '-c', 'core.hooksPath=/dev/null', '-C', root, ...args], { encoding: 'utf8', env });
|
||||
git('init', '-q');
|
||||
git('commit', '-q', '--allow-empty', '-m', 'ident');
|
||||
assert.equal(git('log', '-1', '--format=%an%n%ae%n%cn%n%ce'), `${editor.name}\n${editor.email}\n${editor.name}\n${editor.email}\n`);
|
||||
});
|
||||
|
||||
test('no playground prefix holds another, and no book folder holds another', () => {
|
||||
refuses(e => { e.universes.harbor.playground = ['Book One/Playground/', 'Book One/Playground/Deep/']; return e; },
|
||||
'library-mapping-invalid', /holds playground/);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Scratch repos for the query and index tests (C2-05a). They are built under tmpdir(), as helpers.mjs
|
||||
// repo() does; the gate sets TMPDIR under /mnt/storage/scratch/tmp, never /tmp (RULINGS "tmpdir flag").
|
||||
// Bodies are made up.
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { git } from './helpers.mjs';
|
||||
|
||||
// A repo with one commit holding `files` (path -> text). Removed when the test ends.
|
||||
export function scratchRepo(t, files) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'r216-'));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const repo = join(root, 'books');
|
||||
mkdirSync(repo);
|
||||
git(repo, 'init', '-q', '-b', 'main');
|
||||
for (const [path, text] of Object.entries(files)) {
|
||||
mkdirSync(dirname(join(repo, path)), { recursive: true });
|
||||
writeFileSync(join(repo, path), text);
|
||||
}
|
||||
git(repo, 'add', '-A');
|
||||
git(repo, 'commit', '-qm', 'fixture');
|
||||
return { root, repo, commit: git(repo, 'rev-parse', 'HEAD') };
|
||||
}
|
||||
|
||||
// A note: front matter with a type and a name, optional extra front matter lines, then a body.
|
||||
export const note = (type, name, body = '', extra = []) => ['---', `type: ${type}`, `name: ${name}`, ...extra, '---', body, ''].join('\n');
|
||||
|
||||
// The one exclusion the hybrid tests use, and the paths it hides. C2-15 (row 267) replaces the reading of
|
||||
// `exclude` (a path prefix today); swapping these two constants is the whole change to the tests.
|
||||
export const EXCLUSION = { exclude: 'Book One/Research' };
|
||||
export const EXCLUDED_PATHS = ['Book One/Research/Ferry Logs.md'];
|
||||
|
||||
// C2-05c: synthetic scope fixture, including same-prefix precedence and a draft.
|
||||
export const SUPPORT_FILES = {
|
||||
'Book One/Characters/Canon.md': note('character', 'Canon', 'Scope fixture canon.'),
|
||||
'Book One/Playground/Plan.md': note('summary', 'Plan', 'Scope fixture playground.'),
|
||||
'Book One/Support/Notes/Helper.md': note('summary', 'Helper', 'Scope fixture supporting.'),
|
||||
'Notes/HarborSupport/Loose.md': note('summary', 'Loose', 'Scope fixture supporting loose.'),
|
||||
'Drafts/Idea/Support/Seed.md': note('summary', 'Seed', 'Scope fixture supporting draft.'),
|
||||
};
|
||||
export const withSupporting = entry => ({
|
||||
...entry,
|
||||
universes: {
|
||||
...entry.universes,
|
||||
harbor: { ...entry.universes.harbor,
|
||||
playground: [...entry.universes.harbor.playground, 'Book One/Support/'],
|
||||
supporting: ['Book One/Support/', 'Notes/HarborSupport/'] },
|
||||
idea: { kind: 'draft', name: 'Idea', playground: ['Drafts/Idea/'], supporting: ['Drafts/Idea/Support/'] },
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,466 @@
|
||||
// Query tests for design 3 (C2-05a): universe isolation, scope labels, unmapped files, hybrids, paging
|
||||
// under the reply cap, refusals and manuscript paging. Fixtures are made up and built under the scratch area.
|
||||
import { test } from 'node:test';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import assert from 'node:assert/strict';
|
||||
import { REPLY_CAP } from '../../harness/src/tools.mjs';
|
||||
import { find, get, search, manuscript, universeInfo, QUERY_REPLY_CAP, LOOKUP_LIMIT, MANUSCRIPT_LINES } from '../src/query.mjs';
|
||||
import { mapping, MARKER, git } from './helpers.mjs';
|
||||
import { scratchRepo, note, EXCLUSION, EXCLUDED_PATHS, SUPPORT_FILES, withSupporting } from './query-fixture.mjs';
|
||||
|
||||
// A hybrid over harbor and second. Its one constraint is EXCLUSION (query-fixture.mjs) and it declares no
|
||||
// books or playground.
|
||||
const WITH_BLEND = e => ({
|
||||
...e,
|
||||
universes: {
|
||||
...e.universes,
|
||||
blend: {
|
||||
kind: 'hybrid', name: 'Blend', sources: ['harbor', 'second'], books: [], playground: [],
|
||||
constraints: [EXCLUSION], notes: 'made-up hybrid',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// PRD 1.14 levels (C2-01c): harbor gets a universe-level playground prefix outside every book, and a draft
|
||||
// universe has only playground prefixes. Files there have book null.
|
||||
const WITH_LEVELS = e => ({
|
||||
...WITH_BLEND(e),
|
||||
universes: {
|
||||
...WITH_BLEND(e).universes,
|
||||
harbor: { ...e.universes.harbor, playground: ['Book One/Playground/', 'Notes/Harbor/'] },
|
||||
idea: { kind: 'draft', name: 'New World', playground: ['Drafts/New World/'] },
|
||||
},
|
||||
});
|
||||
const LEVEL_FILES = {
|
||||
'Notes/Harbor/Scraps.md': note('summary', 'Scraps', `${MARKER} loose`),
|
||||
'Notes/Harbor/Chapters/Idea Chapter.md': `# Idea\n${MARKER} unbooked chapter\n`,
|
||||
'Drafts/New World/Characters/Rin.md': note('character', 'Rin', `${MARKER} draft`),
|
||||
'Drafts/New World/Summary.md': note('summary', 'Summary', `${MARKER} draft`),
|
||||
};
|
||||
|
||||
const FILES = {
|
||||
'Book One/Characters/Mara Vell.md': note('character', 'Mara Vell', `Grey eyes. ${MARKER}`,
|
||||
['appears_in: "[[Book One/Chapters/Chapter 01]]"', 'relationships:', ' - "[[Tomas]]"']),
|
||||
'Book One/Characters/Tomas.md': note('character', 'Tomas', MARKER),
|
||||
'Book One/Chapters/Chapter 01.md': `# One\n${MARKER} [[Mara Vell]] met [[Book One/Places/Harbor Town|the harbor]].\n`,
|
||||
'Book One/Chapters/Chapter 02.md': 'Quiet. [[Ferry Logs]] [[Ilse]]\n',
|
||||
'Book One/Places/Harbor Town.md': note('place', 'Harbor Town', `Salt. ${MARKER}`),
|
||||
'Book One/Research/Ferry Logs.md': note('research', 'Ferry Logs', `Timetable. ${MARKER}`),
|
||||
'Book One/Playground/What If.md': note('summary', 'What If', `${MARKER} [[Mara Vell]]`),
|
||||
'Second World/Characters/Ilse.md': note('character', 'Ilse', `${MARKER} Ilse.`),
|
||||
'Second World/Chapters/01.md': '# Elsewhere\n[[Ilse]] waits.\n',
|
||||
'Inbox Note.md': `${MARKER}\n`,
|
||||
'Book One/Board.canvas': '{"nodes":[]}\n',
|
||||
};
|
||||
|
||||
const src = (ref, map = mapping()) => ({ repo: ref.repo, commit: ref.commit, mapping: map });
|
||||
const paths = reply => reply.results.map(r => r.path);
|
||||
|
||||
test('the query reply cap equals the harness reply cap', () => {
|
||||
assert.equal(QUERY_REPLY_CAP, REPLY_CAP);
|
||||
});
|
||||
|
||||
test('find is bound to its universe and refuses a call without one', async t => {
|
||||
const s = src(scratchRepo(t, FILES));
|
||||
const names = paths(await find(s, { universe: 'harbor', query: '' }));
|
||||
assert.ok(names.length > 0);
|
||||
assert.ok(names.every(p => p.startsWith('Book One/')));
|
||||
assert.ok(!names.includes('Second World/Characters/Ilse.md'));
|
||||
await assert.rejects(find(s, { query: '' }), { code: 'query-argument-invalid' });
|
||||
await assert.rejects(find(s, { universe: 'nowhere', query: '' }), { code: 'query-universe-unknown' });
|
||||
});
|
||||
|
||||
test('every result carries its scope, and playground appears only when asked for', async t => {
|
||||
const s = src(scratchRepo(t, FILES));
|
||||
const canon = (await find(s, { universe: 'harbor', query: '' })).results;
|
||||
assert.ok(canon.every(r => r.scope === 'canon'));
|
||||
assert.ok(!canon.some(r => r.path.includes('/Playground/')));
|
||||
const play = (await find(s, { universe: 'harbor', query: '', scope: 'playground' })).results;
|
||||
assert.deepEqual(play.map(r => r.path), ['Book One/Playground/What If.md']);
|
||||
assert.ok(play.every(r => r.scope === 'playground'));
|
||||
});
|
||||
|
||||
test('unmapped files never appear in any reply', async t => {
|
||||
const s = src(scratchRepo(t, FILES));
|
||||
assert.deepEqual((await find(s, { universe: 'harbor', query: 'Inbox' })).results, []);
|
||||
assert.deepEqual((await find(s, { universe: 'harbor', query: 'Board' })).results, []);
|
||||
const hits = (await search(s, { universe: 'harbor', text: MARKER })).results;
|
||||
assert.ok(hits.length > 0);
|
||||
assert.ok(hits.every(r => r.path !== 'Inbox Note.md' && r.path !== 'Book One/Board.canvas'));
|
||||
});
|
||||
|
||||
test('a hybrid sees its sources canon, minus exclusions, and no playground', async t => {
|
||||
const s = src(scratchRepo(t, FILES), mapping(WITH_BLEND));
|
||||
const reply = await find(s, { universe: 'blend', query: '' });
|
||||
assert.deepEqual(paths(reply).sort(), [
|
||||
'Book One/Characters/Mara Vell.md', 'Book One/Characters/Tomas.md', 'Book One/Chapters/Chapter 01.md',
|
||||
'Book One/Chapters/Chapter 02.md', 'Book One/Places/Harbor Town.md',
|
||||
'Second World/Characters/Ilse.md', 'Second World/Chapters/01.md',
|
||||
]);
|
||||
assert.ok(reply.results.every(r => r.scope === 'canon' && ['harbor', 'second'].includes(r.universe)));
|
||||
assert.deepEqual((await find(s, { universe: 'blend', query: '', scope: 'playground' })).results, []);
|
||||
assert.deepEqual((await find(s, { universe: 'blend', query: 'Ferry' })).results, []);
|
||||
});
|
||||
|
||||
test('a hybrid gets a source file it can see, and not an excluded or foreign one', async t => {
|
||||
const ref = scratchRepo(t, FILES);
|
||||
const hybrid = src(ref, mapping(WITH_BLEND));
|
||||
const ilse = await get(hybrid, { universe: 'blend', path: 'Second World/Characters/Ilse.md' });
|
||||
assert.equal(ilse.scope, 'canon');
|
||||
assert.ok(ilse.links_in.some(l => l.path === 'Second World/Chapters/01.md'));
|
||||
assert.equal(await get(hybrid, { universe: 'blend', path: 'Book One/Research/Ferry Logs.md' }), null);
|
||||
assert.equal(await get(src(ref), { universe: 'harbor', path: 'Second World/Characters/Ilse.md' }), null);
|
||||
});
|
||||
|
||||
test('get lists visible links both ways and drops a target another universe cannot see', async t => {
|
||||
const ref = scratchRepo(t, FILES);
|
||||
const harbor = src(ref);
|
||||
const ch1 = await get(harbor, { universe: 'harbor', path: 'Book One/Chapters/Chapter 01.md' });
|
||||
assert.ok(ch1.links_out.some(l => l.path === 'Book One/Characters/Mara Vell.md' && l.line === 2));
|
||||
assert.ok(ch1.links_out.some(l => l.path === 'Book One/Places/Harbor Town.md'));
|
||||
const ch2 = await get(harbor, { universe: 'harbor', path: 'Book One/Chapters/Chapter 02.md' });
|
||||
assert.ok(ch2.links_out.some(l => l.path === 'Book One/Research/Ferry Logs.md'));
|
||||
assert.ok(!ch2.links_out.some(l => l.path === 'Second World/Characters/Ilse.md'));
|
||||
const mara = await get(harbor, { universe: 'harbor', path: 'Book One/Characters/Mara Vell.md' });
|
||||
assert.ok(mara.links_in.some(l => l.path === 'Book One/Chapters/Chapter 01.md'));
|
||||
assert.ok(mara.links_in.some(l => l.path === 'Book One/Playground/What If.md'));
|
||||
assert.ok(mara.links_out.some(l => l.path === 'Book One/Characters/Tomas.md' && l.field === 'relationships'));
|
||||
assert.ok(mara.links_out.some(l => l.path === 'Book One/Chapters/Chapter 01.md' && l.field === 'appears_in'));
|
||||
const blendCh2 = await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path: 'Book One/Chapters/Chapter 02.md' });
|
||||
assert.ok(blendCh2.links_out.some(l => l.path === 'Second World/Characters/Ilse.md'));
|
||||
});
|
||||
|
||||
test('a long list pages by position, never cuts an item, and returns 50 per page', async t => {
|
||||
const files = { ...FILES };
|
||||
for (let i = 1; i <= 120; i++) files[`Book One/Places/Note ${i}.md`] = note('place', `Note ${i}`);
|
||||
const s = src(scratchRepo(t, files));
|
||||
const seen = [];
|
||||
let offset = 0;
|
||||
for (let pages = 0; pages < 10; pages++) {
|
||||
const reply = await find(s, { universe: 'harbor', query: 'Note', offset });
|
||||
assert.ok(JSON.stringify(reply).length <= QUERY_REPLY_CAP);
|
||||
assert.ok(reply.results.length <= LOOKUP_LIMIT);
|
||||
if (pages === 0) assert.equal(reply.results.length, LOOKUP_LIMIT);
|
||||
seen.push(...paths(reply));
|
||||
if (reply.next === null) break;
|
||||
offset = reply.next;
|
||||
}
|
||||
assert.equal(seen.length, 120);
|
||||
assert.equal(new Set(seen).size, 120);
|
||||
});
|
||||
|
||||
test('a page stays under the reply cap when items are large', async t => {
|
||||
const big = 'x'.repeat(2000);
|
||||
const files = { ...FILES };
|
||||
for (let i = 1; i <= 40; i++) files[`Book One/Places/Big ${i}.md`] = note('place', `Big ${i}`, '', [`appears_in: "${big}"`]);
|
||||
const s = src(scratchRepo(t, files));
|
||||
const first = await find(s, { universe: 'harbor', query: 'Big' });
|
||||
assert.ok(JSON.stringify(first).length <= QUERY_REPLY_CAP);
|
||||
assert.ok(first.results.length > 0 && first.results.length < 40);
|
||||
assert.notEqual(first.next, null);
|
||||
const seen = [];
|
||||
let offset = 0;
|
||||
for (let pages = 0; pages < 10; pages++) {
|
||||
const reply = await find(s, { universe: 'harbor', query: 'Big', offset });
|
||||
assert.ok(JSON.stringify(reply).length <= QUERY_REPLY_CAP);
|
||||
seen.push(...paths(reply));
|
||||
if (reply.next === null) break;
|
||||
offset = reply.next;
|
||||
}
|
||||
assert.equal(new Set(seen).size, 40);
|
||||
});
|
||||
|
||||
test('a search line too long to fit alone comes back marked, with no excerpt, and the other hits still return', async t => {
|
||||
const huge = 'y'.repeat(QUERY_REPLY_CAP + 1000);
|
||||
const s = src(scratchRepo(t, { ...FILES, 'Book One/Places/Wall.md': note('place', 'Wall', `${huge} ${MARKER}`) }));
|
||||
const reply = await search(s, { universe: 'harbor', text: 'yyyyyyyy' });
|
||||
assert.deepEqual(reply.results, [{ path: 'Book One/Places/Wall.md', line: 5, excerpt: null, excerpt_too_long: true, scope: 'canon' }]);
|
||||
const mixed = await search(s, { universe: 'harbor', text: MARKER.toLowerCase() });
|
||||
assert.ok(mixed.results.some(r => r.excerpt_too_long === true));
|
||||
assert.ok(mixed.results.some(r => typeof r.excerpt === 'string' && !('excerpt_too_long' in r)));
|
||||
assert.ok(JSON.stringify(mixed).length <= QUERY_REPLY_CAP);
|
||||
});
|
||||
|
||||
test('a find item that cannot fit alone is refused, not cut', async t => {
|
||||
const huge = 'y'.repeat(QUERY_REPLY_CAP + 1000);
|
||||
const s = src(scratchRepo(t, { ...FILES, 'Book One/Places/Wall.md': note('place', 'Wall', '', [`appears_in: "${huge}"`]) }));
|
||||
await assert.rejects(find(s, { universe: 'harbor', query: 'Wall' }), { code: 'query-item-too-large' });
|
||||
});
|
||||
|
||||
test('get pages its lines, and the next line picks up where the page stopped', async t => {
|
||||
const body = `${Array.from({ length: 500 }, (_, i) => `Line ${i + 1} ${'z'.repeat(120)}`).join('\n')}\n`;
|
||||
const s = src(scratchRepo(t, { ...FILES, 'Book One/Chapters/Long.md': body }));
|
||||
const first = await get(s, { universe: 'harbor', path: 'Book One/Chapters/Long.md' });
|
||||
assert.equal(first.lines[0][0], 1);
|
||||
assert.ok(first.lines.length < 501);
|
||||
assert.notEqual(first.next, null);
|
||||
let from = 1, count = 0;
|
||||
for (let pages = 0; pages < 10; pages++) {
|
||||
const page = await get(s, { universe: 'harbor', path: 'Book One/Chapters/Long.md', from });
|
||||
count += page.lines.length;
|
||||
if (page.next === null) break;
|
||||
from = page.next;
|
||||
}
|
||||
assert.equal(count, 501);
|
||||
});
|
||||
|
||||
test('get refuses a file whose front matter alone is over the cap, and a line alone is over the cap', async t => {
|
||||
const huge = 'q'.repeat(QUERY_REPLY_CAP + 1000);
|
||||
const s = src(scratchRepo(t, {
|
||||
...FILES,
|
||||
'Book One/Places/Heavy.md': note('place', 'Heavy', '', [`appears_in: "${huge}"`]),
|
||||
'Book One/Chapters/Wide.md': `${huge}\n`,
|
||||
}));
|
||||
await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Places/Heavy.md' }), { code: 'query-item-too-large' });
|
||||
await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Chapters/Wide.md' }), { code: 'query-item-too-large' });
|
||||
});
|
||||
|
||||
test('get pages the links of a hub note, every page carries the front matter, and the pages add up', async t => {
|
||||
const files = { ...FILES };
|
||||
for (let i = 1; i <= 900; i++) files[`Book One/Chapters/Mention ${i}.md`] = `[[Tomas]] ${'w'.repeat(60)}\n`;
|
||||
const s = src(scratchRepo(t, files));
|
||||
const seen = [];
|
||||
const PAST_END = 1000;
|
||||
let linksFrom = 0, pages = 0, lines = 0, from = 1;
|
||||
for (; pages < 40; pages++) {
|
||||
const page = await get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', from, links_from: linksFrom });
|
||||
assert.ok(JSON.stringify(page).length <= QUERY_REPLY_CAP);
|
||||
assert.equal(page.frontmatter.name, 'Tomas');
|
||||
seen.push(...page.links_in.map(l => l.path));
|
||||
if (from !== PAST_END) lines += page.lines.length;
|
||||
// Once the lines are read, ask past the last one: the page then carries links alone.
|
||||
from = page.next ?? PAST_END;
|
||||
if (page.links_next === null && page.next === null) break;
|
||||
if (page.links_next !== null) linksFrom = page.links_next;
|
||||
}
|
||||
assert.ok(pages > 1, 'the hub needed more than one page');
|
||||
assert.ok(pages < 40);
|
||||
assert.equal(seen.filter(p => p.startsWith('Book One/Chapters/Mention ')).length, 900);
|
||||
assert.equal(new Set(seen).size, seen.length, 'no link comes back twice');
|
||||
assert.equal(lines, 6, 'the five lines of the note and the empty one after its last newline');
|
||||
const tail = await get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', from: PAST_END });
|
||||
assert.deepEqual(tail.lines, []);
|
||||
assert.equal(tail.next, null);
|
||||
});
|
||||
|
||||
test('get rejects a bad links_from', async t => {
|
||||
const s = src(scratchRepo(t, FILES));
|
||||
await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', links_from: -1 }), { code: 'query-argument-invalid' });
|
||||
const past = await get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', links_from: 99 });
|
||||
assert.deepEqual([past.links_out, past.links_in, past.links_next], [[], [], null]);
|
||||
});
|
||||
|
||||
test('search returns scoped line hits, case-insensitively, and refuses empty text', async t => {
|
||||
const s = src(scratchRepo(t, FILES));
|
||||
const reply = await search(s, { universe: 'harbor', text: 'marker' });
|
||||
assert.ok(reply.results.length > 0);
|
||||
assert.ok(reply.results.every(r => r.scope === 'canon' && r.path.startsWith('Book One/') && Number.isInteger(r.line)));
|
||||
assert.ok(reply.results.some(r => r.path === 'Book One/Chapters/Chapter 01.md' && r.line === 2));
|
||||
await assert.rejects(search(s, { universe: 'harbor', text: '' }), { code: 'query-argument-invalid' });
|
||||
});
|
||||
|
||||
test('a manuscript call returns one chapter, at most 400 lines, and names its next line', async t => {
|
||||
const long = `${Array.from({ length: 900 }, (_, i) => `Scene ${i + 1}`).join('\n')}\n`;
|
||||
const s = src(scratchRepo(t, { ...FILES, 'Book One/Chapters/Long Chapter.md': long }));
|
||||
const first = await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Long Chapter' });
|
||||
assert.equal(first.lines.length, MANUSCRIPT_LINES);
|
||||
assert.equal(first.next, MANUSCRIPT_LINES + 1);
|
||||
const second = await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Long Chapter', from: first.next });
|
||||
assert.equal(second.lines[0][0], MANUSCRIPT_LINES + 1);
|
||||
});
|
||||
|
||||
test('a manuscript call without a chapter returns the first chapter in path order and names the next', async t => {
|
||||
const s = src(scratchRepo(t, FILES));
|
||||
const first = await manuscript(s, { universe: 'harbor', book: 'Book One' });
|
||||
assert.equal(first.path, 'Book One/Chapters/Chapter 01.md');
|
||||
assert.equal(first.next_chapter, 'Chapter 02');
|
||||
const last = await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Chapter 02' });
|
||||
assert.equal(last.path, 'Book One/Chapters/Chapter 02.md');
|
||||
assert.equal(last.next_chapter, null);
|
||||
assert.equal(await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Nope' }), null);
|
||||
});
|
||||
|
||||
test('a chapter name that matches two visible files is refused', async t => {
|
||||
const s = src(scratchRepo(t, { ...FILES, 'Book One/Chapters/Part/Chapter 01.md': 'Again.\n' }));
|
||||
await assert.rejects(manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Chapter 01' }), { code: 'query-chapter-ambiguous' });
|
||||
});
|
||||
|
||||
test('a hybrid reads a source chapter through its declared sources only', async t => {
|
||||
const ref = scratchRepo(t, FILES);
|
||||
const hybrid = await manuscript(src(ref, mapping(WITH_BLEND)), { universe: 'blend', book: 'Second World', chapter: '01' });
|
||||
assert.equal(hybrid.path, 'Second World/Chapters/01.md');
|
||||
assert.equal(hybrid.scope, 'canon');
|
||||
assert.equal(await manuscript(src(ref), { universe: 'harbor', book: 'Second World', chapter: '01' }), null);
|
||||
});
|
||||
|
||||
test('universe info reports a hybrid sources, constraints and notes, and refuses an unknown universe', async t => {
|
||||
const s = src(scratchRepo(t, FILES), mapping(WITH_BLEND));
|
||||
const blend = await universeInfo(s, { universe: 'blend' });
|
||||
assert.equal(blend.kind, 'hybrid');
|
||||
assert.deepEqual(blend.sources, ['harbor', 'second']);
|
||||
assert.deepEqual(blend.constraints, [EXCLUSION]);
|
||||
assert.equal(blend.notes, 'made-up hybrid');
|
||||
const harbor = await universeInfo(s, { universe: 'harbor' });
|
||||
assert.equal(harbor.kind, 'universe');
|
||||
assert.equal(harbor.sources, undefined);
|
||||
await assert.rejects(universeInfo(s, { universe: 'ghost' }), { code: 'query-universe-unknown' });
|
||||
});
|
||||
|
||||
test('an exclusion hides what it names from a hybrid and from nothing else', async t => {
|
||||
const ref = scratchRepo(t, FILES);
|
||||
const hybrid = src(ref, mapping(WITH_BLEND));
|
||||
const seen = paths(await find(hybrid, { universe: 'blend', query: '' }));
|
||||
for (const path of EXCLUDED_PATHS) assert.ok(!seen.includes(path), `${path} stays hidden`);
|
||||
const own = paths(await find(src(ref), { universe: 'harbor', query: '' }));
|
||||
for (const path of EXCLUDED_PATHS) assert.ok(own.includes(path), `${path} stays visible to its own universe`);
|
||||
});
|
||||
|
||||
test('PRD 1.14: a lookup that names a book leaves book-null files out', async t => {
|
||||
const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS));
|
||||
const all = paths(await find(s, { universe: 'harbor', query: '', scope: 'playground' }));
|
||||
assert.deepEqual(all.sort(), ['Book One/Playground/What If.md', 'Notes/Harbor/Chapters/Idea Chapter.md', 'Notes/Harbor/Scraps.md']);
|
||||
const booked = await find(s, { universe: 'harbor', query: '', scope: 'playground', book: 'Book One' });
|
||||
assert.deepEqual(paths(booked), ['Book One/Playground/What If.md']);
|
||||
assert.ok(booked.results.every(r => r.book === 'Book One'));
|
||||
const hits = (await search(s, { universe: 'harbor', text: 'unbooked', scope: 'playground' })).results;
|
||||
assert.deepEqual(hits.map(r => r.path), ['Notes/Harbor/Chapters/Idea Chapter.md']);
|
||||
assert.deepEqual((await search(s, { universe: 'harbor', text: 'unbooked', scope: 'playground', book: 'Book One' })).results, []);
|
||||
});
|
||||
|
||||
test('PRD 1.14: library_manuscript never reads a book-null file', async t => {
|
||||
const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS));
|
||||
// The unbooked file sits under a manuscript folder, so it has type manuscript and book null.
|
||||
const listed = await find(s, { universe: 'harbor', query: 'Idea', scope: 'playground' });
|
||||
assert.deepEqual(listed.results.map(r => [r.path, r.type, r.book]), [['Notes/Harbor/Chapters/Idea Chapter.md', 'manuscript', null]]);
|
||||
assert.equal(await manuscript(s, { universe: 'harbor', book: 'Book One', scope: 'playground', chapter: 'Idea Chapter' }), null);
|
||||
assert.equal(await manuscript(s, { universe: 'harbor', book: 'Book One', scope: 'playground' }), null);
|
||||
assert.equal(await manuscript(s, { universe: 'harbor', book: 'Notes', scope: 'playground', chapter: 'Idea Chapter' }), null);
|
||||
});
|
||||
|
||||
test('PRD 1.14: a draft universe has no canon, so its lookups return files only with scope playground', async t => {
|
||||
const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS));
|
||||
assert.deepEqual((await find(s, { universe: 'idea', query: '' })).results, []);
|
||||
assert.deepEqual((await search(s, { universe: 'idea', text: MARKER })).results, []);
|
||||
const found = await find(s, { universe: 'idea', query: '', scope: 'playground' });
|
||||
assert.deepEqual(paths(found).sort(), ['Drafts/New World/Characters/Rin.md', 'Drafts/New World/Summary.md']);
|
||||
assert.ok(found.results.every(r => r.scope === 'playground' && r.universe === 'idea' && r.book === null));
|
||||
const hits = (await search(s, { universe: 'idea', text: 'draft', scope: 'playground' })).results;
|
||||
assert.equal(hits.length, 2);
|
||||
const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' });
|
||||
assert.equal(rin.scope, 'playground');
|
||||
assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' }), null);
|
||||
});
|
||||
|
||||
test('PRD 1.14: library_universe on a draft returns its kind, name and prefixes', async t => {
|
||||
const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS));
|
||||
const draft = await universeInfo(s, { universe: 'idea' });
|
||||
assert.equal(draft.kind, 'draft');
|
||||
assert.equal(draft.name, 'New World');
|
||||
assert.deepEqual(draft.playground, ['Drafts/New World/']);
|
||||
assert.deepEqual(draft.books, []);
|
||||
assert.equal(draft.sources, undefined);
|
||||
});
|
||||
|
||||
test('a hybrid gets a source playground file as null, while the source gets it with scope playground', async t => {
|
||||
const ref = scratchRepo(t, FILES);
|
||||
const path = 'Book One/Playground/What If.md';
|
||||
assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path, scope: 'playground' }), null);
|
||||
const own = await get(src(ref), { universe: 'harbor', path, scope: 'playground' });
|
||||
assert.equal(own.scope, 'playground');
|
||||
});
|
||||
|
||||
// mos-1330 item3: the new get contract defaults to canon even for an explicit playground path.
|
||||
test('C2-05c get without scope returns null for a playground path', async t => {
|
||||
const s = src(scratchRepo(t, FILES));
|
||||
assert.equal(await get(s, { universe: 'harbor', path: 'Book One/Playground/What If.md' }), null);
|
||||
});
|
||||
|
||||
// Row284: scope is explicit for get as well as lists; blob and lines share a served snapshot.
|
||||
test('C2-05c supporting find/search/get are opt-in at book, universe and draft level', async t => {
|
||||
const s = src(scratchRepo(t, SUPPORT_FILES), mapping(withSupporting));
|
||||
const expected = ['Book One/Support/Notes/Helper.md', 'Notes/HarborSupport/Loose.md'];
|
||||
assert.deepEqual(paths(await find(s, { universe: 'harbor', query: '', scope: 'supporting' })), expected);
|
||||
assert.deepEqual((await search(s, { universe: 'harbor', text: 'Scope fixture', scope: 'supporting' })).results.map(x => x.path), expected);
|
||||
assert.deepEqual(paths(await find(s, { universe: 'harbor', query: '' })), ['Book One/Characters/Canon.md']);
|
||||
assert.deepEqual((await search(s, { universe: 'harbor', text: 'Scope fixture' })).results.map(x => x.path), ['Book One/Characters/Canon.md']);
|
||||
for (const path of expected) {
|
||||
assert.equal(await get(s, { universe: 'harbor', path }), null);
|
||||
assert.equal(await get(s, { universe: 'harbor', path, scope: 'playground' }), null);
|
||||
assert.equal((await get(s, { universe: 'harbor', path, scope: 'supporting' })).scope, 'supporting');
|
||||
assert.equal(await get(s, { universe: 'second', path, scope: 'supporting' }), null);
|
||||
}
|
||||
const draftPath = 'Drafts/Idea/Support/Seed.md';
|
||||
assert.deepEqual(paths(await find(s, { universe: 'idea', query: '', scope: 'supporting' })), [draftPath]);
|
||||
assert.equal((await get(s, { universe: 'idea', path: draftPath, scope: 'supporting' })).scope, 'supporting');
|
||||
assert.equal(await get(s, { universe: 'idea', path: draftPath }), null);
|
||||
assert.equal((await find(s, { universe: 'harbor', query: '', book: 'Book One', scope: 'supporting' })).results.length, 1);
|
||||
});
|
||||
|
||||
test('C2-05c an absent supporting prefix returns nothing without refusal', async t => {
|
||||
const s = src(scratchRepo(t, SUPPORT_FILES));
|
||||
assert.deepEqual((await find(s, { universe: 'harbor', query: '', scope: 'supporting' })).results, []);
|
||||
assert.deepEqual((await search(s, { universe: 'harbor', text: 'Scope fixture', scope: 'supporting' })).results, []);
|
||||
assert.equal(await get(s, { universe: 'harbor', path: 'Book One/Characters/Canon.md', scope: 'supporting' }), null);
|
||||
assert.equal(await get(s, { universe: 'harbor', path: 'Book One/Support/Notes/Helper.md', scope: 'supporting' }), null);
|
||||
});
|
||||
|
||||
test('C2-05c each get blob equals its served tree object, even after source HEAD advances', async t => {
|
||||
const ref = scratchRepo(t, SUPPORT_FILES), s = src(ref, mapping(withSupporting));
|
||||
const canon = 'Book One/Characters/Canon.md';
|
||||
const priorBlob = git(ref.repo, 'rev-parse', `${ref.commit}:${canon}`);
|
||||
writeFileSync(join(ref.repo, canon), note('character', 'Canon changed', 'New HEAD must not be read.'));
|
||||
git(ref.repo, 'add', '-A'); git(ref.repo, 'commit', '-qm', 'advance head fixture');
|
||||
assert.notEqual(git(ref.repo, 'rev-parse', `HEAD:${canon}`), priorBlob);
|
||||
for (const [scope, path] of [['canon', canon], ['playground', 'Book One/Playground/Plan.md'], ['supporting', 'Book One/Support/Notes/Helper.md']]) {
|
||||
const result = await get(s, { universe: 'harbor', path, scope });
|
||||
assert.equal(result.commit, ref.commit);
|
||||
assert.equal(result.blob, git(ref.repo, 'rev-parse', `${ref.commit}:${path}`));
|
||||
assert.ok(!JSON.stringify(result.lines).includes('New HEAD must not be read'));
|
||||
}
|
||||
});
|
||||
|
||||
test('C2-05c full outgoing and incoming links stay in scope order across pages without losing cross-scope links', async t => {
|
||||
const files = {}, groups = { supporting: [], playground: [], canon: [] };
|
||||
for (const [scope, folder] of [['canon', 'Book One/Characters'], ['playground', 'Book One/Playground'], ['supporting', 'Book One/Support']]) {
|
||||
for (let n = 0; n < 100; n++) {
|
||||
const path = `${folder}/${scope}-${String(n).padStart(3, '0')}-${'x'.repeat(80)}.md`;
|
||||
groups[scope].push(path);
|
||||
files[path] = `[[Book One/Characters/Hub]]\n`;
|
||||
}
|
||||
}
|
||||
const all = [...groups.canon, ...groups.playground, ...groups.supporting];
|
||||
files['Book One/Characters/Hub.md'] = all.map(path => `[[${path.slice(0, -3)}]]`).join('\n');
|
||||
// Other-universe links are never returned, regardless of rank.
|
||||
files['Second World/Other.md'] = '[[Book One/Characters/Hub]]\n';
|
||||
const s = src(scratchRepo(t, files), mapping(withSupporting));
|
||||
const outs = [], ins = [];
|
||||
let offset = 0, pages = 0;
|
||||
for (; pages < 20; pages++) {
|
||||
const page = await get(s, { universe: 'harbor', path: 'Book One/Characters/Hub.md', from: 999_999, links_from: offset });
|
||||
assert.ok(JSON.stringify(page).length <= QUERY_REPLY_CAP);
|
||||
outs.push(...page.links_out.map(x => x.path)); ins.push(...page.links_in.map(x => x.path));
|
||||
if (page.links_next === null) break;
|
||||
offset = page.links_next;
|
||||
}
|
||||
const expected = [...groups.supporting, ...groups.playground, ...groups.canon];
|
||||
assert.ok(pages > 0 && pages < 20, 'links occupy multiple bounded pages');
|
||||
assert.deepEqual(outs, expected);
|
||||
assert.deepEqual(ins, expected);
|
||||
assert.equal(new Set(outs).size, 300);
|
||||
assert.equal(new Set(ins).size, 300);
|
||||
});
|
||||
|
||||
test('C2-05c manuscript still refuses supporting rather than silently widening its scope', async t => {
|
||||
const s = src(scratchRepo(t, SUPPORT_FILES), mapping(withSupporting));
|
||||
await assert.rejects(manuscript(s, { universe: 'harbor', book: 'Book One', scope: 'supporting' }), { code: 'query-argument-invalid' });
|
||||
});
|
||||
|
||||
test('C2-05c scope expansion keeps hybrid sources canon-only and refuses all/array scopes', async t => {
|
||||
const s = src(scratchRepo(t, SUPPORT_FILES), mapping(entry => withSupporting(WITH_BLEND(entry))));
|
||||
assert.deepEqual((await find(s, { universe: 'blend', query: '', scope: 'supporting' })).results, []);
|
||||
assert.equal(await get(s, { universe: 'blend', path: 'Book One/Support/Notes/Helper.md', scope: 'supporting' }), null);
|
||||
assert.equal((await get(s, { universe: 'blend', path: 'Book One/Characters/Canon.md' })).scope, 'canon');
|
||||
for (const scope of ['all', ['canon', 'supporting']])
|
||||
await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Characters/Canon.md', scope }), { code: 'query-argument-invalid' });
|
||||
});
|
||||
@@ -1,94 +1,80 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { renderReply } from '../src/index.mjs';
|
||||
import { renderItems, UNREADABLE } from '../src/index.mjs';
|
||||
|
||||
// Row 225 (C2-10b): renderReply replaces renderItems (design 7.4, the reply schema of 5.2).
|
||||
const A = 'a'.repeat(40), B = 'b'.repeat(40);
|
||||
const cite = (path, lines, quote, commit = A, extra = {}) => ({ path, commit, lines, quote, ...extra });
|
||||
const web = (url, quote = 'spring tides') => ({ url, quote });
|
||||
const reply = (text, citations) => JSON.stringify({ text, citations });
|
||||
const CITED = reply("Mara's eyes are grey in chapter 3.", [
|
||||
cite('Book One/Chapters/Chapter 03.md', [41, 41], 'her grey eyes', A, { as: 'canon' }),
|
||||
cite('Book One/Playground/What If.md', [5, 6], 'a storm', B),
|
||||
cite('Notes/Eyes.md', [9, 10], 'green eyes'),
|
||||
web('https://example.invalid/tides'),
|
||||
]);
|
||||
const CHECK = { citations: [{ scope: 'canon', agent: null }, { scope: 'playground', agent: null },
|
||||
{ scope: 'supporting', agent: 'acme/scribe' }, { scope: 'web', agent: null }] };
|
||||
const changed = ({ path, commit }) => path === 'Notes/Eyes.md' && commit === A;
|
||||
const cite = (path, lines, quote, commit = A) => ({ path, commit, lines, quote });
|
||||
const options = {
|
||||
scopeOf: path => path.includes('/Playground/') ? 'playground' : path.endsWith('.md') ? 'canon' : undefined,
|
||||
changed: ({ path, commit }) => path === 'Book One/Characters/Mara Vell.md' && commit === A,
|
||||
};
|
||||
const render = items => renderItems(JSON.stringify({ items }), options);
|
||||
|
||||
test('free text renders as itself with no citations', () => {
|
||||
for (const body of ['Grey, in chapter 3.', '', '"a JSON string"', '[1, 2]', '42', 'null'])
|
||||
assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body });
|
||||
// One snapshot per item kind: the plain text both channels show.
|
||||
const SNAPSHOTS = {
|
||||
fact: [{ kind: 'fact', text: 'Tomas is the ferryman.', cites: [cite('Book One/Characters/Tomas.md', [3, 3], 'the ferryman', B)] }],
|
||||
finding: [{ kind: 'finding', text: 'Eye colour differs.', cites: [cite('Book One/Chapters/Chapter 01.md', [41, 41], 'her grey eyes'),
|
||||
cite('Book One/Characters/Mara Vell.md', [9, 10], 'green eyes')] }],
|
||||
correction: [{ kind: 'correction', text: 'Chapter 1 says grey.', from: 'green eyes', to: 'grey eyes',
|
||||
cites: [cite('Book One/Characters/Mara Vell.md', [9, 9], 'green eyes', B)] }],
|
||||
question: [{ kind: 'question', text: 'Is the harbor north of the town?', cites: [cite('Book One/Places/Harbor Town.md', [2, 4], 'the harbor', B)] }],
|
||||
idea: [{ kind: 'idea', text: 'A storm could close the harbor.', cites: [cite('Book One/Playground/What If.md', [5, 5], 'a storm', B),
|
||||
{ url: 'https://example.invalid/tides', quote: 'spring tides' }] }],
|
||||
};
|
||||
const EXPECTED = {
|
||||
fact: '1. Fact: Tomas is the ferryman.\n [canon] Book One/Characters/Tomas.md:3 @bbbbbbb "the ferryman"',
|
||||
finding: '1. Finding: Eye colour differs.\n [canon] Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
|
||||
+ ' [canon] Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes" (changed since)',
|
||||
correction: '1. Correction: Chapter 1 says grey.\n From: "green eyes"\n To: "grey eyes"\n'
|
||||
+ ' [canon] Book One/Characters/Mara Vell.md:9 @bbbbbbb "green eyes"',
|
||||
question: '1. Question: Is the harbor north of the town?\n [canon] Book One/Places/Harbor Town.md:2-4 @bbbbbbb "the harbor"',
|
||||
idea: '1. Idea: A storm could close the harbor.\n [playground] Book One/Playground/What If.md:5 @bbbbbbb "a storm"\n'
|
||||
+ ' [web] https://example.invalid/tides "spring tides"',
|
||||
};
|
||||
|
||||
for (const kind of Object.keys(SNAPSHOTS)) {
|
||||
test(`renderItems snapshot: ${kind}`, () => {
|
||||
const out = render(SNAPSHOTS[kind]);
|
||||
assert.equal(out.kind, 'items');
|
||||
assert.equal(out.text, EXPECTED[kind]);
|
||||
assert.equal(out.items[0].kind, kind);
|
||||
});
|
||||
}
|
||||
|
||||
test('renderItems numbers several items and takes a parsed body as well as a string', () => {
|
||||
const items = [...SNAPSHOTS.fact, ...SNAPSHOTS.question];
|
||||
const out = renderItems({ items }, options);
|
||||
assert.equal(out.text, `${EXPECTED.fact}\n${EXPECTED.question.replace(/^1\./, '2.')}`);
|
||||
assert.deepEqual(out.items[1].cites[0], { badge: 'canon', path: 'Book One/Places/Harbor Town.md', commit: B, lines: [2, 4],
|
||||
ref: 'Book One/Places/Harbor Town.md:2-4 @bbbbbbb', quote: 'the harbor', changed: false });
|
||||
});
|
||||
|
||||
test('a cited reply renders its text, then each cite with the badge from the check result', () => {
|
||||
const out = renderReply(CITED, { check: CHECK, changed });
|
||||
assert.equal(out.kind, 'reply');
|
||||
assert.equal(out.text, "Mara's eyes are grey in chapter 3.");
|
||||
assert.deepEqual(out.citations.map(c => c.badge), ['canon', 'playground', 'supporting', 'web']);
|
||||
assert.equal(out.plain, "Mara's eyes are grey in chapter 3.\n"
|
||||
+ ' [canon] Book One/Chapters/Chapter 03.md:41 @aaaaaaa "her grey eyes"\n'
|
||||
+ ' [playground] Book One/Playground/What If.md:5-6 @bbbbbbb "a storm"\n'
|
||||
+ ' [supporting] Notes/Eyes.md:9-10 @aaaaaaa "green eyes" (agent-written: acme/scribe) (changed since)\n'
|
||||
+ ' [web] https://example.invalid/tides "spring tides"');
|
||||
assert.deepEqual(out.citations[0], { badge: 'canon', path: 'Book One/Chapters/Chapter 03.md', commit: A, lines: [41, 41],
|
||||
ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa', quote: 'her grey eyes', canon: true, agent: null, changed: false });
|
||||
assert.deepEqual(out.citations[3], { badge: 'web', url: 'https://example.invalid/tides', link: 'https://example.invalid/tides',
|
||||
ref: 'https://example.invalid/tides', quote: 'spring tides', agent: null, changed: false });
|
||||
test('without options a path cite has no badge and is never marked changed', () => {
|
||||
assert.equal(renderItems({ items: SNAPSHOTS.finding }).text,
|
||||
'1. Finding: Eye colour differs.\n Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
|
||||
+ ' Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes"');
|
||||
});
|
||||
|
||||
test('a parsed body renders as its string does', () => {
|
||||
assert.deepEqual(renderReply(JSON.parse(CITED), { check: CHECK, changed }), renderReply(CITED, { check: CHECK, changed }));
|
||||
});
|
||||
const NOTICE_SNAPSHOTS = { 'no-cited-answer': 'No cited answer', 'no-answer': 'No answer', 'turn-failed': 'The turn failed' };
|
||||
for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
|
||||
test(`renderItems snapshot: notice ${notice}`, () => {
|
||||
assert.deepEqual(renderItems(JSON.stringify({ items: [], notice }), options), { kind: 'notice', notice, text });
|
||||
});
|
||||
}
|
||||
|
||||
test('without a check result a path cite has no badge, no label and is never changed', () => {
|
||||
const out = renderReply(CITED);
|
||||
assert.deepEqual(out.citations.map(c => [c.badge, c.agent, c.changed]),
|
||||
[[null, null, false], [null, null, false], [null, null, false], ['web', null, false]]);
|
||||
});
|
||||
|
||||
test('the label and the badge come from the check result only, never from a bad entry', () => {
|
||||
const bad = { citations: [{ scope: 'web', agent: '' }, { scope: 'story', agent: 7 }, null, { scope: 'canon', agent: 'x/y' }] };
|
||||
const out = renderReply(CITED, { check: bad });
|
||||
// A path cite can't be web; a url cite is web by its shape whatever the check says, and never agent-written.
|
||||
assert.deepEqual(out.citations.map(c => [c.badge, c.agent]), [[null, null], [null, null], [null, null], ['web', null]]);
|
||||
});
|
||||
|
||||
test('a web cite is a link only for https', () => {
|
||||
const urls = { 'https://example.invalid/a': 'https://example.invalid/a', 'http://example.invalid/a': null,
|
||||
'javascript:alert(1)': null, 'JAVASCRIPT:alert(1)': null, 'data:text/html,x': null, 'not a url': null, '//example.invalid': null };
|
||||
for (const [url, link] of Object.entries(urls)) {
|
||||
const out = renderReply(reply('t', [web(url)]));
|
||||
assert.equal(out.kind, 'reply', url);
|
||||
assert.equal(out.citations[0].link, link, url);
|
||||
assert.equal(out.citations[0].ref, url);
|
||||
test('a body without the reply shape shows a fixed line and none of its text', () => {
|
||||
const secret = 'free prose the guard would refuse';
|
||||
const bodies = [secret, JSON.stringify(secret), '{"items": [], "notice": "other"}', JSON.stringify({ items: [], notice: 'no-answer', extra: secret }),
|
||||
JSON.stringify({ items: [] }), JSON.stringify({ items: [{ kind: 'story', text: secret, cites: [] }] }),
|
||||
JSON.stringify({ items: [{ kind: 'fact', text: secret }] }), JSON.stringify({ items: SNAPSHOTS.fact, prose: secret }),
|
||||
JSON.stringify({ items: [{ kind: 'correction', text: secret, cites: [] }] }),
|
||||
JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [cite('a.md', [2, 1], 'q')] }] }),
|
||||
JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }] }] }),
|
||||
JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }] }] }),
|
||||
null, 42, '[]'];
|
||||
for (const body of bodies) {
|
||||
const out = renderItems(body, options);
|
||||
assert.deepEqual(out, { kind: 'unreadable', text: UNREADABLE }, String(body));
|
||||
assert.ok(!JSON.stringify(out).includes(secret));
|
||||
}
|
||||
});
|
||||
|
||||
test('a write notice renders its fixed line', () => {
|
||||
const body = op => JSON.stringify({ notice: 'library-write', op, path: 'Notes/Eyes.md', commit: B });
|
||||
assert.deepEqual(renderReply(body('write'), { name: 'Scribe' }),
|
||||
{ kind: 'notice', op: 'write', path: 'Notes/Eyes.md', commit: B, text: 'Scribe wrote Notes/Eyes.md @bbbbbbb', plain: 'Scribe wrote Notes/Eyes.md @bbbbbbb' });
|
||||
assert.equal(renderReply(body('delete'), { name: 'Scribe' }).text, 'Scribe deleted Notes/Eyes.md @bbbbbbb');
|
||||
assert.equal(renderReply(body('write')).text, 'An agent wrote Notes/Eyes.md @bbbbbbb');
|
||||
});
|
||||
|
||||
test('a body without the reply or notice shape is free text: the whole body, no citations', () => {
|
||||
const bodies = [
|
||||
JSON.stringify({ text: 't' }), JSON.stringify({ citations: [] }), JSON.stringify({ text: 1, citations: [] }),
|
||||
JSON.stringify({ text: 't', citations: [], extra: 1 }), JSON.stringify({ items: [] }),
|
||||
reply('t', [cite('a.md', [2, 1], 'q')]), reply('t', [cite('a.md', [0, 1], 'q')]), reply('t', [cite('a.md', [1], 'q')]),
|
||||
reply('t', [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }]), reply('t', [cite('a.md', [1, 1], 'q', A, { as: 'playground' })]),
|
||||
reply('t', [cite('a.md', [1, 1], 'q', A, { scope: 'canon' })]), reply('t', [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }]),
|
||||
reply('t', [{ url: 'https://x.invalid' }]), reply('t', [{ url: 'https://x.invalid', quote: 'q', agent: 'x/y' }]),
|
||||
JSON.stringify({ notice: 'library-write', op: 'move', path: 'a.md', commit: A }),
|
||||
JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: 'abc' }),
|
||||
JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: A, by: 'x' }),
|
||||
];
|
||||
for (const body of bodies) assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body }, body);
|
||||
});
|
||||
|
||||
test('an empty citations list is a reply with no cites', () => {
|
||||
assert.deepEqual(renderReply(reply('Grey.', [])), { kind: 'reply', text: 'Grey.', citations: [], plain: 'Grey.' });
|
||||
});
|
||||
@@ -53,3 +53,44 @@ test('C2-01c: a canon file linking into a draft gives R6, and into its own unive
|
||||
assert.deepEqual(codes(path), [], path);
|
||||
}
|
||||
});
|
||||
|
||||
// Supporting prefixes at three levels (C2-01d). Harbor's book-level prefix sits in both lists, and a
|
||||
// second book-level prefix is supporting only.
|
||||
const supporting = () => mapping(e => {
|
||||
e.universes.harbor.playground = ['Book One/Playground/', 'Book One/Sketches/'];
|
||||
e.universes.harbor.supporting = ['Book One/Playground/', 'Book One/Notes/', 'Harbor Notes/'];
|
||||
e.universes.idea = { kind: 'draft', name: 'New World', playground: ['Drafts/New World/'], supporting: ['Drafts/New World Notes/'] };
|
||||
return e;
|
||||
});
|
||||
|
||||
test('C2-01d: each supporting level gives its universe, and its book or null', () => {
|
||||
const m = supporting();
|
||||
assert.deepEqual(classify(m, 'Book One/Notes/Characters/Mara.md'),
|
||||
{ scope: 'supporting', universe: 'harbor', book: 'Book One', type: 'character', folder: 'Characters' });
|
||||
assert.deepEqual(classify(m, 'Harbor Notes/Places/Island.md'),
|
||||
{ scope: 'supporting', universe: 'harbor', book: null, type: 'place', folder: 'Places' });
|
||||
assert.deepEqual(classify(m, 'Harbor Notes/Loose.md'), { scope: 'supporting', universe: 'harbor', book: null, type: null, folder: null });
|
||||
assert.deepEqual(classify(m, 'Drafts/New World Notes/Chapters/Open.md'),
|
||||
{ scope: 'supporting', universe: 'idea', book: null, type: 'manuscript', folder: 'Chapters' });
|
||||
assert.deepEqual(classify(m, 'Drafts/New World/Ana.md'), { scope: 'playground', universe: 'idea', book: null, type: null, folder: null });
|
||||
assert.deepEqual(classify(m, 'Book One/Places/Dock.md'), { scope: 'canon', universe: 'harbor', book: 'Book One', type: 'place', folder: 'Places' });
|
||||
});
|
||||
|
||||
test('C2-01d: supporting comes before playground; a folder in both lists is supporting, one in the playground only stays playground', () => {
|
||||
const m = supporting();
|
||||
assert.deepEqual(classify(m, 'Book One/Playground/What If.md'),
|
||||
{ scope: 'supporting', universe: 'harbor', book: 'Book One', type: null, folder: null });
|
||||
assert.deepEqual(classify(m, 'Book One/Sketches/Research/Tides.md'),
|
||||
{ scope: 'playground', universe: 'harbor', book: 'Book One', type: 'research', folder: 'Research' });
|
||||
// A playground may hold a supporting prefix of its own entry: the inner folder is supporting.
|
||||
const nested = mapping(e => { e.universes.harbor.supporting = ['Book One/Playground/Notes/']; return e; });
|
||||
assert.equal(classify(nested, 'Book One/Playground/Notes/A.md').scope, 'supporting');
|
||||
assert.equal(classify(nested, 'Book One/Playground/A.md').scope, 'playground');
|
||||
});
|
||||
|
||||
test('C2-01d: only .md files are supporting; a .canvas or .base under a supporting prefix is unmapped', () => {
|
||||
const m = supporting();
|
||||
for (const path of ['Book One/Notes/Board.canvas', 'Harbor Notes/Table.base', 'Drafts/New World Notes/Map.canvas',
|
||||
'Harbor Notes/cover.png', 'Harbor Notesx/a.md'])
|
||||
assert.deepEqual(classify(m, path), { scope: 'unmapped' }, path);
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { LibraryError, supportingPath } from '../src/index.mjs';
|
||||
import { mapping } from './helpers.mjs';
|
||||
|
||||
// C2-01d (M12): the path checks an agent write passes. The tree is a listTree array built by hand,
|
||||
// with a symlink, a gitlink, case twins and an NFC name, so no git is needed.
|
||||
const m = mapping(e => {
|
||||
e.universes.harbor.playground = ['Book One/Playground/', 'Book One/Sketches/'];
|
||||
e.universes.harbor.supporting = ['Book One/Playground/', 'Book One/Notes/', 'Harbor Notes/'];
|
||||
e.universes.second.supporting = ['Second Notes/'];
|
||||
e.universes.idea = { kind: 'draft', name: 'New World', playground: ['Drafts/New World/'], supporting: ['Drafts/New World Notes/'] };
|
||||
return e;
|
||||
});
|
||||
const blob = (path, mode = '100644') => ({ mode, type: mode === '160000' ? 'commit' : 'blob', oid: '0'.repeat(40), size: 1, path });
|
||||
const tree = [
|
||||
blob('Book One/Characters/Mara Vell.md'),
|
||||
blob('Book One/Notes/Existing.md'),
|
||||
blob('Book One/Notes/Linked', '120000'),
|
||||
blob('Book One/Notes/Vendor', '160000'),
|
||||
blob('Book One/Notes/Pointer.md', '120000'),
|
||||
blob('Book One/Notes/Case/Twin.md'),
|
||||
blob('Book One/Notes/Café.md'),
|
||||
blob('Harbor Notes/Places/Island.md'),
|
||||
];
|
||||
const refuses = (universe, path, pattern) => assert.throws(() => supportingPath(m, universe, path, tree),
|
||||
e => e instanceof LibraryError && e.code === 'library-write-story-path' && pattern.test(e.message), JSON.stringify(path));
|
||||
|
||||
test('C2-01d: a path passes at book level, universe level, in a draft and in a folder in both lists', () => {
|
||||
for (const [universe, path] of [['harbor', 'Book One/Notes/Characters/New.md'], ['harbor', 'Book One/Notes/Existing.md'],
|
||||
['harbor', 'Harbor Notes/Places/Island.md'], ['harbor', 'Harbor Notes/Loose.md'], ['idea', 'Drafts/New World Notes/Ana.md'],
|
||||
['harbor', 'Book One/Playground/What If.md'], ['second', 'Second Notes/A.md'], ['harbor', 'Book One/Notes/Café.md']])
|
||||
assert.equal(supportingPath(m, universe, path, tree), path);
|
||||
assert.equal(supportingPath(m, 'harbor', 'Book One/Notes/x.md', []), 'Book One/Notes/x.md');
|
||||
});
|
||||
|
||||
test('C2-01d: a path of exactly 1024 bytes passes, and 1025 bytes made of multi-byte characters refuses', () => {
|
||||
const prefix = 'Harbor Notes/';
|
||||
const exact = `${prefix}${'a'.repeat(1024 - prefix.length - 3)}.md`;
|
||||
assert.equal(Buffer.byteLength(exact), 1024);
|
||||
assert.equal(supportingPath(m, 'harbor', exact, tree), exact);
|
||||
// é is two bytes: 1025 bytes but far fewer characters.
|
||||
const name = 'é'.repeat((1025 - prefix.length - 3 - 1) / 2);
|
||||
const long = `${prefix}${name}a.md`;
|
||||
assert.equal(Buffer.byteLength(long), 1025);
|
||||
assert.ok(long.length < 1024);
|
||||
refuses('harbor', long, /is longer than 1024 bytes/);
|
||||
});
|
||||
|
||||
test('C2-01d: a path outside the universe\'s supporting folders refuses', () => {
|
||||
const outside = /is not under a supporting folder of universe/;
|
||||
for (const path of ['Inbox Note.md', 'Book One/Notes/Board.canvas', 'Book One/Notes/Table.base', 'Book One/Notes/cover.png',
|
||||
'Book One/Characters/Mara Vell.md', 'Book One/Sketches/Idea.md', 'Drafts/New World/Ana.md', 'Second Notes/A.md', 'Book One/Notes.md'])
|
||||
refuses('harbor', path, outside);
|
||||
refuses('idea', 'Book One/Notes/A.md', /universe "idea"/);
|
||||
refuses('nowhere', 'Book One/Notes/A.md', /universe "nowhere"/);
|
||||
});
|
||||
|
||||
test('C2-01d: malformed paths refuse by class, in order', () => {
|
||||
refuses('harbor', '/Book One/Notes/A.md', /"\/Book One\/Notes\/A.md" is absolute/);
|
||||
for (const path of ['Book One/Notes/../Notes/A.md', 'Book One/Notes/./A.md', 'Book One//Notes/A.md', 'Book One/Notes/A/', 'Book One/Notes/..'])
|
||||
refuses('harbor', path, /has an empty, \. or \.\. segment/);
|
||||
refuses('harbor', 'Book One/Notes/A\\B.md', /holds a backslash/);
|
||||
for (const c of ['\u0000', '\u0001', '\n', '\u001f', '\u007f', '\u0085', '\u009f'])
|
||||
assert.throws(() => supportingPath(m, 'harbor', `Book One/Notes/A${c}.md`, tree),
|
||||
e => e.code === 'library-write-story-path' && e.message === 'library-write-story-path: the path holds a control character');
|
||||
for (const path of [7, null, undefined, 1n, ['Book One/Notes/A.md']]) refuses('harbor', path, /the path must be a string/);
|
||||
// A backslash in a path that is also absolute names the backslash: the order is fixed.
|
||||
refuses('harbor', '/a\\b.md', /holds a backslash/);
|
||||
assert.throws(() => supportingPath(m, 'harbor', 'Book One/Notes/A.md', { 'Book One/Notes/A.md': '100644' }), TypeError);
|
||||
assert.throws(() => supportingPath(m, 'harbor', 'Book One/Notes/A.md'), TypeError);
|
||||
});
|
||||
|
||||
test('C2-01d: a symlink or gitlink on the way, or at the path itself, refuses', () => {
|
||||
refuses('harbor', 'Book One/Notes/Linked/A.md', /goes through the symlink or gitlink "Book One\/Notes\/Linked"/);
|
||||
refuses('harbor', 'Book One/Notes/Vendor/Deep/A.md', /goes through the symlink or gitlink "Book One\/Notes\/Vendor"/);
|
||||
refuses('harbor', 'Book One/Notes/Pointer.md', /goes through the symlink or gitlink "Book One\/Notes\/Pointer.md"/);
|
||||
});
|
||||
|
||||
test('C2-01d: a name that differs from a tree name only in case or Unicode form refuses, at the file or any folder', () => {
|
||||
refuses('harbor', 'Book One/Notes/existing.md', /differs only in case or Unicode form from "Book One\/Notes\/Existing.md"/);
|
||||
refuses('harbor', 'Book One/Notes/case/New.md', /differs only in case or Unicode form from "Book One\/Notes\/Case"/);
|
||||
refuses('harbor', 'Book One/Notes/CASE/Twin.md', /from "Book One\/Notes\/Case"/);
|
||||
refuses('harbor', 'Harbor notes/A.md', /is not under a supporting folder/);
|
||||
// NFD e + combining acute against the NFC name in the tree: one file on APFS.
|
||||
refuses('harbor', 'Book One/Notes/Café.md', /differs only in case or Unicode form from "Book One\/Notes\/Café.md"/);
|
||||
// A tree that already holds case twins refuses a write to either (R7).
|
||||
const twins = [...tree, blob('Book One/Notes/existing.md')];
|
||||
for (const path of ['Book One/Notes/Existing.md', 'Book One/Notes/existing.md'])
|
||||
assert.throws(() => supportingPath(m, 'harbor', path, twins), e => e.code === 'library-write-story-path' && /differs only in case/.test(e.message));
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -70,12 +70,23 @@ test('with --mapping the survey adds scope, resolver step and problem counts', a
|
||||
assert.equal(out.code, 0, out.stderr);
|
||||
const tail = out.stdout.trimEnd().split('\n').slice(-3);
|
||||
assert.deepEqual(tail, [
|
||||
'scopes canon 5, playground 1, unmapped 3; indexed 6',
|
||||
'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6',
|
||||
'resolved root 2, vault-prefix 1, relative 2, shortest 3, unresolved 0',
|
||||
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0']);
|
||||
for (const s of NEVER) assert.ok(!out.stdout.includes(s), s);
|
||||
});
|
||||
|
||||
test('C2-01d: with --mapping the survey counts supporting files', async t => {
|
||||
const r = repo(t);
|
||||
const entry = JSON.parse(readFileSync(join(FIXTURES, 'mapping.json'), 'utf8'));
|
||||
entry.universes.harbor.supporting = ['Book One/Places/'];
|
||||
const file = join(r.root, 'supporting.json');
|
||||
writeFileSync(file, JSON.stringify(entry));
|
||||
const out = await run(['survey', r.repo, '--mapping', file]);
|
||||
assert.equal(out.code, 0, out.stderr);
|
||||
assert.match(out.stdout, /^scopes canon 4, playground 1, supporting 1, unmapped 3; indexed 6$/m);
|
||||
});
|
||||
|
||||
test('survey exit codes: usage 4, bad mapping 3, unreadable repo 2', async t => {
|
||||
const r = repo(t);
|
||||
for (const args of [[], ['scan', r.repo], ['survey'], ['survey', r.repo, '--mapping'], ['survey', r.repo, '--other'], ['survey', r.repo, r.repo]])
|
||||
|
||||
@@ -189,7 +189,5 @@ const STATUS = {
|
||||
// leaves the remote unreachable, like no bus to ask.
|
||||
'library-sync-busy': 409, 'library-diverged': 409, 'library-accept-stale': 409, 'library-accept-invalid': 400,
|
||||
'library-credential-missing': 503, 'library-fetch-failed': 503,
|
||||
// Row 225 (C2-10b): a Revert of a commit that isn't an agent's is hers to read, like stale-base.
|
||||
'library-revert-not-agent': 409,
|
||||
};
|
||||
export const busStatus = code => STATUS[code] ?? 502;
|
||||
@@ -68,14 +68,14 @@ try {
|
||||
// the system config, Business can't resolve authority and Settings has
|
||||
// no notifier. A refusal printed here names no path (redactor).
|
||||
// --send without a bus refuses to start rather than serve a composer that can't send.
|
||||
let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, libraryRevert = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, decisionWrite = null, busLine, system = null;
|
||||
let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, decisionWrite = null, busLine, system = null;
|
||||
let processing = null, ownerCall = null;
|
||||
try {
|
||||
system = loadSystem();
|
||||
const { dataRoot } = system;
|
||||
const call = ownerCall = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
|
||||
// FED R6 (#1800): the bus-backed options, shared with each granted user's session (console-bus.mjs).
|
||||
({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, libraryRevert, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite } = consoleBus(call, { send: sendOn }));
|
||||
({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite } = consoleBus(call, { send: sendOn }));
|
||||
// Row 252 (#1753): Approve and Deny on grant requests, through the console wrapper, only with
|
||||
// --sso and --send.
|
||||
if (ssoOn && sendOn) decisionWrite = consoleCall({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
|
||||
@@ -107,7 +107,7 @@ try {
|
||||
}
|
||||
let server;
|
||||
try {
|
||||
server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, libraryRevert, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, decisionWrite, userOptions, federation: system?.federation ?? null });
|
||||
server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, decisionWrite, userOptions, federation: system?.federation ?? null });
|
||||
} catch (err) { sso?.close(); throw err; }
|
||||
let testerServer = null;
|
||||
if (testerSocket) {
|
||||
|
||||
@@ -30,10 +30,10 @@ export function consoleBus(call, { send: sendOn = false, owner = null } = {}) {
|
||||
projectFiles: args => call('project.files', args),
|
||||
projectFile: args => call('project.file', args),
|
||||
libraryFile: args => call('library.file', args), libraryStatus: args => call('library.status', args),
|
||||
send: null, librarySync: null, libraryRevert: null, settingsWrite: null, projectSave: null, nameWrite: null, assistantStop: null, instancesWrite: null, routesWrite: null, agentWrite: null,
|
||||
send: null, librarySync: null, settingsWrite: null, projectSave: null, nameWrite: null, assistantStop: null, instancesWrite: null, routesWrite: null, agentWrite: null,
|
||||
};
|
||||
if (sendOn) {
|
||||
o.send = args => call('message.send', args); o.librarySync = args => call('library.sync', args); o.libraryRevert = args => call('library.revert', args); o.settingsWrite = args => call('limits.set', args);
|
||||
o.send = args => call('message.send', args); o.librarySync = args => call('library.sync', args); o.settingsWrite = args => call('limits.set', args);
|
||||
o.projectSave = args => call('project.file.save', args); o.nameWrite = args => call('name.set', args); o.assistantStop = args => call('assistant.stop', args);
|
||||
o.instancesWrite = args => call('instances.set', args); o.routesWrite = args => call('routes.set', args); o.agentWrite = agentCaller(call, AGENT_VERBS);
|
||||
}
|
||||
|
||||
@@ -23,20 +23,3 @@
|
||||
.chat-session-controls label { display: block; }
|
||||
.chat-session-controls select, .chat-session-controls input { max-width: 100%; }
|
||||
.chat-session-controls form { margin: .6rem 0; }
|
||||
|
||||
/* Row 225 (C2-10b, design 7.4): a reply's cites, a write notice and the library status line. The
|
||||
badge and labels are text, so they read the same without colour. */
|
||||
.chat-cites { list-style: none; margin: .5rem 0 0; padding-inline-start: 1rem; display: grid; gap: .35rem; }
|
||||
.chat-log .chat-cites li { padding: 0; border-bottom: 0; overflow-wrap: anywhere; font-size: .92rem; }
|
||||
.chat-cites code { font-family: var(--mono); font-size: .85rem; }
|
||||
.chat-scope, .chat-label { display: inline-block; padding: 0 .4rem; border: 1px solid var(--line); border-radius: var(--r); font-size: .78rem; }
|
||||
.chat-scope-canon { border-color: var(--success); color: var(--success); }
|
||||
.chat-scope-playground { border-color: var(--accent); color: var(--accent); }
|
||||
.chat-scope-web { border-color: var(--action); color: var(--action); }
|
||||
.chat-agent-written { color: var(--muted); }
|
||||
.chat-changed { border-color: var(--warning); color: var(--warning); }
|
||||
.chat-notice { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; white-space: normal; }
|
||||
.chat-revert-receipt { overflow-wrap: anywhere; }
|
||||
.chat-library { font-size: .9rem; margin-block: .5rem; overflow-wrap: anywhere; }
|
||||
.chat-library p { margin: 0; }
|
||||
.chat-library-problems ul { margin: .4rem 0 0; padding-inline-start: 1.25rem; }
|
||||
@@ -17,22 +17,11 @@
|
||||
if (!forms.has(key)) forms.set(key, { name: '', repo: '', instance: '', instanceChosen: false, receipt: '', pending: false });
|
||||
return forms.get(key);
|
||||
};
|
||||
// Row 225 (C2-10b, design 7.4): a reply renders through the library's renderReply, the one renderer the
|
||||
// Discord relay shares. Until it loads, or if it can't, a body shows as its text. Badges, the
|
||||
// agent-written label and "changed since" come from the bus's stored check (C2-06), never the body.
|
||||
const SHA = /^[0-9a-f]{40}$/, CODE = /^[a-z][a-z-]{0,63}$/, NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/;
|
||||
// The library verbs name the business project, which is the chat project's repository link. These
|
||||
// codes mean that repository is no library, or this page may not read it: no status line at all.
|
||||
const NO_LIBRARY = ['project-not-declared', 'project-not-configured', 'not-configured', 'owner-only'];
|
||||
const libraryOf = (c, t) => { const repo = c?.projects.find(p => p.id === t?.project)?.repo; return typeof repo === 'string' && NAME.test(repo) ? repo : null; };
|
||||
let renderReply = null;
|
||||
import('/render.mjs').then(m => { renderReply = m.renderReply; if (current && !out()) paint(); }, () => {});
|
||||
const libraries = new Map(), reverts = new Map();
|
||||
class ChatError extends Error {
|
||||
constructor(code, status = 0, sent) { super(code); this.code = code; this.status = status; this.sent = sent; }
|
||||
}
|
||||
function forget() {
|
||||
generation++; controller?.abort(); states.clear(); forms.clear(); libraries.clear(); reverts.clear();
|
||||
generation++; controller?.abort(); states.clear(); forms.clear();
|
||||
if (current) current.container.replaceChildren();
|
||||
current = null;
|
||||
}
|
||||
@@ -91,93 +80,11 @@
|
||||
const reason = !Number.isSafeInteger(selected?.revision) ? 'Safe clear is unavailable until its dependency is installed.' : '';
|
||||
return `<section class="chat-session-controls" aria-label="Sessions"><label>Session<select id="chat-session">${c.sessions.map(r => `<option value="${esc(r.id)}"${r.id === c.session ? ' selected' : ''}>${txt(r.name)}${r.default ? ' (default)' : ''} — ${txt(r.state ?? 'status unavailable')}</option>`).join('')}</select></label><p id="chat-compactions">Compactions: ${txt(countLabel(selected?.compactions))}</p><p>${txt(selected?.state ?? 'Session status unavailable')}${UUID.test(selected?.successor) ? ` · <a href="${sessionPath(t.id, selected.successor)}">Open fresh successor</a>` : ''}</p><form id="chat-session-form" class="chat-create" data-key="session:${esc(t.id)}"><label>New session name<input id="chat-session-name" data-chat-field="name" value="${esc(f.name)}" maxlength="200" required${disabled(f.pending)}></label><button class="btn" type="submit"${disabled(f.pending)}>New session</button><p role="status">${txt(f.receipt)}</p></form><button class="btn" type="button" id="chat-clear"${disabled(clear.pending || !!reason || selected?.state && selected.state !== 'active')}>Clear context</button>${reason ? `<p>${txt(reason)}</p>` : ''}<p id="chat-clear-receipt" role="status">${txt(clear.receipt)}</p><form id="chat-operation-form"><label>Check clear operation<input id="chat-operation" value="${esc(clear.op ?? '')}" maxlength="160"${clear.pending ? ' disabled' : ''}></label><button class="btn" type="submit">Check status</button><button class="btn" type="button" id="chat-cancel"${disabled(clear.pending || !!reason || !clear.op || ['cancelled', 'failed'].includes(clear.record?.state) || clear.record?.start === 'started')}>Cancel clear</button></form></section>`;
|
||||
}
|
||||
// A cite is changed when the file's last commit at the served tip (`last`, read with the page) is not
|
||||
// its last commit at the cited one (`file`, stored with the check). Either one missing marks nothing.
|
||||
const changedBy = check => (cite, i) => {
|
||||
const e = Array.isArray(check?.citations) ? check.citations[i] : null;
|
||||
return SHA.test(e?.file ?? '') && SHA.test(e?.last ?? '') && e.file !== e.last;
|
||||
};
|
||||
function cites(list) {
|
||||
return `<ul class="chat-cites">${list.map(c => `<li>${c.badge ? `<span class="chat-scope chat-scope-${c.badge}">${txt(c.badge)}</span> ` : ''}${c.link ? `<a href="${esc(c.link)}" rel="noopener noreferrer" target="_blank">${txt(c.ref)}</a>` : `<code>${txt(c.ref)}</code>`} <q>${txt(c.quote)}</q>${c.agent ? ` <span class="chat-label chat-agent-written">agent-written: ${txt(c.agent)}</span>` : ''}${c.changed ? ' <span class="chat-label chat-changed">changed since</span>' : ''}</li>`).join('')}</ul>`;
|
||||
}
|
||||
function messageBody(m) {
|
||||
const r = renderReply?.(m.body, { check: m.check ?? null, name: m.from_role, changed: changedBy(m.check) });
|
||||
if (r?.kind === 'reply') return `<div class="chat-body">${txt(r.text)}</div>${r.citations.length ? cites(r.citations) : ''}`;
|
||||
if (r?.kind === 'notice') {
|
||||
const v = reverts.get(r.commit) ?? {};
|
||||
return `<div class="chat-body chat-notice"><span>${txt(r.text)}</span> <button class="btn" type="button" data-revert="${esc(r.commit)}"${disabled(v.pending)}>Revert</button> <span class="chat-revert-receipt" role="status">${txt(v.receipt ?? '')}</span></div>`;
|
||||
}
|
||||
return `<div class="chat-body">${txt(m.body)}</div>`;
|
||||
}
|
||||
const ago = iso => {
|
||||
const minutes = Math.floor((Date.now() - Date.parse(iso)) / 60000);
|
||||
return !Number.isFinite(minutes) ? 'at an unknown time' : minutes < 1 ? 'just now' : minutes < 60 ? `${minutes} min ago` : minutes < 2880 ? `${Math.floor(minutes / 60)} h ago` : `${Math.floor(minutes / 1440)} days ago`;
|
||||
};
|
||||
// library.status (C2-04): the served commit and when it was last fetched, a held divergence, the last
|
||||
// fetch error, the reply limits in force (C2-06) and the R-problems, listed one click away.
|
||||
function libraryLine(project) {
|
||||
const l = libraries.get(project);
|
||||
if (!l || l.hidden) return '';
|
||||
if (l.error) return `<div class="chat-library"><p role="status">Library status unavailable (${txt(l.error)}).</p></div>`;
|
||||
const v = l.value, parts = [];
|
||||
parts.push(SHA.test(v.served ?? '') ? `Library at <code>${txt(v.served.slice(0, 7))}</code>` : 'Library not served yet');
|
||||
if (typeof v.lastFetch === 'string') parts.push(`fetched ${txt(ago(v.lastFetch))}`);
|
||||
if (SHA.test(v.diverged?.commit ?? '')) parts.push(`<b>library-diverged</b> at <code>${txt(v.diverged.commit.slice(0, 7))}</code>`);
|
||||
if (CODE.test(v.lastError?.code ?? '')) parts.push(`fetch error <code>${txt(v.lastError.code)}</code>`);
|
||||
const limits = v.replyLimits;
|
||||
if (limits && typeof limits === 'object') {
|
||||
parts.push(`citations ${limits.requireCitation === true ? 'required' : 'optional'}${Number.isSafeInteger(limits.citationsPerReply) ? `, up to ${limits.citationsPerReply} per reply` : ''}`);
|
||||
const fell = Array.isArray(limits.fallback) ? limits.fallback.filter(k => typeof k === 'string') : [];
|
||||
if (fell.length) parts.push(`limits fell back: ${fell.map(k => `<code>${txt(k)}</code>`).join(', ')}`);
|
||||
if (CODE.test(limits.problem ?? '')) parts.push(`<code>${txt(limits.problem)}</code>`);
|
||||
}
|
||||
const problems = Array.isArray(v.problems) ? v.problems : [];
|
||||
if (!problems.length) parts.push('no problems');
|
||||
const list = problems.length ? `<details class="chat-library-problems"><summary>${problems.length} problem${problems.length === 1 ? '' : 's'}</summary><ul>${problems.map(p => `<li><code>${txt(p?.code)}</code> ${txt(p?.path)}${Number.isSafeInteger(p?.line) ? ':' + p.line : ''} ${txt(p?.message)}</li>`).join('')}</ul></details>` : '';
|
||||
return `<div class="chat-library"><p role="status">${parts.join(' · ')}</p>${list}</div>`;
|
||||
}
|
||||
async function loadLibrary(project, g, signal) {
|
||||
if (project === null) return;
|
||||
let entry;
|
||||
try {
|
||||
const res = await fetch('/api/library/status?project=' + encodeURIComponent(project), { cache: 'no-store', redirect: 'manual', signal });
|
||||
if (res.type === 'opaqueredirect' || res.status === 401) { signOut(); return; }
|
||||
const data = await res.json().catch(() => null);
|
||||
entry = res.ok && data && typeof data === 'object' && !Array.isArray(data) ? { value: data } : { error: CODE.test(data?.error ?? '') ? data.error : 'invalid-response' };
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return;
|
||||
entry = { error: 'unreachable' };
|
||||
}
|
||||
if (g !== generation || out()) return;
|
||||
libraries.set(project, NO_LIBRARY.includes(entry.error) ? { hidden: true } : entry);
|
||||
paint();
|
||||
}
|
||||
// design (writes): Revert is library.revert {commit}, her act. A refusal shows its code; before
|
||||
// C2-02's verb exists that is the bus's refusal of the verb.
|
||||
async function revert(commit) {
|
||||
const c = current, t = c?.threads.find(t => t.id === c.id), v = reverts.get(commit) ?? {};
|
||||
const project = libraryOf(c, t);
|
||||
if (!c || !c.canSend || out() || v.pending || !SHA.test(commit) || project === null) return;
|
||||
if (!window.confirm(`Revert ${commit.slice(0, 7)}? This makes a new commit in the library that puts the file back as it was before it.`)) return;
|
||||
const unknown = 'Outcome unknown. Read the library status before trying again; nothing was retried.';
|
||||
reverts.set(commit, { pending: true, receipt: 'Reverting…' }); paint();
|
||||
let receipt;
|
||||
try {
|
||||
const res = await fetch('/api/library/revert', { method: 'POST', cache: 'no-store', redirect: 'manual', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ project, commit }) });
|
||||
if (res.type === 'opaqueredirect' || res.status === 401) { signOut(); return; }
|
||||
const data = await res.json().catch(() => null);
|
||||
receipt = res.ok && SHA.test(data?.commit ?? '') ? `Reverted in ${data.commit.slice(0, 7)}.`
|
||||
: data?.sent === false && typeof data.error === 'string' ? `Not reverted (${inert(data.error.slice(0, 80))}).` : unknown;
|
||||
} catch { receipt = unknown; }
|
||||
if (out()) return;
|
||||
reverts.set(commit, { pending: false, receipt });
|
||||
if (current) { paint(); void loadLibrary(project, generation); }
|
||||
}
|
||||
function conversation(t) {
|
||||
const s = state(current.key);
|
||||
const messages = s.rows.map(m => `<li data-message="${esc(m.id)}"><div class="chat-message-meta"><strong>${txt(m.from_role)}</strong> ${stamp(m.at)}</div>${messageBody(m)}</li>`).join('');
|
||||
const messages = s.rows.map(m => `<li data-message="${esc(m.id)}"><div class="chat-message-meta"><strong>${txt(m.from_role)}</strong> ${stamp(m.at)}</div><div class="chat-body">${txt(m.body)}</div></li>`).join('');
|
||||
const receipt = s.receipt;
|
||||
return `<section class="chat-conversation" data-thread="${esc(t.id)}" data-session="${esc(current.session)}"><p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>${heading(t.name)}${libraryLine(libraryOf(current, t))}${sessionControls(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p><button class="btn" type="button" id="chat-resume"${disabledSession(s.pending)}>Resume assistant</button><ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || '<li class="muted">No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div><form id="chat-send-form"><label for="chat-body">Message this thread</label><textarea id="chat-body" rows="4"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><button class="btn primary" type="submit"${disabledSession(s.pending)}>Send</button></form><p id="chat-receipt" role="status">${txt(receipt)}</p></section>`;
|
||||
return `<section class="chat-conversation" data-thread="${esc(t.id)}" data-session="${esc(current.session)}"><p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>${heading(t.name)}${sessionControls(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p><button class="btn" type="button" id="chat-resume"${disabledSession(s.pending)}>Resume assistant</button><ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || '<li class="muted">No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div><form id="chat-send-form"><label for="chat-body">Message this thread</label><textarea id="chat-body" rows="4"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><button class="btn primary" type="submit"${disabledSession(s.pending)}>Send</button></form><p id="chat-receipt" role="status">${txt(receipt)}</p></section>`;
|
||||
}
|
||||
function capture() {
|
||||
if (!current) return null;
|
||||
@@ -326,11 +233,7 @@
|
||||
}
|
||||
} catch (err) { if (err.name === 'AbortError') return; }
|
||||
}
|
||||
if (g === generation) {
|
||||
paint(navigated);
|
||||
const t = kind === 'thread' ? current.threads.find(t => t.id === id) : null;
|
||||
if (t) void loadLibrary(libraryOf(current, t), g, signal);
|
||||
}
|
||||
if (g === generation) paint(navigated);
|
||||
} catch (err) {
|
||||
if (g !== generation || out() || err.name === 'AbortError' || err.code === 'signed-out') return;
|
||||
current.error = readError(err);
|
||||
@@ -437,8 +340,6 @@
|
||||
if (event.target.id === 'chat-cancel') void cancelClear();
|
||||
if (event.target.id === 'chat-clear') void clearContext();
|
||||
if (event.target.id === 'chat-next') void refresh(true);
|
||||
const revertButton = event.target.closest?.('[data-revert]');
|
||||
if (revertButton) void revert(revertButton.dataset.revert);
|
||||
if (event.target.id === 'chat-new') { state(current.key ?? current.id).tailRequested = true; state(current.key ?? current.id).follow = true; paint(); }
|
||||
});
|
||||
document.addEventListener('change', event => {
|
||||
|
||||
@@ -32,8 +32,6 @@ const files = new Map([
|
||||
...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'processing.js', 'agents.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
|
||||
...['shared/app.css', 'tokens.css', 'console.css', 'live.css', 'bus.css', 'shell.css', 'chat.css', 'processing.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
|
||||
['/editor.js', ['editor.js', 'text/javascript; charset=utf-8']],
|
||||
// Row 225 (C2-10b): the one reply renderer, shared with the Discord relay, imported by chat.js.
|
||||
['/render.mjs', ['../../../library/src/render.mjs', 'text/javascript; charset=utf-8']],
|
||||
['/editor.css', ['editor.css', 'text/css; charset=utf-8']],
|
||||
['/icons.svg', ['icons.svg', 'image/svg+xml']],
|
||||
...[400, 500, 600, 700].map(w => [`/assets/fonts/manrope-${w}.woff2`, [`assets/fonts/manrope-${w}.woff2`, 'font/woff2']]),
|
||||
@@ -44,7 +42,6 @@ const files = new Map([
|
||||
const USER_FILES = new Map([
|
||||
['/', ['user.html', 'text/html; charset=utf-8']],
|
||||
...['user.js', 'chat.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
|
||||
['/render.mjs', files.get('/render.mjs')],
|
||||
...['tokens.css', 'console.css', 'chat.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
|
||||
...[...files].filter(([p]) => p.startsWith('/assets/fonts/') || p === '/icons.svg'),
|
||||
]);
|
||||
@@ -599,55 +596,12 @@ async function projectFileRead(res, read, path, search) {
|
||||
return json(res, busStatus(code), { error: code, message: err instanceof BusReadError ? err.message : code });
|
||||
}
|
||||
}
|
||||
// Row 225 (C2-10b, design 7.4): the chat's library status line and a write notice's Revert. The status
|
||||
// is a read like the file list. The revert is her write (C2-02's `library.revert`), with the save's
|
||||
// boundary: header user, exact Origin, same-origin Fetch Metadata, JSON, strict keys. Until the verb
|
||||
// exists the bus refuses it, and the page shows that code.
|
||||
const REVERT_KEYS = ['project', 'commit'];
|
||||
async function libraryRevertSend(req, res, { libraryRevert, preview, log }) {
|
||||
let target = null;
|
||||
const line = outcome => log(`${new Date().toISOString()} library-revert user=${JSON.stringify(preview.user)} project=${quoted(target?.project)} commit=${quoted(target?.commit)} ${outcome}`);
|
||||
const refuse = (status, error, message, sent = false) => { line(`error=${error}`); return json(res, status, { error, message, sent }); };
|
||||
if (!signedIn(req, preview)) return json(res, 401, { error: 'sign-in required', message: 'sign in again', sent: false });
|
||||
if (one(req, 'origin') !== ownOrigin(preview)) return refuse(403, 'cross-origin request refused', 'a revert needs the Console\'s own Origin');
|
||||
if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site request refused', 'a revert needs same-origin Fetch Metadata');
|
||||
let value;
|
||||
try { value = JSON.parse((await body(req)).toString('utf8')); } catch (err) { return refuse(400, 'invalid-request', err.message); }
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== REVERT_KEYS.length ||!REVERT_KEYS.every(k => typeof value[k] === 'string') || !PATH_SHAPE.test(value.project) || !COMMIT.test(value.commit)) {
|
||||
return refuse(400, 'invalid-request', 'a revert takes a project and a 40-hex commit, nothing else');
|
||||
}
|
||||
target = value;
|
||||
let result;
|
||||
try { result = await libraryRevert({ project: value.project, commit: value.commit }); } catch (err) {
|
||||
const { code, sent } = sendOutcome(err);
|
||||
return sent === false
|
||||
? refuse(busStatus(code), code, `not reverted: the bus refused it (${code}).`)
|
||||
: refuse(503, 'outcome-unknown', 'the revert may have been made; nothing was retried. Read the library status before trying again', 'unknown');
|
||||
}
|
||||
if (!COMMIT.test(result?.commit) || result.project !== value.project || result.reverted !== value.commit || typeof result.path !== 'string') return refuse(503, 'outcome-unknown', 'the bus answered without the new commit; the revert may have been made', 'unknown');
|
||||
line(`commit=${result.commit}`);
|
||||
return json(res, 200, { project: value.project, path: result.path, reverted: result.reverted, commit: result.commit, at: new Date().toISOString() });
|
||||
}
|
||||
async function libraryStatusRead(res, read, search) {
|
||||
const query = new URLSearchParams(search);
|
||||
if ([...query.keys()].some(k => k !== 'project') || query.getAll('project').length !== 1 || !PATH_SHAPE.test(query.get('project')))
|
||||
return json(res, 400, { error: 'invalid-request', message: 'a library status read takes a project' });
|
||||
if (!read) return json(res, 503, { error: 'not-configured', message: 'the Console has no bus configured' });
|
||||
try {
|
||||
const result = await read({ project: query.get('project') });
|
||||
if (result === null || typeof result !== 'object' || Array.isArray(result) || result.project !== query.get('project')) throw new BusReadError('invalid-response');
|
||||
return json(res, 200, { ...result, at: new Date().toISOString() });
|
||||
} catch (err) {
|
||||
const code = err instanceof BusReadError ? err.code : 'invalid-response';
|
||||
return json(res, busStatus(code), { error: code, message: err instanceof BusReadError ? err.message : code });
|
||||
}
|
||||
}
|
||||
// `send` is the bus write (row 76): `args => Promise<{id}>`, only with the preview mode or `sso`.
|
||||
// `sso` is createSso() from sso.mjs (Q175 phase 1): the human listener signs people in itself.
|
||||
// `log` takes one line per send attempt that got past sign-in; never the body. `fieldsFile` is for
|
||||
// tests; the field table is otherwise the committed packages/tasks/fields.json.
|
||||
// `agentWrite` (row 194, D7) is `(verb, args) => Promise` for the Agents section's verbs and instance.request.
|
||||
export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, libraryStatus = null, libraryRevert = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) {
|
||||
export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) {
|
||||
if (!isLoopback(host)) throw new Error('refusing to bind to non-loopback host');
|
||||
if (host === 'localhost') host = '127.0.0.1';
|
||||
if (tester && preview !== null) throw new Error('tester listener cannot use preview mode');
|
||||
@@ -655,7 +609,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (sso !== null && (preview !== null || proof !== null)) throw new Error('--sso cannot be combined with --auth-header, --allow-user or --proof-*');
|
||||
if (tester && host !== '127.0.0.1') throw new Error('tester listener must bind 127.0.0.1');
|
||||
// Row 177 (#1675): chat is allowed. The tester listener's chat binds the tester, so the bus keeps it on tester threads.
|
||||
if (tester && [send, settingsWrite, projectSave, libraryRevert, assistantStop].some(value => value !== null)) throw new Error('tester listener permits only instance, route, name and chat writes');
|
||||
if (tester && [send, settingsWrite, projectSave, assistantStop].some(value => value !== null)) throw new Error('tester listener permits only instance, route, name and chat writes');
|
||||
if (tester && !testerSocket) throw new Error('tester listener requires private socket');
|
||||
if (!tester && testerSocket) throw new Error('tester socket requires tester listener');
|
||||
// Row 252 (#1753, design section 4): Console decision answers need --sso and --send, never the tester.
|
||||
@@ -665,7 +619,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
// `userOptions` (FED R6, #1800) is `session => options`, the bus-backed options over that session
|
||||
// user's own transport (console-bus.mjs). Only a signed-in listener has user sessions.
|
||||
if (userOptions !== null && (sso === null || typeof userOptions !== 'function')) throw new Error('user sessions need --sso');
|
||||
const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files };
|
||||
const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files };
|
||||
const hub = federationView(federation);
|
||||
// An object with none of the three is refused too: asking for the preview never serves without it.
|
||||
if (preview !== null && !(preview = previewOptions({ publicHost: preview.host, authHeader: preview.header, allowUser: preview.user }))) {
|
||||
@@ -675,7 +629,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso');
|
||||
const server = createServer(async (req, res) => {
|
||||
// A granted user's request rebinds these below; every other request keeps the owner's.
|
||||
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false;
|
||||
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false;
|
||||
res.setHeader('x-content-type-options', 'nosniff');
|
||||
res.setHeader('referrer-policy', 'no-referrer');
|
||||
// Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and
|
||||
@@ -742,7 +696,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' });
|
||||
if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' });
|
||||
({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));
|
||||
reads = null; upstream = null; libraryStatus = null; libraryRevert = null; files = USER_FILES; granted = true;
|
||||
reads = null; upstream = null; files = USER_FILES; granted = true;
|
||||
}
|
||||
}
|
||||
if (named && preview !== null) {
|
||||
@@ -794,11 +748,6 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if ((!tester && (!send || !named)) || !routesWrite) return json(res, 403, { error: 'read-only', sent: false });
|
||||
return routesSend(req, res, { routesWrite, preview: writer, log });
|
||||
}
|
||||
if (path === '/api/library/revert') {
|
||||
if (req.method !== 'POST') return json(res, 405, { error: 'method not allowed' });
|
||||
if (!send || !libraryRevert || !named) return json(res, 403, { error: 'read-only', sent: false });
|
||||
return libraryRevertSend(req, res, { libraryRevert, preview: signer, log });
|
||||
}
|
||||
if (path === '/api/project-file' && req.method === 'POST') {
|
||||
if (!send || !projectSave || !named) return json(res, 403, { error: 'read-only', sent: false });
|
||||
return projectFileSend(req, res, { projectSave, preview: signer, log });
|
||||
@@ -867,10 +816,6 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' });
|
||||
return consoleRead(res, () => fieldTable(fieldsFile));
|
||||
}
|
||||
if (path === '/api/library/status') {
|
||||
if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' });
|
||||
return libraryStatusRead(res, libraryStatus, search);
|
||||
}
|
||||
if (path === '/api/project-files' || path === '/api/project-file') {
|
||||
if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' });
|
||||
return projectFileRead(res, path === '/api/project-file' ? projectFile : projectFiles, path, search);
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
// Row 225 (C2-10b, design 7.4): replies, cites, write notices and the library status line in the
|
||||
// Console chat. Fixture only: the chat and library answers are stubbed; /render.mjs is the real one.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { startServer } from '../src/serve.mjs';
|
||||
import { fixture, close } from './fixture.mjs';
|
||||
import { browser } from './browser.mjs';
|
||||
const P = '10000000-0000-4000-8000-000000000001';
|
||||
const A = '20000000-0000-4000-8000-000000000002';
|
||||
const C1 = 'a'.repeat(40), C2 = 'b'.repeat(40), C3 = 'c'.repeat(40), C4 = 'd'.repeat(40);
|
||||
const HOSTILE = '<img src=x onerror="window.injected=1">';
|
||||
const cite = (path, lines, quote, commit = C1, extra = {}) => ({ path, commit, lines, quote, ...extra });
|
||||
const reply = (text, citations) => JSON.stringify({ text, citations });
|
||||
const row = (id, body, check, from = 'assistant') => ({ id: `5000000${id}-0000-4000-8000-000000000000`, from_role: from, at: '2026-10-23T18:00:00.000Z', body, ...(check === undefined ? {} : { check }) });
|
||||
const ROWS = [
|
||||
row(1, 'Plain text answer.', undefined, 'jason'),
|
||||
row(2, reply("Mara's eyes are grey.", [
|
||||
cite('Book One/Chapters/Chapter 03.md', [41, 41], 'her grey eyes', C1, { as: 'canon' }),
|
||||
cite('Book One/Playground/What If.md', [5, 6], 'a storm'),
|
||||
cite('Notes/Eyes.md', [9, 10], 'green eyes'),
|
||||
{ url: 'https://example.invalid/tides', quote: 'spring tides' },
|
||||
{ url: 'http://example.invalid/plain', quote: 'plain http' },
|
||||
{ url: 'javascript:window.injected=2', quote: 'script' },
|
||||
]), { citations: [
|
||||
{ scope: 'canon', agent: null, file: C1, last: C1 },
|
||||
{ scope: 'playground', agent: null, file: C2, last: C3 },
|
||||
{ scope: 'supporting', agent: 'acme/scribe', file: C2 },
|
||||
{ scope: 'web', agent: null }, { scope: 'web', agent: null }, { scope: 'web', agent: null },
|
||||
] }),
|
||||
row(3, reply('Unchecked.', [cite('Notes/Eyes.md', [1, 1], HOSTILE)])),
|
||||
row(4, JSON.stringify({ notice: 'library-write', op: 'write', path: 'Notes/Eyes.md', commit: C4 }), undefined, 'scribe'),
|
||||
];
|
||||
const STATUS = { project: 'books', version: 1, branch: 'main', served: C3, lastFetch: 'FIVE-MINUTES-AGO',
|
||||
lastError: { code: 'library-fetch-failed', at: '2026-10-23T18:00:00.000Z' }, diverged: { commit: C2, at: '2026-10-23T18:00:00.000Z' },
|
||||
problems: [{ code: 'R1', path: 'Notes/Eyes.md', message: 'no heading', line: 3 }, { code: 'R2', path: HOSTILE, message: 'bad name' }] };
|
||||
const stub = `(() => {
|
||||
const real = fetch.bind(window);
|
||||
window.__reverts=[];window.__revertAnswers=[];window.__statusReads=[];window.__confirms=[];
|
||||
window.__status={status:200,body:${JSON.stringify(STATUS)}};
|
||||
window.confirm=text=>{window.__confirms.push(text);return window.__confirm!==false;};
|
||||
window.fetch=async (url,init={})=>{
|
||||
const path=String(url), method=init.method||'GET';
|
||||
const answer=(value,status=200)=>new Response(JSON.stringify(value),{status,headers:{'content-type':'application/json'}});
|
||||
if(path==='/api/config')return answer({send:!location.search.includes('readonly')});
|
||||
if(path.startsWith('/api/bus/'))return answer({rows:[],unread:0,at:new Date().toISOString()});
|
||||
if(path==='/api/business')return answer({projects:['books']});
|
||||
if(path.startsWith('/api/library/status')){
|
||||
window.__statusReads.push(path);
|
||||
const body={...window.__status.body};
|
||||
if(body.lastFetch==='FIVE-MINUTES-AGO')body.lastFetch=new Date(Date.now()-330000).toISOString();
|
||||
return answer(body,window.__status.status);
|
||||
}
|
||||
if(path==='/api/library/revert'){
|
||||
window.__reverts.push({method,body:JSON.parse(init.body),headers:init.headers,redirect:init.redirect});
|
||||
const a=window.__revertAnswers.shift();if(!a)throw new Error('No revert answer');
|
||||
if(a.reject)throw new TypeError('network');
|
||||
return answer(a.body,a.status||200);
|
||||
}
|
||||
if(!path.startsWith('/api/chat'))return real(url,init);
|
||||
const data=value=>answer({...value,at:new Date().toISOString()});
|
||||
if(path==='/api/chat/instances')return data({rows:[{instance:'assistant',name:'assistant'}]});
|
||||
if(path==='/api/chat/projects')return data({rows:[{id:'${P}',name:'Books',repo:window.__repo===undefined?'books':window.__repo}]});
|
||||
if(path.endsWith('/sessions'))return data({rows:[{id:'${A}',name:'default',default:true}]});
|
||||
if(path.startsWith('/api/chat/threads')&&!path.includes('/messages'))return data({rows:[{id:'${A}',project:'${P}',name:'alpha',instance:'assistant'}]});
|
||||
return data({rows:${JSON.stringify(ROWS)},after:null,hasMore:false});
|
||||
};
|
||||
})();`;
|
||||
async function setup(t, { suffix = '', before = '' } = {}) {
|
||||
const f = await fixture(); t.after(() => f.close());
|
||||
const server = await startServer({ port: 0, board: f.boardURL }); t.after(() => close(server));
|
||||
const b = await browser(); t.after(() => b.close());
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: stub + before });
|
||||
await b.navigate(`http://127.0.0.1:${server.address().port}/${suffix}#/chat/threads/${A}`);
|
||||
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30));}throw new Error(${JSON.stringify(expression)});})()`);
|
||||
const text = selector => b.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`);
|
||||
const message = n => `.chat-log > li:nth-child(${n})`;
|
||||
await wait('document.querySelector(".chat-cites")');
|
||||
return { b, wait, text, message };
|
||||
}
|
||||
|
||||
test('chat replies: free text, cites with badges, the agent label, changed since and safe links', async t => {
|
||||
const { b, text, message } = await setup(t);
|
||||
assert.equal(await text(`${message(1)} .chat-body`), 'Plain text answer.');
|
||||
assert.equal(await text(`${message(2)} .chat-body`), "Mara's eyes are grey.");
|
||||
const cites = await b.evaluate(`[...document.querySelectorAll(${JSON.stringify(message(2) + ' .chat-cites > li')})].map(li => ({
|
||||
badge: li.querySelector('.chat-scope')?.textContent ?? null, ref: (li.querySelector('a') ?? li.querySelector('code'))?.textContent,
|
||||
href: li.querySelector('a')?.getAttribute('href') ?? null, rel: li.querySelector('a')?.getAttribute('rel') ?? null,
|
||||
quote: li.querySelector('q').textContent, agent: li.querySelector('.chat-agent-written')?.textContent ?? null,
|
||||
changed: !!li.querySelector('.chat-changed') }))`);
|
||||
assert.deepEqual(cites, [
|
||||
{ badge: 'canon', ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa', href: null, rel: null, quote: 'her grey eyes', agent: null, changed: false },
|
||||
{ badge: 'playground', ref: 'Book One/Playground/What If.md:5-6 @aaaaaaa', href: null, rel: null, quote: 'a storm', agent: null, changed: true },
|
||||
// `last` absent: nothing is marked changed (sage-3596).
|
||||
{ badge: 'supporting', ref: 'Notes/Eyes.md:9-10 @aaaaaaa', href: null, rel: null, quote: 'green eyes', agent: 'agent-written: acme/scribe', changed: false },
|
||||
{ badge: 'web', ref: 'https://example.invalid/tides', href: 'https://example.invalid/tides', rel: 'noopener noreferrer', quote: 'spring tides', agent: null, changed: false },
|
||||
{ badge: 'web', ref: 'http://example.invalid/plain', href: null, rel: null, quote: 'plain http', agent: null, changed: false },
|
||||
{ badge: 'web', ref: 'javascript:window.injected=2', href: null, rel: null, quote: 'script', agent: null, changed: false },
|
||||
]);
|
||||
// A reply with no check result: no badge, no label, never changed; its quote is inert text.
|
||||
assert.equal(await b.evaluate(`document.querySelectorAll(${JSON.stringify(message(3) + ' .chat-scope, ' + message(3) + ' .chat-label')}).length`), 0);
|
||||
assert.equal(await text(`${message(3)} q`), HOSTILE);
|
||||
assert.equal(await b.evaluate('!!(document.querySelector(".chat-log img") || window.injected)'), false);
|
||||
});
|
||||
|
||||
test('chat replies: a write notice reads as its line with a Revert; a refused revert shows its code', async t => {
|
||||
const { b, wait, text, message } = await setup(t);
|
||||
assert.equal(await text(`${message(4)} .chat-notice > span`), 'scribe wrote Notes/Eyes.md @ddddddd');
|
||||
assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(message(4) + ' [data-revert]')}).dataset.revert`), C4);
|
||||
assert.equal(await b.evaluate('document.querySelectorAll("[data-revert]").length'), 1, 'only the notice has a Revert');
|
||||
// Cancelled at the confirm: nothing is sent.
|
||||
await b.evaluate('window.__confirm=false;document.querySelector("[data-revert]").click();true');
|
||||
assert.equal(await b.evaluate('window.__reverts.length'), 0);
|
||||
assert.match(await b.evaluate('window.__confirms[0]'), /^Revert ddddddd\?/);
|
||||
// Before C2-02 the bus refuses the verb; the page shows that code and doesn't retry.
|
||||
await b.evaluate(`window.__confirm=true;window.__revertAnswers.push({status:502,body:{error:'unknown-verb',message:'not reverted: the bus refused it (unknown-verb).',sent:false}});document.querySelector("[data-revert]").click();true`);
|
||||
await wait('document.querySelector(".chat-revert-receipt").textContent.startsWith("Not")');
|
||||
assert.equal(await text('.chat-revert-receipt'), 'Not reverted (unknown-verb).');
|
||||
assert.deepEqual(await b.evaluate('window.__reverts'), [{ method: 'POST', body: { project: 'books', commit: C4 }, headers: { 'content-type': 'application/json' }, redirect: 'manual' }]);
|
||||
for (const [answer, receipt] of [
|
||||
[{ status: 409, body: { error: 'library-revert-not-agent', sent: false } }, 'Not reverted (library-revert-not-agent).'],
|
||||
[{ status: 409, body: { error: 'stale-base', sent: false } }, 'Not reverted (stale-base).'],
|
||||
[{ status: 503, body: { error: 'outcome-unknown', sent: 'unknown' } }, 'Outcome unknown. Read the library status before trying again; nothing was retried.'],
|
||||
[{ reject: true }, 'Outcome unknown. Read the library status before trying again; nothing was retried.'],
|
||||
[{ body: { project: 'books', path: 'Notes/Eyes.md', reverted: C4, commit: C1 } }, 'Reverted in aaaaaaa.'],
|
||||
]) {
|
||||
const n = await b.evaluate('window.__reverts.length');
|
||||
await b.evaluate(`window.__revertAnswers.push(${JSON.stringify(answer)});document.querySelector("[data-revert]").click();true`);
|
||||
await wait(`window.__reverts.length===${n + 1} && !document.querySelector("[data-revert]").disabled`);
|
||||
assert.equal(await text('.chat-revert-receipt'), receipt);
|
||||
}
|
||||
assert.ok(await b.evaluate('window.__statusReads.length') >= 2, 'the status line is read again after a revert');
|
||||
});
|
||||
|
||||
test('chat replies: a read-only Console shows the notice with Revert disabled', async t => {
|
||||
const { b, message } = await setup(t, { suffix: '?readonly' });
|
||||
assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(message(4) + ' [data-revert]')}).disabled`), true);
|
||||
await b.evaluate('document.querySelector("[data-revert]").click();true');
|
||||
assert.equal(await b.evaluate('window.__reverts.length + window.__confirms.length'), 0);
|
||||
});
|
||||
|
||||
test('chat status line: served commit and age, divergence, fetch error and the problems one click away', async t => {
|
||||
const { b, wait, text } = await setup(t);
|
||||
await wait('document.querySelector(".chat-library")');
|
||||
assert.equal(await b.evaluate('window.__statusReads[0]'), '/api/library/status?project=books');
|
||||
assert.equal(await text('.chat-library [role=status]'), 'Library at ccccccc · fetched 5 min ago · library-diverged at bbbbbbb · fetch error library-fetch-failed');
|
||||
assert.equal(await text('.chat-library-problems summary'), '2 problems');
|
||||
assert.deepEqual(await b.evaluate('[...document.querySelectorAll(".chat-library-problems li")].map(li => li.textContent)'),
|
||||
['R1 Notes/Eyes.md:3 no heading', `R2 ${HOSTILE} bad name`]);
|
||||
assert.equal(await b.evaluate('!!document.querySelector(".chat-library img")'), false);
|
||||
});
|
||||
|
||||
test('chat status line: limits when the status carries them, no problems, and refusals', async t => {
|
||||
const limits = { ...STATUS, lastError: null, diverged: null, problems: [], replyLimits: { requireCitation: true, citationsPerReply: 4, fallback: ['citationsPerReply'], problem: null } };
|
||||
const { b, wait, text } = await setup(t, { before: `window.__status={status:200,body:${JSON.stringify(limits)}};` });
|
||||
await wait('document.querySelector(".chat-library")');
|
||||
assert.equal(await text('.chat-library [role=status]'), 'Library at ccccccc · fetched 5 min ago · citations required, up to 4 per reply · limits fell back: citationsPerReply · no problems');
|
||||
assert.equal(await b.evaluate('!!document.querySelector(".chat-library-problems")'), false);
|
||||
for (const [status, error, shown] of [[502, 'library-state-invalid', 'Library status unavailable (library-state-invalid).'], [502, 'Not A Code', 'Library status unavailable (invalid-response).'],
|
||||
[403, 'owner-only', null], [502, 'project-not-declared', null], [502, 'library-mapping-invalid', 'Library status unavailable (library-mapping-invalid).'], [503, 'project-not-configured', null], [503, 'not-configured', null]]) {
|
||||
await wait('!document.querySelector("#refresh").disabled');
|
||||
await b.evaluate(`window.__status={status:${status},body:{error:${JSON.stringify(error)}}};document.querySelector("#refresh").click();true`);
|
||||
await wait(`window.__statusReads.length && document.querySelector(".chat-conversation") && ${shown === null ? '!document.querySelector(".chat-library")' : `document.querySelector(".chat-library")?.textContent===${JSON.stringify(shown)}`}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('chat status line: a project with no repository link reads no library status', async t => {
|
||||
const { b } = await setup(t, { before: 'window.__repo=null;' });
|
||||
assert.equal(await b.evaluate('window.__statusReads.length'), 0);
|
||||
assert.equal(await b.evaluate('!!document.querySelector(".chat-library")'), false);
|
||||
});
|
||||
|
||||
// Removing the nested problem display would hide the reason default limits are in force.
|
||||
test('chat status line: unreadable reply limits show their problem and both fallback keys', async t => {
|
||||
const limits = { ...STATUS, lastError: null, diverged: null, problems: [],
|
||||
replyLimits: { requireCitation: false, citationsPerReply: 8,
|
||||
fallback: ['requireCitation', 'citationsPerReply'], problem: 'reply-limits-unreadable' } };
|
||||
const { wait, text } = await setup(t, { before: `window.__status={status:200,body:${JSON.stringify(limits)}};` });
|
||||
await wait('document.querySelector(".chat-library")');
|
||||
assert.equal(await text('.chat-library [role=status]'), 'Library at ccccccc · fetched 5 min ago · citations optional, up to 8 per reply · limits fell back: requireCitation, citationsPerReply · reply-limits-unreadable · no problems');
|
||||
});
|
||||
|
||||
// Comparing commit to last would incorrectly mark the first cite; trusting missing file would
|
||||
// incorrectly mark the second. The third ensures malformed last never becomes a changed marker.
|
||||
test('chat replies: unchanged file at a different library commit and incomplete checks stay unmarked', async t => {
|
||||
const { b, wait } = await setup(t);
|
||||
await b.evaluate(`window.__checkOverride={citations:[{scope:'canon',agent:null,file:'${C2}',last:'${C2}'},
|
||||
{scope:'playground',agent:null,last:'${C3}'}, {scope:'supporting',agent:null,file:'${C2}',last:'bad'}]};true`);
|
||||
await b.evaluate(`const old=window.fetch;window.fetch=async(url,init)=>{const res=await old(url,init);
|
||||
if(!String(url).includes('/messages'))return res;
|
||||
const data=await res.json();data.rows[1].check=window.__checkOverride;
|
||||
return new Response(JSON.stringify(data),{headers:{'content-type':'application/json'}});
|
||||
};true`);
|
||||
await wait('!document.querySelector("#refresh").disabled');
|
||||
await b.evaluate('document.querySelector(".chat-area").dataset.beforeRefresh="yes";document.querySelector("#refresh").click();true');
|
||||
await wait('!document.querySelector(".chat-area")?.dataset.beforeRefresh && !document.querySelector("#refresh").disabled');
|
||||
assert.equal(await b.evaluate('document.querySelectorAll(".chat-changed").length'), 0);
|
||||
});
|
||||
|
||||
// Invalid configuration is a diagnostic, unlike an ordinary project with no library mapping.
|
||||
test('chat status line: an invalid library mapping remains visible for diagnosis', async t => {
|
||||
const { b, wait, text } = await setup(t, { before: "window.__status={status:502,body:{error:'library-mapping-invalid'}};" });
|
||||
await wait('window.__statusReads.length===1');
|
||||
assert.equal(await text('.chat-library [role=status]'), 'Library status unavailable (library-mapping-invalid).');
|
||||
assert.equal(await b.evaluate('window.__reverts.length'), 0);
|
||||
});
|
||||
@@ -1,185 +0,0 @@
|
||||
// Row 225 (C2-10b, design 7.4): the chat's library status read, a write notice's Revert and the
|
||||
// shared reply renderer served to the page. The revert is C2-02's `library.revert`; until that verb
|
||||
// exists the bus refuses it and the route reports the code.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { request } from 'node:http';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { startServer, userRoute } from '../src/serve.mjs';
|
||||
import { BusReadError } from '../src/bus.mjs';
|
||||
const preview = { host: 'console.example', header: 'x-user', user: 'jason' };
|
||||
const headers = { host: 'console.example', 'x-user': 'jason', origin: 'https://console.example', 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' };
|
||||
const C1 = 'a'.repeat(40), C2 = 'b'.repeat(40);
|
||||
const payload = { project: 'books', commit: C1 };
|
||||
const STATUS = { project: 'books', version: 1, branch: 'main', served: C2, lastFetch: '2026-10-23T18:00:00.000Z', lastError: null, diverged: null, problems: [] };
|
||||
async function setup(t, { enabled = true, revert, status, lines = [] } = {}) {
|
||||
const calls = { revert: [], status: [] };
|
||||
const server = await startServer({
|
||||
port: 0, preview, log: l => lines.push(l),
|
||||
send: enabled ? async () => ({}) : null,
|
||||
libraryStatus: status === null ? null : async args => { calls.status.push(args); return status ? status(args) : STATUS; },
|
||||
libraryRevert: enabled ? async args => { calls.revert.push(args); return revert ? revert(args) : { project: args.project, path: 'Notes/Eyes.md', reverted: args.commit, commit: C2 }; } : null,
|
||||
});
|
||||
t.after(() => new Promise(r => server.close(r)));
|
||||
return { calls, lines, port: server.address().port };
|
||||
}
|
||||
function call(s, { method = 'POST', path = '/api/library/revert', hs = headers, data = payload } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = request({ hostname: '127.0.0.1', port: s.port, path, method, headers: hs }, r => {
|
||||
let text = '';
|
||||
r.on('data', c => text += c);
|
||||
r.on('end', () => resolve({ status: r.statusCode, type: r.headers['content-type'], text, body: text.startsWith('{') ? JSON.parse(text) : null }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end(data === null ? undefined : typeof data === 'string' ? data : JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
const get = (s, path, hs = headers) => call(s, { method: 'GET', path, hs, data: null });
|
||||
const refusal = code => { throw new BusReadError(code); };
|
||||
|
||||
test('a signed-in same-origin revert reaches exactly one library.revert and returns the new commit', async t => {
|
||||
const s = await setup(t), r = await call(s);
|
||||
assert.equal(r.status, 200, r.text);
|
||||
assert.deepEqual(s.calls.revert, [payload]);
|
||||
assert.deepEqual({ ...r.body, at: undefined }, { project: 'books', path: 'Notes/Eyes.md', reverted: C1, commit: C2, at: undefined });
|
||||
assert.ok(s.lines.some(l => l.includes('library-revert') && l.includes(`commit=${C2}`) && l.includes('project="books"')));
|
||||
});
|
||||
|
||||
test('a revert needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata', async t => {
|
||||
const off = await setup(t, { enabled: false });
|
||||
assert.deepEqual((await call(off)).body, { error: 'read-only', sent: false });
|
||||
const s = await setup(t);
|
||||
for (const [key, value, status] of [['host', 'wrong.example', 403], ['x-user', 'other', 401], ['origin', 'https://else.example', 403], ['sec-fetch-site', 'cross-site', 403],
|
||||
['origin', undefined, 403], ['sec-fetch-site', undefined, 403], ['x-user', ['jason', 'jason'], 401]]) {
|
||||
const hs = { ...headers };
|
||||
if (value === undefined) delete hs[key]; else hs[key] = value;
|
||||
const r = await call(s, { hs });
|
||||
assert.equal(r.status, status, key);
|
||||
assert.notEqual(r.body?.sent, true);
|
||||
}
|
||||
assert.equal((await call(s, { method: 'GET', data: null })).status, 405);
|
||||
assert.deepEqual(s.calls.revert, []);
|
||||
});
|
||||
|
||||
test('a revert takes a project and a 40-hex commit, nothing else, before the bus is asked', async t => {
|
||||
const s = await setup(t);
|
||||
for (const data of ['not json', 'null', '[]', '5', { project: 'books' }, { commit: C1 }, { ...payload, path: 'a.md' },
|
||||
{ project: 'books', commit: 'abc' }, { project: 'books', commit: C1.toUpperCase() }, { project: '../x', commit: C1 }, { project: 7, commit: C1 }]) {
|
||||
const r = await call(s, { data });
|
||||
assert.equal(r.status, 400, JSON.stringify(data));
|
||||
assert.equal(r.body.error, 'invalid-request');
|
||||
assert.equal(r.body.sent, false);
|
||||
}
|
||||
assert.deepEqual(s.calls.revert, []);
|
||||
});
|
||||
|
||||
test('a bus refusal is reported as not reverted with its code; before C2-02 that is the verb refusal', async t => {
|
||||
const codes = { 'unknown-verb': 502, 'library-revert-not-agent': 409, 'stale-base': 409, 'human-required': 403, 'project-not-configured': 503, 'invalid-request': 400 };
|
||||
for (const [code, status] of Object.entries(codes)) {
|
||||
const s = await setup(t, { revert: () => refusal(code) }), r = await call(s);
|
||||
assert.equal(r.status, status, code);
|
||||
assert.equal(r.body.error, code);
|
||||
assert.equal(r.body.sent, false);
|
||||
assert.match(r.body.message, new RegExp(`not reverted: the bus refused it \\(${code}\\)`));
|
||||
assert.equal(s.calls.revert.length, 1, 'never retried');
|
||||
}
|
||||
});
|
||||
|
||||
test('a revert whose answer is lost or unusable is reported unknown and is never retried', async t => {
|
||||
const answers = [() => refusal('outcome-unknown'), () => { throw new Error('socket closed'); }, () => ({ project: 'books', path: 'a.md', reverted: C1 }),
|
||||
() => ({ project: 'books', path: 'a.md', reverted: C2, commit: C2 }), () => ({ project: 'books', reverted: C1, commit: C2 }), () => null];
|
||||
for (const revert of answers) {
|
||||
const s = await setup(t, { revert }), r = await call(s);
|
||||
assert.equal(r.status, 503, revert.toString());
|
||||
assert.equal(r.body.error, 'outcome-unknown');
|
||||
assert.equal(r.body.sent, 'unknown');
|
||||
assert.equal(s.calls.revert.length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('the status read takes one project query and answers the bus result for that project', async t => {
|
||||
const s = await setup(t), r = await get(s, '/api/library/status?project=books');
|
||||
assert.equal(r.status, 200, r.text);
|
||||
assert.deepEqual(s.calls.status, [{ project: 'books' }]);
|
||||
assert.deepEqual({ ...r.body, at: undefined }, { ...STATUS, at: undefined });
|
||||
for (const path of ['/api/library/status', '/api/library/status?project=books&x=1', '/api/library/status?project=books&project=other', '/api/library/status?project=../x'])
|
||||
assert.equal((await get(s, path)).status, 400, path);
|
||||
assert.equal((await call(s, { path: '/api/library/status?project=books' })).status, 403, 'the preview write fence precedes GET-only routes');
|
||||
assert.equal(s.calls.status.length, 1);
|
||||
});
|
||||
|
||||
test('a status answer for another project or of the wrong shape is refused; a refusal keeps its status', async t => {
|
||||
for (const answer of [{ ...STATUS, project: 'other' }, null, [], 'books']) {
|
||||
const s = await setup(t, { status: () => answer }), r = await get(s, '/api/library/status?project=books');
|
||||
assert.equal(r.status, 502, JSON.stringify(answer));
|
||||
assert.equal(r.body.error, 'invalid-response');
|
||||
}
|
||||
for (const [code, status] of Object.entries({ 'human-required': 403, 'project-not-configured': 503, 'project-not-declared': 400, 'library-mapping-invalid': 502 })) {
|
||||
const s = await setup(t, { status: () => refusal(code) }), r = await get(s, '/api/library/status?project=books');
|
||||
assert.equal(r.status, status, code);
|
||||
assert.equal(r.body.error, code);
|
||||
}
|
||||
const none = await setup(t, { status: null });
|
||||
assert.equal((await get(none, '/api/library/status?project=books')).body.error, 'not-configured');
|
||||
});
|
||||
|
||||
test('the status read needs no send; a read-only Console still has no revert', async t => {
|
||||
const s = await setup(t, { enabled: false });
|
||||
assert.equal((await get(s, '/api/library/status?project=books')).status, 200);
|
||||
assert.equal((await call(s)).status, 403);
|
||||
});
|
||||
|
||||
test('library routes are the owner\'s: a granted user is refused them, and the tester never reverts', async t => {
|
||||
for (const [path, method] of [['/api/library/status', 'GET'], ['/api/library/revert', 'POST']]) assert.equal(userRoute(path, method), false, path);
|
||||
await assert.rejects(startServer({ port: 0, tester: true, testerSocket: '/nonexistent', libraryRevert: async () => ({}) }), /tester listener permits only/);
|
||||
});
|
||||
|
||||
test('the page gets the one library renderer, byte for byte, as a JavaScript module', async t => {
|
||||
const s = await setup(t), r = await get(s, '/render.mjs');
|
||||
assert.equal(r.status, 200);
|
||||
assert.match(r.type, /^text\/javascript/);
|
||||
assert.equal(r.text, readFileSync(new URL('../../library/src/render.mjs', import.meta.url), 'utf8'));
|
||||
});
|
||||
|
||||
// A valid commit from an unrelated project is an unusable receipt, never evidence of this revert.
|
||||
test('a revert receipt for another project is unknown and is never retried', async t => {
|
||||
const s = await setup(t, { revert: args => ({ project: 'other', path: 'Notes/Eyes.md', reverted: args.commit, commit: C2 }) });
|
||||
const r = await call(s);
|
||||
assert.equal(r.status, 503);
|
||||
assert.equal(r.body.error, 'outcome-unknown');
|
||||
assert.equal(r.body.sent, 'unknown');
|
||||
assert.deepEqual(s.calls.revert, [payload]);
|
||||
});
|
||||
|
||||
// Catch a route being added ahead of the granted-user owner fence: neither transport may be used.
|
||||
test('a granted-user HTTP session cannot reach owner library callbacks', async t => {
|
||||
let ownerCalls = 0, userCalls = 0;
|
||||
const sso = { publicHost: 'console.example', publicOrigin: 'https://console.example',
|
||||
formTargets: () => [], session: () => ({ name: 'Alice', user: 'alice' }) };
|
||||
const server = await startServer({ port: 0, sso, send: async () => ({}),
|
||||
libraryStatus: async () => { ownerCalls++; return STATUS; },
|
||||
libraryRevert: async () => { ownerCalls++; return { project: 'books', path: 'a.md', reverted: C1, commit: C2 }; },
|
||||
userOptions: () => { userCalls++; return {}; }, log: () => {} });
|
||||
t.after(() => new Promise(r => server.close(r)));
|
||||
const s = { port: server.address().port };
|
||||
for (const options of [{ method: 'GET', path: '/api/library/status?project=books', data: null }, {}]) {
|
||||
const r = await call(s, options);
|
||||
assert.equal(r.status, 403);
|
||||
assert.equal(r.body.error, 'owner-only');
|
||||
}
|
||||
assert.equal(ownerCalls, 0);
|
||||
assert.equal(userCalls, 0);
|
||||
});
|
||||
|
||||
// A supplied callback is insufficient authority: --send must independently be enabled.
|
||||
test('a configured revert callback remains unreachable when send is off', async t => {
|
||||
let calls = 0;
|
||||
const server = await startServer({ port: 0, preview,
|
||||
libraryRevert: async () => { calls++; return { project: 'books', path: 'a.md', reverted: C1, commit: C2 }; } });
|
||||
t.after(() => new Promise(r => server.close(r)));
|
||||
const r = await call({ port: server.address().port });
|
||||
assert.equal(r.status, 403);
|
||||
assert.equal(r.body.error, 'read-only');
|
||||
assert.equal(r.body.sent, false);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
Reference in new issue
Block a user