How to Test OpenWork Components: A Complete Guide to @openwork/testkit
Test OpenWork components using @openwork/testkit specs located in evals/specs/**/*.test.ts, recording observable assertions on an ambient evidence tape that can be published for CI verification.
OpenWork uses a specialized end-to-end testing framework called @openwork/testkit to verify UI components across Electron, Den, and web dashboard surfaces. The testing workflow isolates component verification in the evals/ workspace, producing durable evidence tapes that serve as PR-verified proof of correct behavior.
Understanding the OpenWork Testing Architecture
The testing system follows a three-stage workflow designed to bridge local development with CI verification. Each spec is a self-contained TypeScript file that declares dependencies, exercises the UI, and records persistent claims.
The Three-Stage Workflow
| Stage | Purpose | Core API |
|---|---|---|
| Author | Write a spec describing the user journey and asserting observable outcomes | import { test, needs, server, app } from "@openwork/testkit" |
| Run | Execute locally or in a Daytona sandbox with auto-provisioned dependencies | pnpm --dir evals exec vitest run --project stack specs/<slug>.slow.test.ts |
| Publish | Convert the recorded tape into a PR-verified evidence bundle | pnpm evals --publish --pr <pr-number> |
Core Testkit APIs
The @openwork/testkit package exports several critical functions for component testing:
test()– Defines a spec that receives anevidenceobject for recording persistent claimsneeds()– Declares external services (MySQL, mock MCP, OAuth); skips the test with a meaningful reason if unavailableserver()– Spins up a local HTTP server serving the app under testapp()– Launches the Electron/Den/Web surface and returns a CDP client for low-level interaction
All assertions that must survive beyond the test run require evidence.fact(). This method writes to the evidence tape that CI verifiers check.
Writing Your First Component Spec
Specs follow a consistent pattern combining Vitest assertions with OpenWork's evidence recording system.
Spec Structure and Pattern
A valid spec imports from @openwork/testkit and uses the standard Arrange-Act-Assert structure:
import { test } from "@openwork/testkit";
test("description of the behavior", async ({ evidence }) => {
// 1️⃣ Arrange – load files, call helpers, etc.
// 2️⃣ Act – invoke the UI, e.g. click a button, fill a form
// 3️⃣ Assert – use Vitest’s `expect` and `evidence.fact` for durable claims
});
Component tests that drive an app surface must use the .slow.test.ts suffix. Files using only .test.ts run as unit-only tests and lack the required sandbox resources.
Recording Evidence with evidence.fact
The evidence.fact() method creates observable claims that appear in the final evidence tape. Without this call, a passing test is marked Incomplete for PR verification purposes.
Consider this real-world example from evals/specs/welcome-one-field.slow.test.ts, which tests the welcome screen's input parsing:
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { expect } from "vitest";
import { test } from "@openwork/testkit";
import {
parseInviteLinkInput,
parseServerUrlInput,
} from "../../apps/app/src/react-app/domains/cloud/join-organization-input";
const welcomePagePath = fileURLToPath(
new URL("../../apps/app/src/react-app/domains/onboarding/welcome-page.tsx", import.meta.url)
);
const dialogPath = fileURLToPath(
new URL("../../apps/app/src/react-app/domains/cloud/join-organization-dialog.tsx", import.meta.url)
);
const enLocalePath = fileURLToPath(
new URL("../../apps/app/src/i18n/locales/en.ts", import.meta.url)
);
test("the welcome screen folds the server‑URL door into the join‑organization field", async ({ evidence }) => {
const welcomeSource = readFileSync(welcomePagePath, "utf8");
const dialogSource = readFileSync(dialogPath, "utf8");
const enLocale = readFileSync(enLocalePath, "utf8");
// Parsing helpers validation
const invite = parseInviteLinkInput("https://den.acme.test/join-org?invite=inv_123");
expect(invite).toEqual({
url: "https://den.acme.test/join-org?invite=inv_123",
origin: "https://den.acme.test",
host: "den.acme.test",
});
expect(parseInviteLinkInput("https://den.acme.test/install?token=abc")).toBeNull();
expect(parseServerUrlInput("https://openwork.acme.test/")).toEqual({
url: "https://openwork.acme.test",
host: "openwork.acme.test",
});
// UI source checks
expect(welcomeSource).not.toContain("OrganizationServerAffordance");
expect(welcomeSource).toContain('data-testid="welcome-join-org"');
expect(enLocale).toContain('"welcome.join_org_subtitle": "Paste your invite link, install link, or server URL"');
expect(dialogSource).toContain("if (await submitInstallLink(trimmedInput)) return;");
expect(dialogSource).toContain("if (await submitInviteLink(trimmedInput)) return;");
expect(dialogSource).toContain("if (await submitServerUrl(trimmedInput)) return;");
// Persistent claim for evidence tape
evidence.fact(
"The welcome screen has one paste field for invite links, install links, server URLs, and sign‑in codes",
"The separate Using OpenWork on‑premises affordance is gone from Welcome; the join dialog classifies install link, then invite link, then server URL, then sign‑in code, and requires explicit host confirmation before opening web invites.",
true,
);
});
Source: welcome-one-field.test.ts
This test validates both the parsing logic in apps/app/src/react-app/domains/cloud/join-organization-input and the UI structure in apps/app/src/react-app/domains/onboarding/welcome-page.tsx.
Running Tests Locally and in CI
The evals/ workspace operates independently from the main product installation, requiring separate dependency management.
Local Execution
First, install the isolated workspace dependencies:
pnpm --dir evals install
Execute a single spec using Vitest with the stack project configuration:
OPENWORK_EVAL_APP_SPECS=1 \
pnpm --dir evals exec vitest run --project stack specs/welcome-one-field.slow.test.ts
The OPENWORK_EVAL_APP_SPECS=1 environment flag is mandatory for tests that drive the App surface. Without it, the test runner lacks the infrastructure to launch Electron or web contexts.
Daytona Sandbox Testing
For CI-compatible execution or isolated environments, run tests inside a Daytona sandbox:
OPENWORK_EVAL_DAYTONA=1 \
OPENWORK_EVAL_APP_SPECS=1 \
pnpm --dir evals exec vitest run --project stack specs/<slug>.slow.test.ts
Setting OPENWORK_EVAL_DAYTONA=1 automatically provisions declared dependencies via the needs() API within the sandbox environment.
Publishing Evidence Tapes
Once a spec passes, publish its evidence tape to attach verifiable proof to a pull request:
pnpm evals --publish --pr 1234 [--roll <dir|name>]
Only tapes containing observable claims via evidence.fact() can achieve Passed status. Tests using only Vitest's expect without recording facts are marked Incomplete and cannot satisfy PR verification requirements.
The CLI also supports bulk execution (pnpm evals <spec-names…>) and vision-enabled validation (--with-llm-vision) for AI-assisted assertions.
Common Pitfalls to Avoid
When you test OpenWork components, avoid these frequent mistakes:
- Missing
evidence.fact()– The test may pass locally but will be treated as Incomplete in CI because no durable evidence exists for verification. - Undeclared dependencies – Always use
needs()for external services like MySQL or OAuth; otherwise, the spec fails with nondeterministic errors when those services are unavailable. - Incorrect file suffix – App-driving specs must use
.slow.test.ts. Using.test.tscauses the test to run as a unit test without the required sandbox resources, leading to failures when callingapp()orserver().
Summary
- Test OpenWork components using the
@openwork/testkitframework in the isolatedevals/workspace - Write specs in
evals/specs/**/*.slow.test.tsusing thetest()function andevidence.fact()for persistent claims - Import testing utilities from
@openwork/testkit:test,needs,server, andapp - Run locally with
OPENWORK_EVAL_APP_SPECS=1and the Vitest stack project - Publish evidence tapes using
pnpm evals --publish --pr <number>for CI verification - Consult
evals/README.mdandevals/specs/welcome-one-field.slow.test.tsfor implementation reference
Frequently Asked Questions
What is the difference between .test.ts and .slow.test.ts files in OpenWork?
Files ending in .slow.test.ts are recognized by the test runner as end-to-end specs that require sandbox resources like Electron instances or HTTP servers. Files using only .test.ts run as fast unit tests without access to app(), server(), or the full environment provisioning system. Always use .slow.test.ts when testing components that interact with the UI surface.
How does evidence.fact() differ from Vitest's expect()?
expect() assertions verify behavior during the test run but disappear once the process exits. evidence.fact() records persistent claims to an evidence tape that survives the test execution. According to the OpenWork source code, only specs containing evidence.fact() calls can be marked Passed for PR verification; tests using only expect() are classified as Incomplete regardless of whether they pass locally.
Can I run OpenWork component tests without installing the main application?
Yes. The evals/ workspace is intentionally isolated from the main product install. You only need to run pnpm --dir evals install to set up the testing environment. The testkit handles provisioning dependencies declared via needs(), allowing you to test OpenWork components without configuring the full production stack locally.
What happens if I forget to declare a dependency with needs()?
If your test requires an external service (MySQL, mock MCP server, OAuth provider) but you don't declare it with needs(), the test will likely fail with a nondeterministic error when that service is unavailable. The needs() API ensures the test is skipped with a meaningful reason rather than failing, making the test suite more robust across different environments.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →