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
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ issue when you are not sure.
repository preload, and the repository browser
wrapper wait automatically for other commands using the same checkout's build
output. Runtime tests hold exclusive access because their fixtures rebuild the
packages. Every selected package is rebuilt. Persistent build-output reuse is
packages. Coordinated runtime invocations freshly build their selected package
closure once before tests; hooks verify that same live invocation, checkout,
exclusive lease and successful build before omitting duplicate compilation.
Direct or unattested hooks rebuild. Source edits made during a run take effect
on the next invocation. Ordinary explicit builds still rebuild every selected
package. Watch/hot runs remain unattested. Persistent build-output reuse is
disabled
until its effective configuration, installed toolchain, runtime and stylesheet
inputs can be qualified; package versions and symlink target strings are not
Expand Down
60 changes: 60 additions & 0 deletions docs/decisions/ci-utility.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,66 @@ this now-completed same-source count comparison. Dependency copies and private
builds count in proof time and sampled VM resources; setup/install and queues
remain separate step measurements. No deadline, shard count or N changed here.

### One fresh build per test invocation

The [2026-10-20 option-B ruling for #2279](https://github.com/schlessera/brain-kit/issues/2279#issuecomment-6293488248)
permits one fresh package build under the invocation's continuously held write
lease. `scripts/test.ts` prepares the selected runtime consumers' forward
package closure before starting Bun tests. Each private unit worker requests a
full build inside that same build/test lifetime, after namespace entry. There
are still at most two workers, with serial compilers inside each. A worker can
now start tests while its sibling is still preparing; account for this scheduling
placement separately from duplicate compilation avoided.

The seventeen runtime hooks use a checked helper. Admission requires the live
ancestor coordinator's PID/start identity, invocation nonce, canonical checkout
and root inode, active exclusive lease owner/descriptor/inode, and the actual
builder's completed successful package/child receipt for the required closure.
The coordinator waits for exit and both output EOFs before publishing a bounded
record through its held descriptor. Closing the descriptor or losing the owner
revokes it; leftover files and the builder's best-effort observation fingerprints
have no authority. Failed or signaled preparation stops the invocation before
tests; missing diagnostics after successful preparation leave hooks rebuilding.

Build and test commands each run beneath a private process-group anchor that
stays alive until command exit and both output EOFs have been joined. Cancellation
signals only that anchored group, with one-second forced cleanup for survivors
and children forked during signal handling. Losing the wrapper's IPC lifetime
channel also stops the group. A command leader exiting first cannot orphan its
inherited-output holders, and a nested wrapper does not signal the outer lease
or unrelated siblings. The anchor closes its own output descriptors after launch
so it cannot manufacture EOF or hold output open itself. Cancellation after a
successful builder exit still prevents attestation. This is cooperating
process-group ownership, not containment of processes deliberately escaping
their process group; it adds no product extension interface.

Every ordinary nested wrapper creates a fresh invocation. Only the explicitly
checked isolated external-speech child borrows its live parent's build, outside
both its setup and outer-test deadlines. Direct/unattested hooks rebuild;
standalone coordinated runtime invocations freshly prepare. Shared read batches,
unsupported alternate working directories and watch/hot runs remain unattested.
Ordinary explicit `scripts/build.ts` calls still compile every selected package,
including freshness/configuration/compiler/output-damage fixture controls.

A receipt describes its own build, not whatever `dist` holds later. Every
writer of the shared package output (`scripts/build.ts`, including nested
wrappers, hook fallbacks and the CI build operation, and `scripts/clean.ts`)
first replaces the workspace's random output generation token, before any
mutation. The receipt records its build's token, and admission requires it to
be current. A later successful, failed or cancelled writer beneath the same
lease therefore revokes the outer invocation's authority, and its remaining
hooks rebuild. The token only revokes; matching it never makes output fresh.
`--help` and `-h` reach Bun without preparation.
Client/browser bundling stays fresh and consumers keep their default exports.

Source edits during a run take effect at the next invocation, as explicitly
accepted by the ruling. The lease coordinates cooperating first-party writers;
it does not make source immutable or protect against hostile same-UID processes.
No persistent stamp, boolean skip, generic producer interface, assertion or
deadline change follows. This is distinct from deferred #2319 activation below.
Measure complete preparation, hook admission, test execution and cleanup costs;
small hook durations alone are not a whole-invocation speedup.

### Opt-in compilation within one hosted workspace

The [same-source compilation comparison](https://github.com/schlessera/brain-kit/issues/2321#issuecomment-6233589351)
Expand Down
4 changes: 2 additions & 2 deletions docs/process/test-waits.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ restrict egress. Failed scene preparation closes its owned browser context,
including its page listeners, before rethrowing the original failure.

The rendering helper (`finishRender`,
`packages/ui-react/tests/connection-continuity-runtime.test.ts:362-387`) waits
`packages/ui-react/tests/connection-continuity-runtime.test.ts:363-388`) waits
finite animation completion, fonts and two stable geometry frames. It does **not**
claim arbitrary asynchronous work is finished. The negative-window helper
(`settle`, `packages/ui-react/tests/connection-continuity-runtime.test.ts:396-406`)
(`settle`, `packages/ui-react/tests/connection-continuity-runtime.test.ts:397-407`)
retains the original 500 ms and two animation frames. It does not add a wait for
all finite animations at these negative-observation sites. The finite-animation
helper serves the four original rendering call sites, its readiness control,
Expand Down
10 changes: 9 additions & 1 deletion docs/process/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,15 @@ credentials. A fork run awaiting maintainer approval has not passed.

Hosted unit partitions run up to two serial Bun test processes through
`scripts/unit-workers.ts`. Each owns its source checkout, copied frozen
install, build output and workspace lease. The wrapper preserves the caller's
install, build output and workspace lease. Full preparation now runs after
namespace entry inside the test wrapper, under one continuous build/test lease;
the two workers keep independent invocation identities and serial compilers.
A prepared worker may start tests while its sibling is still building. Account
for that scheduling change separately from duplicate hook builds. Runtime hooks
verify the live invocation and successful closure receipt before reuse; missing
authority rebuilds, and a failed preparation stops before tests without retry.
See [the invocation rule](../decisions/ci-utility.md#one-fresh-build-per-test-invocation).
The wrapper preserves the caller's
umask, drops setup capabilities and binds a private maintenance ledger without
changing within-worker child limits or hiding the host's `/tmp` from Docker.
Its checkout container lives outside host `/tmp`, which native workers replace
Expand Down
4 changes: 2 additions & 2 deletions packages/ui-react/tests/answer-delivery-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
/**
* Answer delivery (#910) in real Chrome, on the public ChatPage, against the
* real host (`createApp`, its WebSocket, SQLite catalog and dispatch) with a
Expand Down Expand Up @@ -82,8 +83,7 @@ beforeAll(async () => {
// Unit shards start without dist. Generate the real package styles (and
// their exported token dependency), as the sibling styled runtime fixtures
// do, instead of depending on another test having built them first.
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [buildOut, buildErr, buildCode] = await hook.child("package-build", build);
const [buildOut, buildErr, buildCode] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], hook);
if (buildCode !== 0) throw new Error(`Answer delivery fixture build failed (${buildCode}): ${buildOut}\n${buildErr}`);
scratch = await mkdtemp(resolve(tmpdir(), "odysseus-answer-delivery-"));
const brain = resolve(scratch, "brain");
Expand Down
4 changes: 2 additions & 2 deletions packages/ui-react/tests/approval-recovery-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases.ts";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { statSync } from "node:fs";
Expand Down Expand Up @@ -35,8 +36,7 @@ const hook = runtimeHookPhases("approval-recovery-runtime");

beforeAll(async () => {
if (!executablePath) return;
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [out, err, code] = await hook.child("package-build", build);
const [out, err, code] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], hook);
if (code !== 0) throw new Error(`Recovery fixture package build failed (${code}): ${out}\n${err}`);
scratch = await mkdtemp(resolve(tmpdir(), "odysseus-approval-recovery-"));
const brain = resolve(scratch, "brain");
Expand Down
7 changes: 4 additions & 3 deletions packages/ui-react/tests/artifact-recovery-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases";
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
import { afterAll, beforeAll, expect, test } from "bun:test";
import { statSync } from "node:fs";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
Expand Down Expand Up @@ -47,9 +49,8 @@ async function child(args: string[]) {
}

beforeAll(async () => {
// A focused source mutation still bundles the client below from its fresh
// source; only already-built, unchanged workspace dependencies may be reused.
if (process.env.BRAIN_ARTIFACT_DEPENDENCIES_BUILT !== "1") await child([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"]);
// The invocation owns fresh package output; client bundling stays fresh below.
await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], runtimeHookPhases("artifact-recovery-runtime.test.ts"));
fixture = await mkdtemp(resolve(tmpdir(), "odysseus-artifact-recovery-"));
brain = resolve(fixture, "brain");
await createFixtureBrain(repo, brain);
Expand Down
6 changes: 2 additions & 4 deletions packages/ui-react/tests/association-signout-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases.ts";
import { afterAll, beforeAll, expect, test } from "bun:test";
import { statSync } from "node:fs";
Expand Down Expand Up @@ -79,11 +80,8 @@ async function stopHost(h: Host | undefined) {
const hook = runtimeHookPhases("association-signout-runtime.test.ts");
beforeAll(() => hook.phase("setup", async () => {
if (!executablePath && !process.env.BRAIN_PINNED_CHROMIUM) return;
if (!process.env.BRAIN_AUTH_FIXTURE_BUILT) {
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [out, err, code] = await hook.child("package-build", build);
const [out, err, code] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], hook);
if (code !== 0) throw new Error(`Local work fixture package build failed (${code}): ${out}\n${err}`);
}
scratch = await mkdtemp(resolve(tmpdir(), "odysseus-association-signout-"));
brain = resolve(scratch, "brain");
await hook.phase("fixture-files", () => createFixtureBrain(repo, brain));
Expand Down
6 changes: 2 additions & 4 deletions packages/ui-react/tests/auth-expiry-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases.ts";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { statSync } from "node:fs";
Expand Down Expand Up @@ -111,11 +112,8 @@ const hook = runtimeHookPhases("auth-expiry-runtime");

beforeAll(async () => {
if (!executablePath) return;
if (!process.env.BRAIN_AUTH_FIXTURE_BUILT) {
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [out, err, code] = await hook.child("package-build", build);
const [out, err, code] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], hook);
if (code !== 0) throw new Error(`Local work fixture package build failed (${code}): ${out}\n${err}`);
}
scratch = await mkdtemp(resolve(tmpdir(), "odysseus-auth-expiry-"));
brain = resolve(scratch, "brain");
await hook.phase("fixture-brain", () => createFixtureBrain(repo, brain));
Expand Down
4 changes: 2 additions & 2 deletions packages/ui-react/tests/chat-focus-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases.ts";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { existsSync, statSync } from "node:fs";
Expand Down Expand Up @@ -33,8 +34,7 @@ beforeAll(async () => {
if (!executablePath) return;
// Browser imports use the published default exports. Rebuild from source
// on every run so a marker mutation cannot be hidden by stale dist output.
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [out, err, code] = await hook.child("package-build", build);
const [out, err, code] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], hook);
if (code !== 0) throw new Error(`Focus fixture package build failed (${code}): ${out}\n${err}`);
scratch = await mkdtemp(resolve(tmpdir(), "odysseus-chat-focus-"));
brain = resolve(scratch, "brain");
Expand Down
5 changes: 3 additions & 2 deletions packages/ui-react/tests/connection-continuity-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases";
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
import { statSync } from "node:fs";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
Expand Down Expand Up @@ -204,8 +206,7 @@ function listen() {

beforeAll(async () => {
if (!executablePath) return;
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [out, err, code] = await Promise.all([new Response(build.stdout).text(), new Response(build.stderr).text(), build.exited]);
const [out, err, code] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], runtimeHookPhases("connection-continuity-runtime.test.ts"));
if (code !== 0) throw new Error(`Connection-continuity fixture package build failed (${code}): ${out}\n${err}`);
scratch = await mkdtemp(resolve(tmpdir(), "odysseus-continuity-"));
const brain = resolve(scratch, "brain");
Expand Down
4 changes: 2 additions & 2 deletions packages/ui-react/tests/draft-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runtimePackageBuild } from "./fixtures/runtime-package-build";
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases.ts";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { statSync } from "node:fs";
Expand Down Expand Up @@ -67,8 +68,7 @@ const HARBOUR = png([30, 90, 160]);
const hook = runtimeHookPhases("draft-runtime.test.ts");
beforeAll(() => hook.phase("setup", async () => {
if (!executablePath) return;
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [out, err, code] = await hook.child("package-build", build);
const [out, err, code] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], hook);
if (code !== 0) throw new Error(`Draft fixture package build failed (${code}): ${out}\n${err}`);
scratch = await mkdtemp(resolve(tmpdir(), "odysseus-drafts-"));
const brain = resolve(scratch, "brain");
Expand Down
8 changes: 4 additions & 4 deletions packages/ui-react/tests/external-speech-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runtimePackageBuild, externalSpeechDelegation } from "./fixtures/runtime-package-build";
import { runtimeHookPhases } from "./fixtures/runtime-hook-phases.ts";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { statSync } from "node:fs";
Expand Down Expand Up @@ -33,8 +34,7 @@ if (worker) {
if (!executablePath) return;
const repo = resolve(import.meta.dir, "../../..");
process.stderr.write("[external-speech] building packages\n");
const build = Bun.spawn([process.execPath, "scripts/build.ts", "--for", "ui-react,ui-server,core"], { cwd: repo, stdout: "pipe", stderr: "pipe" });
const [out, err, code] = await runtimeHookPhases("external-speech-runtime").child("package-build", build, relay);
const [out, err, code] = await runtimePackageBuild(repo, ["ui-react", "ui-server", "core"], runtimeHookPhases("external-speech-runtime"), relay);
if (code !== 0) throw new Error(`Speech fixture package build failed: ${out}\n${err}`);
process.stderr.write("[external-speech] constructing local speech and application servers\n");
speechServer = Bun.serve<{ audio: number }>({ hostname: "127.0.0.1", port: 0,
Expand Down Expand Up @@ -146,9 +146,9 @@ if (worker) describe.skipIf(!executablePath)("external speech through public app
else test.skipIf(!executablePath)("external speech: actual browser audio drains and unmount hard-stops capture in an isolated process", async () => {
// Parent test files may install DOM/transport globals. The real app/build/
// Chrome harness gets a fresh runtime, as the existing media proof does.
const child = Bun.spawn([process.execPath, "run", "test", import.meta.path], {
const child = Bun.spawn([process.execPath, "run", "test", "--internal-external-speech", import.meta.path], {
cwd: resolve(import.meta.dir, "../../.."),
env: { ...process.env, BRAIN_EXTERNAL_SPEECH_WORKER: "1" },
env: { ...externalSpeechDelegation(resolve(import.meta.dir, "../../..")), BRAIN_EXTERNAL_SPEECH_WORKER: "1" },
stdin: "ignore", stdout: "pipe", stderr: "pipe",
});
// Keep stage evidence even if the runner times out before the child exits.
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-react/tests/fixtures/runtime-hook-phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ export function runtimeHookPhases(file: string) {
report(stage, "complete", details);
return result;
}
return { phase, child };
return { phase, child, report };
}
Loading
Loading