Skip to content
Draft
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
405 changes: 405 additions & 0 deletions docs/plans/2026-06-30-kernel-test-fixture-ownership-design.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/repository-organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Kandelo is organized as a kernel-first monorepo. The kernel and host runtimes ar
| `web-libs/` | Browser-independent reusable UI/session contracts | App-specific page code |
| `packages/registry/<name>/` | One ported package: manifest, build script, patches, package-owned demos, package-owned tests | Kernel/host behavior tests |
| `packages/sets/` | Named product or CI package sets | Package implementation details |
| `tests/` | External conformance suites and package-system tooling tests | Package-owned integration tests |
| `tests/` | External conformance suites, package-system tooling tests, and shared host/kernel test artifact manifests | Package-owned integration tests |
| `images/` | Rootfs sources and VFS/archive build scripts | Package source builds |
| `tools/` | Repo automation such as `xtask` and `mkrootfs` | Product runtime code |
| `sdk/` | Cross-compilation wrapper CLI and SDK support code | Runtime host implementation |
Expand Down Expand Up @@ -55,6 +55,7 @@ Package behavior tests live with the package so future CI can map changes to rel
| `host/test/` | Host/kernel runtime behavior: process lifecycle, VFS semantics, syscalls, worker behavior, host parity |
| `packages/registry/<name>/test/` | Behavior of a specific ported package |
| `tests/package-system/` | Package registry and binary-fetching automation |
| `tests/test-artifacts/` | Shared host/kernel test artifact ownership manifests |
| `tests/libc/`, `tests/posix/`, `tests/sortix/` | External conformance suites and overlays |
| `apps/browser-demos/test/` | Browser app and demo-page integration behavior |

Expand Down
7 changes: 4 additions & 3 deletions packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ kernel, but this tree keeps the CI and shipped software story organized.
- `registry/<name>/test/` contains package-owned tests and fixtures. A package
PR should be able to trigger these paths without treating the change as a
host/runtime change.
- `registry/kernel-test-programs/` describes the small wasm programs used by
host/kernel smoke tests; the source files themselves live in `../programs/`
and `../examples/`.
- `sets/*.toml` names product or CI scenarios that should be kept buildable as
a group. These are advisory manifests today; automation can consume them
once the package-set schema is wired into `tools/xtask`.

Shared host/kernel test artifact inventories live under
[`../tests/test-artifacts/`](../tests/test-artifacts/), not in the package
registry.

Package-system tests that validate registry tooling rather than a specific
package live in [`../tests/package-system/`](../tests/package-system/). Root
[`../examples/`](../examples/) is reserved for small kernel and SDK examples.
39 changes: 0 additions & 39 deletions packages/registry/kernel-test-programs/package.toml

This file was deleted.

2 changes: 1 addition & 1 deletion scripts/fetch-binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# places `binaries/programs/<arch>/<output>.wasm` symlinks pointing
# into the cache. Browser demos hardcode these paths.
#
# Packages without `build.toml` (kernel-test-programs, kind=source,
# Packages without `build.toml` (kind=source, manifest-only entries,
# libraries that ship only as link-time inputs) are skipped here.
# Kernel and userspace now have build.toml entries, so published
# release indexes can populate `binaries/kernel.wasm` and
Expand Down
2 changes: 2 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ External and generated conformance test trees live here.
automation.
- `posix/` contains the Open POSIX test suite.
- `sortix/` contains the Sortix os-test submodule and build outputs.
- `test-artifacts/` contains explicit ownership manifests for shared
host/kernel test artifacts that are not packages.
- `results/` stores local test-run metadata.

Package-owned tests and fixtures live beside their packages under
Expand Down
1 change: 0 additions & 1 deletion tests/package-system/browser-binary-dependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const localOnlyBrowserImports = new Set([
]);

const registryPackagesWithoutBuildToml = new Set([
"kernel-test-programs",
"pcre2-source",
"sqlite-cli",
]);
Expand Down
6 changes: 3 additions & 3 deletions tests/package-system/fetch-binaries-allow-stale.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* --binaries-dir <repo>/binaries resolve <name>
*
* once per declared arch. Packages without a sibling `build.toml` are
* skipped (kernel/userspace/source/etc).
* skipped (source/helper or stale manifest-only entries).
*
* Strategy: stage a fake "repo root" in a tempdir with a hand-
* written `packages/registry/` tree of one-line package.toml files
Expand Down Expand Up @@ -166,8 +166,8 @@ describe.skipIf(!rustcAvailable)("fetch-binaries.sh per-package walk", () => {
);
writeFileSync(path.join(registry, "charlie", "build.toml"), `revision = 1\n`);

// delta: kind=program, no build.toml (mirrors kernel/userspace/
// examples). Must be skipped silently.
// delta: kind=program, no build.toml (mirrors source/helper or stale
// manifest-only entries). Must be skipped silently.
mkdirSync(path.join(registry, "delta"), { recursive: true });
writeFileSync(
path.join(registry, "delta", "package.toml"),
Expand Down
162 changes: 162 additions & 0 deletions tests/package-system/kernel-test-fixtures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
const manifestPath = join(repoRoot, "tests", "test-artifacts", "kernel-test-programs.json");
const registryPackagePath = join(repoRoot, "packages", "registry", "kernel-test-programs");
const browserBinaryDepsTest = join(
repoRoot,
"tests",
"package-system",
"browser-binary-dependencies.test.ts",
);

type FixtureEntry = {
name: string;
arch: "wasm32" | "wasm64";
source: string;
binary: string;
resolver_path: string;
consumers: string[];
};

type LegacyOutput = {
name: string;
wasm: string;
disposition: string;
fixture?: string;
owner?: string;
alternate_test_fixture?: string;
reason?: string;
};

type KernelTestProgramsManifest = {
schema: number;
name: string;
owner: string;
producer: {
script: string;
output_root: string;
};
fixtures: FixtureEntry[];
legacy_outputs: LegacyOutput[];
};

const expectedFixtures = [
"exec-caller",
"exec-child",
"fork-exec",
"hello64",
"ifhwaddr",
"mmap_shared_test",
].sort();

const legacyOutputsFromRemovedPackage = [
"exec-caller",
"exec-child",
"fork-exec",
"hello",
"hello64",
"ifhwaddr",
"mmap_shared_test",
].sort();

function readManifest(): KernelTestProgramsManifest {
return JSON.parse(readFileSync(manifestPath, "utf8")) as KernelTestProgramsManifest;
}

function isSafeRelativePath(path: string): boolean {
if (!path || path.startsWith("/") || path.includes("\\")) return false;
return !path.split("/").some((segment) => !segment || segment === "." || segment === "..");
}

function fileExists(relPath: string): boolean {
return existsSync(join(repoRoot, relPath));
}

function formulaNames(): string[] {
const formulaDir = join(repoRoot, "homebrew", "kandelo-homebrew", "Formula");
if (!existsSync(formulaDir)) return [];
return readdirSync(formulaDir)
.filter((name) => name.endsWith(".rb"))
.map((name) => name.replace(/\.rb$/, ""));
}

describe("kernel test fixture ownership", () => {
it("keeps kernel-test-programs out of package and Homebrew discovery", () => {
expect(existsSync(join(registryPackagePath, "package.toml"))).toBe(false);
expect(formulaNames()).not.toContain("kernel-test-programs");
expect(
existsSync(
join(repoRoot, "homebrew", "kandelo-homebrew", "Kandelo", "formula", "kernel-test-programs.json"),
),
).toBe(false);

const browserBinaryDepsSource = readFileSync(browserBinaryDepsTest, "utf8");
const exceptionSet = browserBinaryDepsSource.match(
/const registryPackagesWithoutBuildToml = new Set\(\[([\s\S]*?)\]\);/,
);
expect(exceptionSet?.[1] ?? "").not.toContain("\"kernel-test-programs\"");
});

it("records every migrated fixture with source, output, and consumer ownership", () => {
const manifest = readManifest();
expect(manifest.schema).toBe(1);
expect(manifest.name).toBe("kernel-test-programs");
expect(manifest.owner).toBe("host-kernel-tests");
expect(manifest.producer).toEqual({
script: "scripts/build-programs.sh",
output_root: "local-binaries/programs",
});
expect(isSafeRelativePath(manifest.producer.script), "producer script").toBe(true);
expect(isSafeRelativePath(manifest.producer.output_root), "producer output root").toBe(true);
expect(fileExists(manifest.producer.script)).toBe(true);

const fixtureNames = manifest.fixtures.map((fixture) => fixture.name).sort();
expect(fixtureNames).toEqual(expectedFixtures);

for (const fixture of manifest.fixtures) {
expect(isSafeRelativePath(fixture.source), `${fixture.name} source`).toBe(true);
expect(isSafeRelativePath(fixture.binary), `${fixture.name} binary`).toBe(true);
expect(isSafeRelativePath(fixture.resolver_path), `${fixture.name} resolver_path`).toBe(true);
expect(fileExists(fixture.source), `${fixture.name} source exists`).toBe(true);
expect(fixture.binary).toBe(`programs/${fixture.arch}/${fixture.name}.wasm`);
if (fixture.arch === "wasm32") {
expect(fixture.resolver_path).toBe(`programs/${fixture.name}.wasm`);
} else {
expect(fixture.resolver_path).toBe(fixture.binary);
}
expect(fixture.consumers.length, `${fixture.name} consumers`).toBeGreaterThan(0);
for (const consumer of fixture.consumers) {
expect(isSafeRelativePath(consumer), `${fixture.name} consumer ${consumer}`).toBe(true);
expect(fileExists(consumer), `${fixture.name} consumer ${consumer} exists`).toBe(true);
}
}
});

it("accounts for every output from the removed registry manifest", () => {
const manifest = readManifest();
const legacyNames = manifest.legacy_outputs.map((output) => output.name).sort();
expect(legacyNames).toEqual(legacyOutputsFromRemovedPackage);

const fixtures = new Set(manifest.fixtures.map((fixture) => fixture.name));
for (const output of manifest.legacy_outputs) {
expect(isSafeRelativePath(output.wasm), `${output.name} wasm`).toBe(true);
if (output.disposition === "migrated-fixture") {
expect(output.fixture, `${output.name} fixture pointer`).toBe(output.name);
expect(fixtures.has(output.fixture!), `${output.name} fixture exists`).toBe(true);
} else if (output.name === "hello") {
expect(output.disposition).toBe("package-owned");
expect(output.owner).toBe("packages/registry/hello/package.toml");
expect(output.alternate_test_fixture).toBe("examples/hello.wasm");
expect(output.reason).toMatch(/GNU hello/);
expect(fileExists(output.owner!), "hello owner exists").toBe(true);
expect(fileExists("examples/hello.c"), "hello test source exists").toBe(true);
} else {
throw new Error(`unexpected legacy disposition for ${output.name}: ${output.disposition}`);
}
}
});
});
Loading
Loading