Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .agents/conformance-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ Generated 2026-08-01 with `pnpm matrix`.
| **loopback** | `test/drivers.test.ts` — the driver behind `createLoopback`, no transport | 252 passed, 32 skipped (4 targets: memory; node-fs; unstorage; node:fs/promises (raw)) |
| **FUSE** | `test/fuse/conformance-mount.test.ts` — a real kernel mount, `node:fs` as the client | 130 passed, 12 skipped (2 targets: memory driver, through a FUSE mount; node-fs driver, through a FUSE mount) |
| **9P** | `test/9p/conformance.test.ts` — 9P2000.L through the codecs, the JS client from `test/9p/client.ts` | 204 passed, 9 skipped (3 targets: memory driver, over 9P; memory driver with no handles, over 9P; node-fs oracle, over 9P) |
| **NFSv3** | `test/nfs/v3/conformance.test.ts` — NFSv3 over a TCP socket, the JS client from `test/nfs/v3/client.ts` | 124 passed, 18 skipped (2 targets: memory driver, over NFS; node-fs driver, over NFS) |
| **NFSv4.1** | `test/nfs/v4/conformance.test.ts` — NFSv4.1 over a TCP socket, the JS client from `test/nfs/v4/client.ts` and the driver over it in `test/nfs/v4/driver.ts` | 124 passed, 18 skipped (2 targets: memory driver, over NFSv4.1; node-fs driver, over NFSv4.1) |
| **NFSv3** | `test/nfs/v3/conformance.test.ts` — NFSv3 over a TCP socket, the JS client from `test/nfs/v3/client.ts` | 128 passed, 14 skipped (2 targets: memory driver, over NFS; node-fs driver, over NFS) |
| **NFSv4.1** | `test/nfs/v4/conformance.test.ts` — NFSv4.1 over a TCP socket, the JS client from `test/nfs/v4/client.ts` and the driver over it in `test/nfs/v4/driver.ts` | 128 passed, 14 skipped (2 targets: memory driver, over NFSv4.1; node-fs driver, over NFSv4.1) |
| **S3** | `test/s3/conformance.test.ts` — an S3 gateway in process, the JS client from `test/s3/client.ts` | 48 passed, 23 skipped (1 target: memory driver, over S3) |

## Capability loss

Derived from the run, not declared here: a requirement counts as unmet in a column when no case that names it passed there. `root` is an environment fact rather than a transport one — it gates the one case that hands a file away, which only root may do — so it is reported in its own column, and _recorded_ there rather than derived: a column can skip that case for want of `symlinks` long before privilege is reached, so the absence of a pass is not evidence about the run's uid. Every column is run with root when root is reachable, including the five that do not need it. A `mountx.*` requirement is left out of this table entirely: the suite calls an extension by name through `fs.mountx`, so a skip is a fact about whether that column's client offers the name, not about what the transport carries. Two columns do offer it — the loopback one directly, and the 9P one because `Tmknod` carries the whole `mode` and `p9Driver` can hand it over unchanged. The rest skip for reasons of their own: FUSE drives a real mount with `node:fs` as the client, and `node:fs` cannot `mknod(2)`; NFSv3 and NFSv4.1 carry the file type in `ftype3`/`nfs_ftype4` rather than in the mode, so a client there could not offer the whole extension without deciding part of it itself; S3 has no way to name a FIFO at all. All four sessions do carry `mknod` — see the per-case rows below, and the FUSE column's own `mkfifo`/`mknod`/`bind` case over a real mount.
Derived from the run, not declared here: a requirement counts as unmet in a column when no case that names it passed there. `root` is an environment fact rather than a transport one — it gates the one case that hands a file away, which only root may do — so it is reported in its own column, and _recorded_ there rather than derived: a column can skip that case for want of `symlinks` long before privilege is reached, so the absence of a pass is not evidence about the run's uid. Every column is run with root when root is reachable, including the five that do not need it. A `mountx.*` requirement is left out of this table entirely: the suite calls an extension by name through `fs.mountx`, so a skip is a fact about whether that column's client offers the name, not about what the transport carries. Four columns do offer it — the loopback one directly, the 9P one because `Tmknod` carries the whole `mode` and `p9Driver` can hand it over unchanged, and both NFS ones because MKNOD and CREATE are operations their clients can call by name. The NFS pair offers it in part: `ftype3`/`nfs_ftype4` carries the file type and the mode carries only permission bits, so the two cases needing a mode to name a type are gated on `mknod.anyType` and skip there rather than let a client decide them itself. The remaining columns skip for reasons of their own: FUSE drives a real mount with `node:fs` as the client, and `node:fs` cannot `mknod(2)`; S3 has no way to name a FIFO at all. All four sessions do carry `mknod` — see the per-case rows below, and the FUSE column's own `mkfifo`/`mknod`/`bind` case over a real mount.

**What this direction of derivation cannot check.** A column declares its own capabilities (`THROUGH_FUSE`, `THROUGH_9P`/`THROUGH_9P_REOPENED`, `THROUGH_NFS`, `THROUGH_NFS4`, `THROUGH_S3`), and declaring one `false` skips every case that needs it — which is exactly what a real loss looks like from here. So a capability the transport _does_ carry, wrongly declared lost, is reported as a loss with nothing to contradict it; the evidence only ever runs the other way, from a passing case to a capability that must be present. Every entry below is therefore a claim the transport's own test file makes and the run did not refute, and the comment at each declaration is where the reasoning for it lives.

Expand Down Expand Up @@ -131,14 +131,14 @@ Derived from the run, not declared here: a requirement counts as unmet in a colu

### special files

| Case | Needs | loopback | FUSE | 9P | NFSv3 | NFSv4.1 | S3 |
| ---------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------- | ---------------------------------- | ---------------------------------- |
| creates a FIFO and a socket that stat and readdir both name | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mountx.mknod | skip: mountx.mknod | skip: mountx.mknod |
| carries the device number of a character and a block device | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mountx.mknod | skip: mountx.mknod | skip: mountx.mknod |
| creates a regular file from a mode naming one, or naming no type | `mountx.mknod` + `mknod.anyType` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod + mknod.anyType | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mountx.mknod + mknod.anyType | skip: mountx.mknod + mknod.anyType | skip: mountx.mknod + mknod.anyType |
| is an ordinary name once it exists: rename, unlink, stat again | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mountx.mknod | skip: mountx.mknod | skip: mountx.mknod |
| refuses an existing name and a missing directory | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mountx.mknod | skip: mountx.mknod | skip: mountx.mknod |
| refuses a type with its own call | `mountx.mknod` + `mknod.anyType` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod + mknod.anyType | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mountx.mknod + mknod.anyType | skip: mountx.mknod + mknod.anyType | skip: mountx.mknod + mknod.anyType |
| Case | Needs | loopback | FUSE | 9P | NFSv3 | NFSv4.1 | S3 |
| ---------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------- |
| creates a FIFO and a socket that stat and readdir both name | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | pass (memory driver, over NFS), skip (node-fs driver, over NFS) | pass (memory driver, over NFSv4.1), skip (node-fs driver, over NFSv4.1) | skip: mountx.mknod |
| carries the device number of a character and a block device | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | pass (memory driver, over NFS), skip (node-fs driver, over NFS) | pass (memory driver, over NFSv4.1), skip (node-fs driver, over NFSv4.1) | skip: mountx.mknod |
| creates a regular file from a mode naming one, or naming no type | `mountx.mknod` + `mknod.anyType` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod + mknod.anyType | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mknod.anyType | skip: mknod.anyType | skip: mountx.mknod + mknod.anyType |
| is an ordinary name once it exists: rename, unlink, stat again | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | pass (memory driver, over NFS), skip (node-fs driver, over NFS) | pass (memory driver, over NFSv4.1), skip (node-fs driver, over NFSv4.1) | skip: mountx.mknod |
| refuses an existing name and a missing directory | `mountx.mknod` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | pass (memory driver, over NFS), skip (node-fs driver, over NFS) | pass (memory driver, over NFSv4.1), skip (node-fs driver, over NFSv4.1) | skip: mountx.mknod |
| refuses a type with its own call | `mountx.mknod` + `mknod.anyType` | pass (memory), skip (node-fs), skip (unstorage), skip (node:fs/promises (raw)) | skip: mountx.mknod + mknod.anyType | pass (memory driver, over 9P), pass (memory driver with no handles, over 9P), skip (node-fs oracle, over 9P) | skip: mknod.anyType | skip: mknod.anyType | skip: mountx.mknod + mknod.anyType |

### metadata

Expand Down
25 changes: 17 additions & 8 deletions .agents/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,24 @@ through its Tier-1 JS client, and FUSE contributes a real-mount column.
the same capabilities now that `unstorage` runs beside `memory`. A `mountx.*`
requirement is dropped from the "capabilities lost" table (not from the per-case
rows): the suite reaches an extension by name through `fs.mountx`, and whether a
column's client offers that name is a fact about the client. Two do — the loopback
column directly, and the 9P one because `Tmknod` carries the whole `mode` and
`p9Driver.mountx.mknod` hands it over unchanged, so the special-files cases run
there against the memory targets. The rest skip for their own reasons: FUSE's
column's client offers that name is a fact about the client. Four do — the loopback
column directly, the 9P one because `Tmknod` carries the whole `mode` and
`p9Driver.mountx.mknod` hands it over unchanged, and both NFS ones because MKNOD
(§3.3.11) and CREATE (§18.4) are operations their clients can call by name — so the
special-files cases run there against the memory targets. The two that do not: FUSE's
client is `node:fs`, which cannot `mknod(2)` (its column covers special files by
its own case and by pjdfstest instead); NFSv3 and NFSv4.1 put the file type in
`ftype3`/`nfs_ftype4` rather than in the mode, so a client there cannot offer the
whole extension without deciding part of it itself; S3 cannot name a FIFO. All
four sessions carry `mknod` either way.
its own case and by pjdfstest instead), and S3 cannot name a FIFO at all. All four
sessions carry `mknod` either way.
- A column may carry _part_ of an extension, which `capabilities.extensions` cannot
express: `ConformanceTarget.carries` is that, and `Carried` has one member,
`mknod.anyType`. NFSv3 and NFSv4.1 put the file type in `ftype3`/`nfs_ftype4` and
leave the mode carrying permission bits only, so a mode naming a regular file, a
directory or no type at all is a question neither wire can ask — and answering it in
the client would be the client inventing an errno the driver should have produced
(invariant 5). The two cases needing it are gated on `mknod.anyType`, both NFS targets
declare `carries: []`, and the adapters throw a plain, deliberately non-errno-shaped
`Error` for those types so that un-gating a case fails loudly instead of passing for
the wrong reason. Unset means carried whole, so nothing else had to change.

## Per area

Expand Down
22 changes: 12 additions & 10 deletions test/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,14 +446,16 @@ function main(): void {
"reachable, including the five that do not need it. A `mountx.*` requirement is left out " +
"of this table entirely: the suite calls an extension by name through `fs.mountx`, so a " +
"skip is a fact about whether that column's client offers the name, not about what the " +
"transport carries. Two columns do offer it — the loopback one directly, and the 9P one " +
"because `Tmknod` carries the whole `mode` and `p9Driver` can hand it over unchanged. The " +
"rest skip for reasons of their own: FUSE drives a real mount with `node:fs` as the " +
"client, and `node:fs` cannot `mknod(2)`; NFSv3 and NFSv4.1 carry the file type in " +
"`ftype3`/`nfs_ftype4` rather than in the mode, so a client there could not offer the " +
"whole extension without deciding part of it itself; S3 has no way to name a FIFO at all. " +
"All four sessions do carry `mknod` — see the per-case rows below, and the FUSE column's " +
"own `mkfifo`/`mknod`/`bind` case over a real mount.",
"transport carries. Four columns do offer it — the loopback one directly, the 9P one " +
"because `Tmknod` carries the whole `mode` and `p9Driver` can hand it over unchanged, and " +
"both NFS ones because MKNOD and CREATE are operations their clients can call by name. " +
"The NFS pair offers it in part: `ftype3`/`nfs_ftype4` carries the file type and the mode " +
"carries only permission bits, so the two cases needing a mode to name a type are gated on " +
"`mknod.anyType` and skip there rather than let a client decide them itself. The remaining " +
"columns skip for reasons of their own: FUSE drives a real mount with `node:fs` as the " +
"client, and `node:fs` cannot `mknod(2)`; S3 has no way to name a FIFO at all. All four " +
"sessions do carry `mknod` — see the per-case rows below, and the FUSE column's own " +
"`mkfifo`/`mknod`/`bind` case over a real mount.",
);
push();
push(
Expand All @@ -476,8 +478,8 @@ function main(): void {
// `root` is an environment fact, reported in its own cell. A `mountx.*`
// requirement is neither a capability nor a loss: the suite reaches an
// extension by name through `fs.mountx`, and whether a column's client
// offers that name is a fact about the client. The loopback and 9P columns
// do; the others skip for reasons that are theirs rather than the
// offers that name is a fact about the client. The loopback, 9P and two NFS
// columns do; the others skip for reasons that are theirs rather than the
// session's (see the preamble above). A skip says nothing about whether the
// transport carries the *operation* (all four sessions carry `mknod`), so
// calling it a loss would be a claim the run cannot make in either
Expand Down
Loading
Loading