test(harness): name the root check for what it asserts, and lint the crate
`open_and_refresh_see_an_empty_origin_root` claimed more than it checked: since the previous commit an empty *entry* is not a document, so `list` being empty means "no documents", not "no entries". A check whose name overstates it is the shape this repo keeps finding — a green tick against a claim nobody verified. Renamed, and `wipe`'s doc now says out loud that it cannot clear a zero-byte entry, and why that costs nothing. `harness/opfs-contract` is not a workspace member, so the `test` job's workspace-wide `cargo fmt --check` and clippy never reach it. The browser job now runs both against it — it was, in fact, unformatted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,13 @@ jobs:
|
||||
curl -sSfL "$url" | tar -xz --strip-components=1 -C /usr/local/bin \
|
||||
"wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl/wasm-pack"
|
||||
wasm-pack --version
|
||||
# Not a workspace member (it only builds for wasm32), so the `test` job's
|
||||
# workspace-wide fmt and clippy do not reach it.
|
||||
- name: Lint the harness crate
|
||||
working-directory: harness/opfs-contract
|
||||
run: |
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --target wasm32-unknown-unknown --all-targets -- -D warnings
|
||||
- name: Install the harness
|
||||
working-directory: harness
|
||||
run: npm ci
|
||||
|
||||
@@ -62,12 +62,18 @@ impl Report {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove every entry in the origin root, so a run does not inherit the last
|
||||
/// one's leftovers.
|
||||
/// Remove every *document* in the origin root, so a run does not inherit the
|
||||
/// last one's leftovers.
|
||||
///
|
||||
/// Uses only the backend's own vocabulary — `refresh`, `delete`,
|
||||
/// `commit_deletes` — rather than reaching for `removeEntry` directly, so a
|
||||
/// broken `delete` shows up here as well as in the contract.
|
||||
///
|
||||
/// It therefore cannot clear a zero-byte entry, because `list` no longer
|
||||
/// names one. That is deliberate and costs nothing: `attach` recreates and
|
||||
/// `write` fills whatever a previous run left behind, and a leftover empty
|
||||
/// entry is invisible to every assertion here for the same reason it is
|
||||
/// invisible to a document picker.
|
||||
async fn wipe(fs: &OpfsFs) -> Result<usize> {
|
||||
fs.refresh().await?;
|
||||
let names = fs.list(Path::new(""))?;
|
||||
@@ -99,12 +105,15 @@ pub async fn run_checks() -> Result<String, JsError> {
|
||||
.map_err(|e| JsError::new(&format!("wipe: {e:#}")))?;
|
||||
|
||||
report
|
||||
.run("open_and_refresh_see_an_empty_origin_root", async || {
|
||||
fs.refresh().await?;
|
||||
let listed = fs.list(Path::new(""))?;
|
||||
ensure!(listed.is_empty(), "a wiped root listed {listed:?}");
|
||||
Ok("empty".into())
|
||||
})
|
||||
.run(
|
||||
"open_and_refresh_see_an_origin_root_holding_no_documents",
|
||||
async || {
|
||||
fs.refresh().await?;
|
||||
let listed = fs.list(Path::new(""))?;
|
||||
ensure!(listed.is_empty(), "a wiped root listed {listed:?}");
|
||||
Ok("empty".into())
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// `attach` must create the file to obtain a sync access handle for a
|
||||
@@ -114,28 +123,37 @@ pub async fn run_checks() -> Result<String, JsError> {
|
||||
// the improvise-9x6 browser failure (the seed was skipped as "already
|
||||
// there" and `load_from` got an empty buffer).
|
||||
report
|
||||
.run("an_attached_but_unwritten_entry_is_not_a_document", async || {
|
||||
fs.attach(Path::new("attached.improv")).await?;
|
||||
let listed = fs.list(Path::new(""))?;
|
||||
ensure!(listed.is_empty(), "attach listed {listed:?} before anything was written");
|
||||
.run(
|
||||
"an_attached_but_unwritten_entry_is_not_a_document",
|
||||
async || {
|
||||
fs.attach(Path::new("attached.improv")).await?;
|
||||
let listed = fs.list(Path::new(""))?;
|
||||
ensure!(
|
||||
listed.is_empty(),
|
||||
"attach listed {listed:?} before anything was written"
|
||||
);
|
||||
|
||||
// ...and a second instance, which shares no index, must not see
|
||||
// it either: the filter has to be in `refresh`, not only in the
|
||||
// omission from `attach`.
|
||||
let other = OpfsFs::open().await?;
|
||||
other.refresh().await?;
|
||||
let seen = other.list(Path::new(""))?;
|
||||
ensure!(seen.is_empty(), "refresh() listed the empty entry: {seen:?}");
|
||||
// ...and a second instance, which shares no index, must not see
|
||||
// it either: the filter has to be in `refresh`, not only in the
|
||||
// omission from `attach`.
|
||||
let other = OpfsFs::open().await?;
|
||||
other.refresh().await?;
|
||||
let seen = other.list(Path::new(""))?;
|
||||
ensure!(
|
||||
seen.is_empty(),
|
||||
"refresh() listed the empty entry: {seen:?}"
|
||||
);
|
||||
|
||||
// The handle is real, though: writing through it works, and then
|
||||
// it IS a document.
|
||||
fs.write(Path::new("attached.improv"), b"now it is one")?;
|
||||
ensure!(
|
||||
fs.list(Path::new(""))? == vec![std::path::PathBuf::from("attached.improv")],
|
||||
"a written document was not listed"
|
||||
);
|
||||
Ok("created, unlisted until written".into())
|
||||
})
|
||||
// The handle is real, though: writing through it works, and then
|
||||
// it IS a document.
|
||||
fs.write(Path::new("attached.improv"), b"now it is one")?;
|
||||
ensure!(
|
||||
fs.list(Path::new(""))? == vec![std::path::PathBuf::from("attached.improv")],
|
||||
"a written document was not listed"
|
||||
);
|
||||
Ok("created, unlisted until written".into())
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// The hand-rolled `keys()` async-iterator walk in `refresh` is the least
|
||||
@@ -168,7 +186,10 @@ pub async fn run_checks() -> Result<String, JsError> {
|
||||
let p = Path::new("offsets.improv");
|
||||
fs.attach(p).await?;
|
||||
fs.write(p, b"0123456789ABCDEF")?;
|
||||
ensure!(fs.read(p)? == b"0123456789ABCDEF", "long write did not round-trip");
|
||||
ensure!(
|
||||
fs.read(p)? == b"0123456789ABCDEF",
|
||||
"long write did not round-trip"
|
||||
);
|
||||
fs.write(p, b"xy")?;
|
||||
let short = fs.read(p)?;
|
||||
ensure!(
|
||||
@@ -200,32 +221,38 @@ pub async fn run_checks() -> Result<String, JsError> {
|
||||
// OPFS at the end of the batch. Both halves, because either one alone
|
||||
// would pass against a backend that got the timing exactly backwards.
|
||||
report
|
||||
.run("a_delete_is_visible_at_once_and_lands_on_commit", async || {
|
||||
wipe(&fs).await?;
|
||||
let p = Path::new("doomed.improv");
|
||||
fs.attach(p).await?;
|
||||
fs.write(p, b"bytes")?;
|
||||
.run(
|
||||
"a_delete_is_visible_at_once_and_lands_on_commit",
|
||||
async || {
|
||||
wipe(&fs).await?;
|
||||
let p = Path::new("doomed.improv");
|
||||
fs.attach(p).await?;
|
||||
fs.write(p, b"bytes")?;
|
||||
|
||||
fs.delete(p)?;
|
||||
ensure!(fs.read(p).is_err(), "a deleted document was still readable");
|
||||
ensure!(fs.list(Path::new(""))?.is_empty(), "a deleted document was still listed");
|
||||
fs.delete(p)?;
|
||||
ensure!(fs.read(p).is_err(), "a deleted document was still readable");
|
||||
ensure!(
|
||||
fs.list(Path::new(""))?.is_empty(),
|
||||
"a deleted document was still listed"
|
||||
);
|
||||
|
||||
let before = OpfsFs::open().await?;
|
||||
before.refresh().await?;
|
||||
ensure!(
|
||||
before.list(Path::new(""))?.iter().any(|q| q == p),
|
||||
"the entry left OPFS before commit_deletes — the queue is not a queue"
|
||||
);
|
||||
let before = OpfsFs::open().await?;
|
||||
before.refresh().await?;
|
||||
ensure!(
|
||||
before.list(Path::new(""))?.iter().any(|q| q == p),
|
||||
"the entry left OPFS before commit_deletes — the queue is not a queue"
|
||||
);
|
||||
|
||||
fs.commit_deletes().await?;
|
||||
let after = OpfsFs::open().await?;
|
||||
after.refresh().await?;
|
||||
ensure!(
|
||||
!after.list(Path::new(""))?.iter().any(|q| q == p),
|
||||
"commit_deletes did not remove the entry"
|
||||
);
|
||||
Ok("queued then removed".into())
|
||||
})
|
||||
fs.commit_deletes().await?;
|
||||
let after = OpfsFs::open().await?;
|
||||
after.refresh().await?;
|
||||
ensure!(
|
||||
!after.list(Path::new(""))?.iter().any(|q| q == p),
|
||||
"commit_deletes did not remove the entry"
|
||||
);
|
||||
Ok("queued then removed".into())
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(report.finish())
|
||||
|
||||
@@ -21,7 +21,7 @@ async function run(page, cmd) {
|
||||
* Rust on purpose: a check deleted on one side has to be deleted on both,
|
||||
* which is what stops the battery quietly shrinking. */
|
||||
const EXPECTED = [
|
||||
"open_and_refresh_see_an_empty_origin_root",
|
||||
"open_and_refresh_see_an_origin_root_holding_no_documents",
|
||||
"an_attached_but_unwritten_entry_is_not_a_document",
|
||||
"refresh_walks_every_key_and_sorts_them",
|
||||
"read_and_write_are_absolute_at_offset_zero",
|
||||
|
||||
Reference in New Issue
Block a user