chadtmiller.com/knotless

Git hosted in your atproto account, on any stock PDS, fast enough for agents.

git clone knotless://chadtmiller.com/knotless

needs the knotless git helper. set it up

cov: both runs side by side, in about two minutes; getFeed pages

harness/cov.mjs builds the tests once, instrumented, and runs every test
binary against a stock PDS and a spaces one together, several at a time,
each harness shared as `just pds` is, clearing only the last run's
counts: from about six minutes to under two and a half.

Sharing a PDS showed what the tests leaned on running alone, and each
is fixed: dev.knotless.getFeed pages with a cursor, by time and URI as
listRepos does, so the feed test reads back to where it began; the
proxy's answer to too big a body can close the connection under the
rest of it; the forge test's repository has a name no other test uses;
and a pooled connection is let go before a Node server's five seconds
of keep-alive end, rather than at ninety, as a PDS's are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chad Miller committed on Oct 21, 2026 parent 5026daf commit a4ed85c
Showing 11 changed files with 366 additions and 95 deletions
CLAUDE.md+3−1
@@ -26,7 +26,9 @@ work is done.
a block or component beside it splits the text into nodes, which shapes
differently from the same text whole and moves pixels.
- Done: `just lint` (it runs svelte-check too), `just cov` (100% lines and
functions over the stock and spaces-alpha runs; slow, so once) and, if
functions over the stock and spaces-alpha runs, side by side, in under
two minutes; `COV_CLEAN=1` rebuilds everything first, for a miss that
makes no sense) and, if
pages changed, `just shots-check` and `just shots-live`.
`cargo fmt` can put a `?` on a line of its own, which coverage then counts
Justfile+1−4
@@ -42,10 +42,7 @@ spaces := env_var_or_default("KNOTLESS_DEV_ENV", "../atproto-space-ref/packages/
# Fails under 100% of lines or functions, over runs against the stock
# reference PDS and the spaces alpha together.
cov:
{{cargo}} llvm-cov clean --workspace
KNOTLESS_DEV_ENV= {{cargo}} llvm-cov --workspace --no-report
KNOTLESS_DEV_ENV={{spaces}} {{cargo}} llvm-cov --workspace --no-report
{{cargo}} llvm-cov report --ignore-filename-regex '{{ignore}}' --fail-under-lines 100 --fail-under-functions 100 --show-missing-lines
node harness/cov.mjs '{{spaces}}' '{{ignore}}'
# Coverage over the stock run alone, failing on nothing: quicker than
# `cov`, but what only the spaces alpha reaches shows as missing.
README.md+1−1
@@ -346,7 +346,7 @@ covered across the two runs, and CI fails below that.
just setup # npm install in harness/ and web/, llvm-tools for coverage
just test # every test
just test-spaces # against the alpha: KNOTLESS_DEV_ENV names its dev-env
just cov # both runs, failing under 100% lines or functions
just cov # both runs side by side, failing under 100% lines or functions
just lint # rustfmt, clippy and svelte-check
just bench 50 # the table above, at 50 ms RTT
just bench-cards # what a social card costs to draw, with the PNGs
crates/knotless-serve/src/index.rs+66−34
@@ -31,11 +31,16 @@ pub struct Listed {
#[serde(skip_serializing_if = "Option::is_none")]
pub fork_of: Option<String>,
pub created_at: String,
/// Its record, which pages are cut after.
#[serde(skip)]
pub uri: String,
}
/// A ref moving, as a claim recorded it.
#[derive(Debug, Clone, PartialEq)]
pub struct Push {
/// The claim, which pages are cut after.
pub uri: String,
/// The repository's record.
pub repo: String,
pub did: String,
@@ -424,11 +429,7 @@ impl Index {
) -> rusqlite::Result<(Vec<Listed>, Option<String>)> {
let db = self.db.lock().unwrap();
let pattern = format!("%{}%", query.replace(['%', '_'], ""));
// Newest first, ties broken by URI, so a page ends at a point the
// next can start strictly after.
let (at, uri) = cursor
.and_then(|c| c.split_once(' '))
.unwrap_or(("\u{10ffff}", ""));
let (at, uri) = after(cursor);
let mut stmt = db.prepare_cached(
"SELECT did, name, description, fork_of, created_at, uri FROM repo
WHERE (name LIKE ?1 OR description LIKE ?1)
@@ -436,27 +437,32 @@ impl Index {
AND did NOT IN down AND uri NOT IN down
ORDER BY created_at DESC, uri DESC LIMIT ?4",
)?;
let rows: Vec<(Listed, String)> = stmt
.query_map(params![pattern, at, uri, limit as i64], paged)?
let rows: Vec<Listed> = stmt
.query_map(params![pattern, at, uri, limit as i64], listed)?
.collect::<rusqlite::Result<_>>()?;
let next = (rows.len() == limit)
.then(|| {
rows.last()
.map(|(r, uri)| format!("{} {uri}", r.created_at))
})
.then(|| rows.last().map(|r| cursor_at(&r.created_at, &r.uri)))
.flatten();
Ok((rows.into_iter().map(|(r, _)| r).collect(), next))
Ok((rows, next))
}
/// Pulls, issues, comments and changes of state on any repository,
/// newest first, with the owner and key of the repository each is on:
/// without what its owner hides, with what was said about that, or
/// what those its owner blocks wrote, as `forge` leaves them out.
pub fn recent(&self, limit: usize) -> rusqlite::Result<Vec<(Entry, String, String)>> {
/// what those its owner blocks wrote, as `forge` leaves them out. After
/// `cursor`, as [`cursor_at`] makes one, if given.
pub fn recent(
&self,
limit: usize,
cursor: Option<&str>,
) -> rusqlite::Result<Vec<(Entry, String, String)>> {
let (at, uri) = after(cursor);
let db = self.db.lock().unwrap();
let mut stmt = db.prepare_cached(
"SELECT f.uri, f.author, f.value, f.repo_did, f.repo_key FROM forge f
WHERE f.uri NOT LIKE '%/dev.knotless.hide/%' AND f.uri NOT LIKE '%/dev.knotless.block/%'
AND (json_extract(f.value, '$.createdAt') < ?2
OR (json_extract(f.value, '$.createdAt') = ?2 AND f.uri < ?3))
AND f.uri NOT IN down AND f.author NOT IN down
AND coalesce(json_extract(f.value, '$.subject'), '') NOT IN down
AND f.author NOT IN (SELECT subject FROM block WHERE did = f.repo_did)
@@ -467,21 +473,26 @@ impl Index {
OR (o.uri LIKE '%/dev.knotless.hide/%'
AND json_extract(o.value, '$.subject')
IN (f.uri, coalesce(json_extract(f.value, '$.subject'), '')))))
ORDER BY json_extract(f.value, '$.createdAt') DESC LIMIT ?1",
ORDER BY json_extract(f.value, '$.createdAt') DESC, f.uri DESC LIMIT ?1",
)?;
stmt.query_map([limit as i64], recent)?.collect()
stmt.query_map(params![limit as i64, at, uri], recent)?
.collect()
}
/// Refs that moved on repositories that stand, newest first.
pub fn pushes(&self, limit: usize) -> rusqlite::Result<Vec<Push>> {
/// Refs that moved on repositories that stand, newest first; after
/// `cursor`, as [`cursor_at`] makes one, if given.
pub fn pushes(&self, limit: usize, cursor: Option<&str>) -> rusqlite::Result<Vec<Push>> {
let db = self.db.lock().unwrap();
let (at, uri) = after(cursor);
let mut stmt = db.prepare_cached(
"SELECT r.uri, p.did, r.name, r.fork_of, p.ref, p.before, p.after, p.created_at FROM push p
JOIN repo r ON r.did = p.did AND r.key = p.key
"SELECT r.uri, p.did, r.name, r.fork_of, p.ref, p.before, p.after, p.created_at, p.uri
FROM push p JOIN repo r ON r.did = p.did AND r.key = p.key
WHERE p.did NOT IN down AND r.uri NOT IN down
ORDER BY p.created_at DESC LIMIT ?1",
AND (p.created_at < ?2 OR (p.created_at = ?2 AND p.uri < ?3))
ORDER BY p.created_at DESC, p.uri DESC LIMIT ?1",
)?;
stmt.query_map([limit as i64], push)?.collect()
stmt.query_map(params![limit as i64, at, uri], push)?
.collect()
}
/// Where the repository at `repo` had `refname` just before `at`, as
@@ -508,7 +519,7 @@ impl Index {
pub fn forks(&self, did: &str, key: &str) -> rusqlite::Result<Vec<Listed>> {
let db = self.db.lock().unwrap();
let mut stmt = db.prepare_cached(
"SELECT did, name, description, fork_of, created_at FROM repo
"SELECT did, name, description, fork_of, created_at, uri FROM repo
WHERE fork_did = ?1 AND fork_key = ?2 AND did NOT IN down AND uri NOT IN down
ORDER BY created_at",
)?;
@@ -550,6 +561,7 @@ fn push(row: &rusqlite::Row) -> rusqlite::Result<Push> {
from: row.get(5)?,
to: row.get(6)?,
created_at: row.get(7)?,
uri: row.get(8)?,
})
}
@@ -557,9 +569,17 @@ fn recent(row: &rusqlite::Row) -> rusqlite::Result<(Entry, String, String)> {
Ok((entry(row)?, row.get(3)?, row.get(4)?))
}
/// A listed repository, and its URI to page from.
fn paged(row: &rusqlite::Row) -> rusqlite::Result<(Listed, String)> {
Ok((listed(row)?, row.get(5)?))
/// Where a page cut after the record `uri`, made at `at`, starts the next:
/// newest first, ties broken by URI, so the next starts strictly after it.
pub fn cursor_at(at: &str, uri: &str) -> String {
format!("{at} {uri}")
}
/// The time and URI a cursor is after, or past everything without one.
fn after(cursor: Option<&str>) -> (&str, &str) {
cursor
.and_then(|c| c.split_once(' '))
.unwrap_or(("\u{10ffff}", ""))
}
fn listed(row: &rusqlite::Row) -> rusqlite::Result<Listed> {
@@ -569,6 +589,7 @@ fn listed(row: &rusqlite::Row) -> rusqlite::Result<Listed> {
description: row.get(2)?,
fork_of: row.get(3)?,
created_at: row.get(4)?,
uri: row.get(5)?,
})
}
@@ -606,8 +627,8 @@ mod tests {
assert!(index.forks("did:a", "k").is_err());
assert!(index.forge("did:a", "k").is_err());
assert!(index.name("did:a", "k").is_err());
assert!(index.recent(1).is_err());
assert!(index.pushes(1).is_err());
assert!(index.recent(1, None).is_err());
assert!(index.pushes(1, None).is_err());
}
#[test]
@@ -783,11 +804,12 @@ mod tests {
&json!({"repo": "x", "name": "refs/heads/main", "createdAt": "2026-01-04T00:00:00Z"}),
)
.unwrap();
let pushes = index.pushes(10).unwrap();
let pushes = index.pushes(10, None).unwrap();
assert_eq!(
pushes,
[
Push {
uri: "at://did:a/dev.knotless.claim/k.m.1".into(),
repo: "at://did:a/dev.knotless.repo/r".into(),
did: "did:a".into(),
name: "r".into(),
@@ -798,6 +820,7 @@ mod tests {
created_at: "2026-01-03T00:00:00Z".into(),
},
Push {
uri: "at://did:a/dev.knotless.claim/k.m.0".into(),
repo: "at://did:a/dev.knotless.repo/r".into(),
did: "did:a".into(),
name: "r".into(),
@@ -819,8 +842,11 @@ mod tests {
index.delete("did:a", nsid::CLAIM, "k.m.2").unwrap();
// Compaction drops claims; the history stays.
index.delete("did:a", nsid::CLAIM, "k.m.0").unwrap();
assert_eq!(index.pushes(10).unwrap().len(), 3);
assert_eq!(index.pushes(1).unwrap().len(), 1);
assert_eq!(index.pushes(10, None).unwrap().len(), 3);
assert_eq!(index.pushes(1, None).unwrap().len(), 1);
let all = index.pushes(10, None).unwrap();
let at = cursor_at(&all[0].created_at, &all[0].uri);
assert_eq!(index.pushes(10, Some(&at)).unwrap(), all[1..]);
// Pulls, issues and what was said, newest first, without what the
// owner hides or blocks, or those records themselves.
@@ -875,18 +901,24 @@ mod tests {
index
.put("did:a", nsid::BLOCK, "12", &json!({"subject": "did:e"}))
.unwrap();
let recent = index.recent(10).unwrap();
let recent = index.recent(10, None).unwrap();
let kinds: Vec<&str> = recent
.iter()
.map(|(e, _, _)| e.uri.rsplit('/').nth(1).unwrap())
.collect();
assert_eq!(kinds, [nsid::COMMENT, nsid::PULL, nsid::ISSUE]);
assert_eq!((recent[0].1.as_str(), recent[0].2.as_str()), ("did:a", "k"));
assert_eq!(index.recent(1).unwrap().len(), 1);
assert_eq!(index.recent(1, None).unwrap().len(), 1);
// Paged by time and URI, the next page starting strictly after.
let first = &index.recent(1, None).unwrap()[0].0;
let at = cursor_at(first.value["createdAt"].as_str().unwrap(), &first.uri);
let rest = index.recent(10, Some(&at)).unwrap();
assert_eq!(rest.len(), 2);
assert_eq!(rest[0].0.uri, recent[1].0.uri);
// Gone with the repository.
index.delete("did:a", nsid::REPO, "r").unwrap();
assert!(index.pushes(10).unwrap().is_empty());
assert!(index.pushes(10, None).unwrap().is_empty());
}
#[test]
crates/knotless-serve/src/web/feed.rs+83−31
@@ -2,7 +2,7 @@
//! knows, newest first. The front page shows it.
use super::*;
use crate::index::Index;
use crate::index::{Index, cursor_at};
use knotless::records::nsid;
use std::collections::HashSet;
@@ -18,6 +18,10 @@ const LISTED: usize = 10;
#[ts(export, rename = "Feed")]
pub struct Output {
events: Vec<Event>,
/// Where the next page starts, when there may be one.
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
cursor: Option<String>,
}
/// Something an account did to a repository.
@@ -107,6 +111,7 @@ pub enum Action {
#[derive(Deserialize, Default)]
pub struct Params {
limit: Option<usize>,
cursor: Option<String>,
}
pub async fn get_feed(
@@ -118,8 +123,8 @@ pub async fn get_feed(
"this server keeps no index".into(),
))?;
let limit = p.limit.unwrap_or(LIMIT).clamp(1, 100);
let events = events(&app, index, limit).await?;
Ok(axum::Json(Output { events }))
let (events, cursor) = events(&app, index, limit, p.cursor.as_deref()).await?;
Ok(axum::Json(Output { events, cursor }))
}
/// The owner and name of the repository `uri` names.
@@ -128,21 +133,63 @@ fn owner_and_name(uri: &str) -> Option<(String, String)> {
Some((did.to_string(), name.to_string()))
}
/// What happened, newest first: repositories made, refs moved, and what
/// was opened, said and decided on them.
async fn events(app: &App, index: &Index, limit: usize) -> Result<Vec<Event>> {
let repos = index
.repos("", limit)
/// Where a page of `limit` ends, from the time and URI of each record it
/// could hold, newest `limit` of each kind: the last it holds, if it is
/// full.
fn cut<'a>(mut keys: Vec<(&'a str, &'a str)>, limit: usize) -> Option<(&'a str, &'a str)> {
keys.sort_by(|a, b| b.cmp(a));
keys.get(limit - 1).copied()
}
/// What happened, newest first, after `cursor`: repositories made, refs
/// moved, and what was opened, said and decided on them; and where the
/// next page starts. A page is the newest `limit` records of all three
/// kinds, by time and then URI, less those not shown.
async fn events(
app: &App,
index: &Index,
limit: usize,
cursor: Option<&str>,
) -> Result<(Vec<Event>, Option<String>)> {
let (mut repos, _) = index
.repos_page("", limit, cursor)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
let pushes = index
.pushes(limit)
let mut pushes = index
.pushes(limit, cursor)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
let said = index
.recent(limit)
let mut said = index
.recent(limit, cursor)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
let at = |e: &forge::Entry| {
e.value["createdAt"]
.as_str()
.unwrap_or_default()
.to_string()
};
let keys = repos
.iter()
.map(|r| (r.created_at.as_str(), r.uri.as_str()))
.chain(
pushes
.iter()
.map(|p| (p.created_at.as_str(), p.uri.as_str())),
)
.chain(said.iter().map(|(e, _, _)| {
let at = e.value["createdAt"].as_str().unwrap_or_default();
(at, e.uri.as_str())
}))
.collect();
let end = cut(keys, limit).map(|(at, uri)| (at.to_string(), uri.to_string()));
let next = end.as_ref().map(|(at, uri)| cursor_at(at, uri));
if let Some(end) = &end {
let held = |at: &str, uri: &str| (at, uri) >= (end.0.as_str(), end.1.as_str());
repos.retain(|r| held(&r.created_at, &r.uri));
pushes.retain(|p| held(&p.created_at, &p.uri));
said.retain(|(e, _, _)| held(&at(e), &e.uri));
}
// Each account named, resolved together.
let parents: Vec<(String, String)> = repos
@@ -174,20 +221,25 @@ async fn events(app: &App, index: &Index, limit: usize) -> Result<Vec<Event>> {
name: name.to_string(),
};
let mut events: Vec<Event> = repos
let mut events: Vec<(String, Event)> = repos
.iter()
.map(|r| Event {
created_at: r.created_at.clone(),
actor: actor(&r.did),
repo: repo(&r.did, &r.name),
action: Action::Created {
description: r.description.clone(),
fork_of: r
.fork_of
.as_deref()
.and_then(owner_and_name)
.map(|(did, name)| repo(&did, &name)),
},
.map(|r| {
(
r.uri.clone(),
Event {
created_at: r.created_at.clone(),
actor: actor(&r.did),
repo: repo(&r.did, &r.name),
action: Action::Created {
description: r.description.clone(),
fork_of: r
.fork_of
.as_deref()
.and_then(owner_and_name)
.map(|(did, name)| repo(&did, &name)),
},
},
)
})
.collect();
@@ -259,12 +311,13 @@ async fn events(app: &App, index: &Index, limit: usize) -> Result<Vec<Event>> {
}
}
};
events.push(Event {
let event = Event {
created_at: p.created_at.clone(),
actor: actor(&p.did),
repo: repo(&p.did, &p.name),
action,
});
};
events.push((p.uri.clone(), event));
}
// Pulls and issues, as their repositories' owners let them stand, by
@@ -326,11 +379,10 @@ async fn events(app: &App, index: &Index, limit: usize) -> Result<Vec<Event>> {
repo: named[&(did.as_str(), key.as_str())].clone(),
action,
};
events.extend(shown.then_some(event));
events.extend(shown.then(|| (e.uri.clone(), event)));
}
events.sort_by(|a, b| b.created_at.cmp(&a.created_at));
events.truncate(limit);
Ok(events)
events.sort_by(|(a, x), (b, y)| (&y.created_at, b).cmp(&(&x.created_at, a)));
Ok((events.into_iter().map(|(_, e)| e).collect(), next))
}
#[cfg(test)]
crates/knotless-serve/tests/forge.rs+11−9
@@ -92,13 +92,13 @@ fn forks_pulls_reviews_and_merges() {
let av = appview();
let owner = person(account(), &av);
let contrib = person(account(), &av);
let url = owner.url("project");
let url = owner.url("forkable");
let work = owner.work("w", 1);
owner.git(&work, &["push", "-q", &url, "main"]);
// A fork in the contributor's account, with a branch to offer.
let fork = ok(&contrib, &["fork", &url]);
assert_eq!(fork, format!("knotless://{}/project", contrib.account.did));
assert_eq!(fork, format!("knotless://{}/forkable", contrib.account.did));
let clone = contrib.path("clone");
contrib.git(contrib.dir.path(), &["clone", "-q", &fork, "clone"]);
contrib.git(&clone, &["checkout", "-q", "-b", "feature"]);
@@ -263,16 +263,18 @@ fn forks_pulls_reviews_and_merges() {
);
// The index knows the fork, and finds repositories by name.
let found: Value =
reqwest::blocking::get(format!("{}/xrpc/dev.knotless.listRepos?q=proj", av.base))
.unwrap()
.json()
.unwrap();
let found: Value = reqwest::blocking::get(format!(
"{}/xrpc/dev.knotless.listRepos?q=forkable",
av.base
))
.unwrap()
.json()
.unwrap();
assert_eq!(found["repos"].as_array().unwrap().len(), 2);
assert!(found.get("cursor").is_none());
// A page at a time, the same two.
let list = |cursor: Option<&str>| -> Value {
let mut query = vec![("q", "proj"), ("limit", "1")];
let mut query = vec![("q", "forkable"), ("limit", "1")];
query.extend(cursor.map(|c| ("cursor", c)));
reqwest::blocking::Client::new()
.get(format!("{}/xrpc/dev.knotless.listRepos", av.base))
@@ -294,7 +296,7 @@ fn forks_pulls_reviews_and_merges() {
.unwrap()
.is_empty()
);
let uri = format!("at://{}/dev.knotless.repo/project", owner.account.did);
let uri = format!("at://{}/dev.knotless.repo/forkable", owner.account.did);
let indexed = av.app.index.as_ref().unwrap();
let key = indexed.repo(&uri).unwrap().unwrap()["key"]
.as_str()
crates/knotless-serve/tests/web.rs+46−11
@@ -67,8 +67,22 @@ impl Site {
self.json(&format!("/_data{path}"))
}
fn feed(&self) -> Value {
self.json("/xrpc/dev.knotless.getFeed?limit=100")["events"].clone()
/// The feed back to `since`, page by page.
fn feed(&self, since: &str) -> Value {
let mut events = Vec::new();
let mut cursor = String::new();
loop {
let page = self.json(&format!(
"/xrpc/dev.knotless.getFeed?limit=100&cursor={}",
cursor.replace(' ', "%20")
));
events.extend(page["events"].as_array().unwrap().iter().cloned());
let last = events.last().map(|e| e["createdAt"].as_str().unwrap());
match page["cursor"].as_str() {
Some(next) if last.is_some_and(|at| at >= since) => cursor = next.into(),
_ => return Value::Array(events),
}
}
}
fn status(&self, path: &str) -> (u16, String) {
@@ -685,12 +699,20 @@ fn the_feed() {
let owner = person(account(), &site);
let other = person(account(), &site);
let (o, t) = (&owner.account.handle, &other.account.handle);
// Other tests' pushes and pulls share the feed, so each part is looked
// for while it is recent: once the feed shows `last`, it must show
// each of `wants`.
// Other tests' pushes and pulls share the feed, so it is read back to
// where it was as this began: once it shows `last`, it must show each
// of `wants`.
let newest = site.json("/xrpc/dev.knotless.getFeed?limit=1");
let since = newest["events"][0]["createdAt"].as_str().unwrap_or("");
let shows = |handle: &str, last: (&str, Value), wants: &[(&str, Value)]| {
wait_until(|| did(&about(&site.feed(), handle, "fed"), last.0, last.1.clone()));
let feed = site.feed();
wait_until(|| {
did(
&about(&site.feed(since), handle, "fed"),
last.0,
last.1.clone(),
)
});
let feed = site.feed(since);
let events = about(&feed, handle, "fed");
for (kind, fields) in wants {
assert!(
@@ -782,7 +804,7 @@ fn the_feed() {
let (pull, issue) = (pull.trim(), issue.trim());
wait_until(|| {
did(
&about(&site.feed(), o, "fed"),
&about(&site.feed(since), o, "fed"),
"changed",
json!({"subject": issue, "state": "open"}),
)
@@ -835,6 +857,15 @@ fn the_feed() {
};
assert!(events("") <= 40);
assert_eq!((events("?limit=1"), events("?limit=0")), (1, 1));
// Page by page, each starting strictly after the last.
let first = site.json("/xrpc/dev.knotless.getFeed?limit=2");
let cursor = first["cursor"].as_str().unwrap().replace(' ', "%20");
let next = site.json(&format!(
"/xrpc/dev.knotless.getFeed?limit=1&cursor={cursor}"
));
let (shown, after) = (first["events"].as_array().unwrap(), &next["events"][0]);
assert!(after["createdAt"].as_str() <= shown.last().unwrap()["createdAt"].as_str());
assert!(!shown.contains(after));
let found = site.json("/_data/search?q=FED");
assert_eq!(found["query"], "FED");
assert!(names(&found["repos"]).contains(&"fed"));
@@ -920,12 +951,16 @@ fn pages_drawn_elsewhere() {
.unwrap();
assert!(ours.text().unwrap().contains("keeps no index"));
// Too much to hand on, or no app to hand it to.
// The answer can come before all of it is sent, which closes the
// connection under the rest.
let big = client
.post(format!("{base}/x"))
.body(vec![b'x'; 2 << 20])
.send()
.unwrap();
assert_eq!(big.status().as_u16(), 502);
.send();
match big {
Ok(res) => assert_eq!(res.status().as_u16(), 502),
Err(e) => assert!(e.is_request(), "{e}"),
}
let (gone, nowhere) = listen();
drop(gone);
let res = client.get(front(nowhere)).send().unwrap();
crates/knotless/src/env.rs+5−1
@@ -142,7 +142,11 @@ impl Ctx {
.user_agent(concat!("knotless/", env!("CARGO_PKG_VERSION")))
.connect_timeout(Duration::from_secs(10))
.timeout(env.timeout)
.pool_idle_timeout(Duration::from_secs(90))
// Under the 5s Node servers keep an idle connection by default,
// as a PLC directory does: past it, one can close under the
// request reusing it. A PDS keeps one 90s, which ties with any
// longer wait here.
.pool_idle_timeout(Duration::from_secs(4))
.tcp_nodelay(true)
.build()
.expect("the HTTP client configuration is static");
harness/cov.mjsnew+141−0
@@ -0,0 +1,141 @@
// Coverage over the stock reference PDS and the spaces alpha together,
// failing under 100% of lines or functions: what `just cov` runs.
//
// node harness/cov.mjs <spaces dev-env> <ignore regex>
//
// The tests are built once, instrumented, and each test binary is run
// against both PDSes, several at a time, rather than one after another
// as `cargo test` would: one harness for each PDS, shared by every binary
// run against it, as `just pds` is. COV_JOBS sets how many run at once;
// COV_CLEAN=1 builds every workspace crate again first.
import { spawn, spawnSync } from 'node:child_process'
import { mkdirSync, openSync, writeFileSync } from 'node:fs'
import { availableParallelism } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const [spaces, ignore] = process.argv.slice(2)
const cargo = ['run', 'stable', 'cargo']
const jobs = Number(process.env.COV_JOBS) || Math.max(2, Math.floor(availableParallelism() / 2))
const started = Date.now()
const took = () => `${((Date.now() - started) / 1000).toFixed(0)}s`
/// Runs `rustup run stable cargo <args>`, its output shown, failing with it.
const run = (args, env = process.env, opts = {}) => {
const r = spawnSync('rustup', [...cargo, ...args], { cwd: root, env, stdio: 'inherit', ...opts })
if (r.status !== 0) process.exit(r.status ?? 1)
return r
}
// What cargo-llvm-cov sets to build and run instrumented, in its own
// target directory, as `cargo llvm-cov` uses.
const target = join(root, 'target/llvm-cov-target')
const shown = spawnSync('rustup', [...cargo, 'llvm-cov', 'show-env', '--sh'], {
cwd: root,
env: { ...process.env, CARGO_TARGET_DIR: target },
encoding: 'utf8',
})
const env = { ...process.env, CARGO_TARGET_DIR: target }
for (const [, k, v] of shown.stdout.matchAll(/^export ([A-Z_0-9]+)=(.*)$/gm)) env[k] = v.replace(/^'(.*)'$/, '$1')
// Only the last run's counts are cleared, so only what changed is built
// again. A binary left from an older build adds no counts, only regions
// none ran, so it can fail the gate but never pass it: COV_CLEAN=1 clears
// them too, as after a miss that makes no sense.
run(['llvm-cov', 'clean', process.env.COV_CLEAN ? '--workspace' : '--profraw-only'])
// The harnesses start while the tests build.
mkdirSync(join(root, 'target'), { recursive: true })
const harness = (name, devEnv) => {
const file = join(root, `target/cov-net-${name}.json`)
// What the PDS logs, which tests provoke on purpose, is kept out of the way.
const log = openSync(join(root, `target/cov-net-${name}.log`), 'w')
const child = spawn('node', [join(root, 'harness/pds.mjs'), '--accounts', 'seed'], {
cwd: root,
env: { ...process.env, KNOTLESS_DEV_ENV: devEnv },
stdio: ['pipe', 'pipe', log],
})
const up = new Promise((ok, fail) => {
let out = ''
child.stdout.on('data', (d) => {
out += d
if (!out.includes('\n')) return
writeFileSync(file, out)
ok(file)
})
child.on('exit', (code) => fail(new Error(`the ${name} harness exited with ${code}: target/cov-net-${name}.log`)))
})
return { up, stop: () => child.stdin.end() }
}
const nets = { stock: harness('stock', ''), spaces: harness('spaces', spaces) }
const stop = () => Object.values(nets).forEach((n) => n.stop())
process.on('exit', stop)
// Every test binary, and the package it is in: cargo runs each there.
const built = spawnSync('rustup', [...cargo, 'test', '--workspace', '--no-run', '--message-format=json'], {
cwd: root,
env,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
maxBuffer: 1 << 28,
})
if (built.status !== 0) process.exit(built.status ?? 1)
const tests = built.stdout
.split('\n')
.filter((l) => l.startsWith('{'))
.map((l) => JSON.parse(l))
.filter((m) => m.reason === 'compiler-artifact' && m.profile?.test && m.executable)
.map((m) => ({ exe: m.executable, dir: dirname(m.manifest_path), name: `${m.target.name} (${m.target.kind[0]})` }))
console.log(`built ${tests.length} test binaries in ${took()}`)
const files = Object.fromEntries(await Promise.all(Object.entries(nets).map(async ([k, n]) => [k, await n.up])))
// The integration tests first: they take longest, so start soonest.
const slow = (t) => (t.name.endsWith('(test)') ? 0 : 1)
const runs = Object.keys(files)
.flatMap((net) => tests.map((t) => ({ ...t, net })))
.sort((a, b) => slow(a) - slow(b))
/// One binary against one harness, its output kept to show if it fails.
const one = (t) =>
new Promise((ok) => {
const child = spawn(t.exe, [], {
cwd: t.dir,
env: {
...env,
KNOTLESS_TEST_NET: files[t.net],
KNOTLESS_DEV_ENV: t.net === 'spaces' ? spaces : '',
CARGO_MANIFEST_DIR: t.dir,
// As .cargo/config.toml sets them for cargo: where the answers'
// TypeScript types are written.
TS_RS_EXPORT_DIR: join(root, 'web/src/lib/api'),
TS_RS_LARGE_INT: 'number',
},
stdio: ['ignore', 'pipe', 'pipe'],
})
let out = ''
child.stdout.on('data', (d) => (out += d))
child.stderr.on('data', (d) => (out += d))
child.on('exit', (code) => ok({ ...t, code, out }))
})
const failed = []
let next = 0
await Promise.all(
Array.from({ length: jobs }, async () => {
while (next < runs.length) {
const r = await one(runs[next++])
const line = r.out.match(/^test result: .*$/m)?.[0] ?? `exited with ${r.code}`
console.log(`${r.code === 0 ? 'ok ' : 'FAIL'} ${r.net.padEnd(6)} ${r.name}: ${line}`)
if (r.code !== 0) failed.push(r)
}
}),
)
stop()
for (const r of failed) console.log(`\n---- ${r.net} ${r.name}\n${r.out}`)
if (failed.length) process.exit(1)
console.log(`ran ${runs.length} in ${took()}`)
run(['llvm-cov', 'report', '--ignore-filename-regex', ignore, '--fail-under-lines', '100', '--fail-under-functions', '100', '--show-missing-lines'])
console.log(`coverage in ${took()}`)
lexicons/dev/knotless/getFeed.json+4−2
@@ -9,7 +9,8 @@
"parameters": {
"type": "params",
"properties": {
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 40 }
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 40 },
"cursor": { "type": "string", "description": "Where the page starts: a previous page's cursor." }
}
},
"output": {
@@ -18,7 +19,8 @@
"type": "object",
"required": ["events"],
"properties": {
"events": { "type": "array", "items": { "type": "ref", "ref": "#eventView" } }
"events": { "type": "array", "items": { "type": "ref", "ref": "#eventView" } },
"cursor": { "type": "string", "description": "Where the next page starts, when there may be one." }
}
}
}
web/src/lib/api/Feed.ts+5−1
@@ -1,4 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FeedEvent } from "./FeedEvent";
export type Feed = { events: Array<FeedEvent>, };
export type Feed = { events: Array<FeedEvent>,
/**
* Where the next page starts, when there may be one.
*/
cursor?: string, };