Skip to content

♻️ Let the smoke tests start the site, on @effectionx/bdd - #1256

Open
taras wants to merge 3 commits into
v4from
tm/smoke-tests-own-server
Open

taras wants to merge 3 commits into
v4from
tm/smoke-tests-own-server

Conversation

@taras

@taras taras commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Motivation

The markdown smoke tests added in #1247 assert against a site that someone else
has already started. The workflow backgrounds deno run -A main.tsx &, polls
with curl until the port answers, and hands the address over in SMOKE_URL:

- name: Serve Website
  run: |
    deno run -A main.tsx &
    until curl --silent --head --fail http://127.0.0.1:8000; do sleep 1; done

- name: Smoke test
  run: deno task smoke
  env:
    SMOKE_URL: http://127.0.0.1:8000

Nothing at the call site says so. deno task smoke on its own produces a wall
of connection errors, the file carries a comment explaining it is deliberately
not named *.test.ts because "there is no server to talk to", and CI leans on a
background process surviving between workflow steps.

Approach

useSite, a resource

serve is split so the part that builds and starts the site is a resource, and
serve is that plus the suspend that keeps the process alive:

export function useSite(options: Options): Operation<ServerInfo> {
  return resource(function* (provide) {
    …
    yield* provide(yield* revolution.start({ port: options.port }));
  });
}

function* serve(options: Options) {
  let server = yield* useSite(options);
  console.log(`www -> ${urlFromServer(server)}`);
  yield* suspend();
}

Teardown needed no new code — revolution's useServer is already a resource
whose finally calls server.shutdown(), so ending the scope is enough. Nor is
there anything to poll for: start() resolves from Deno.serve's onListen, so
holding the ServerInfo means the socket is bound.

The suite takes the site in a beforeAll

@effectionx/bdd is effection-native: it bodies are generators, and
beforeAll takes an operation whose resources are held on the suite's scope —
it runs the setup in a spawned task that then suspends, and the suite's
afterAll halts it. That is exactly the lifetime wanted here, already written
and maintained:

describe("the markdown an agent reads", () => {
  let site: URL;

  beforeAll(function* () {
    let server = yield* useSite(options());
    site = new URL("/", `http://…:${server.port}`);
  });

  it("/AGENTS.md serves the behavioral contract", function* () {
    let [response, body] = yield* get("/AGENTS.md");
    …
  });
});

Nothing bridges a value out of effection, nothing is halted by hand, and there
is nothing to poll for: start() resolves from Deno.serve's onListen, and
revolution closes the server in the resource's finally.

One site for all seven cases rather than one each, which is also forced:
initImageStore can only initialize resvg's wasm once in a process.

(Earlier revisions of this branch reached for createScope, then for run plus
a parked task and a promise to carry the url out. Both were hand-rolled versions
of what beforeAll already does.)

Options, and staying out of the way

The options come from the Configliere command, so the defaults stay declared in
one place and GITHUB_TOKEN / JSR_API are still picked up when present. Four
are overridden:

  • port: 0 — the OS picks, so a dev server on 8000 is left alone
  • clonesDir, worktreesDir, tailwindOutdir under build/smoke/ — the site
    rm -rfs the first two and emptyDirs the third at boot, so sharing build/
    means whichever process starts second pulls the directories out from under the
    other

deno task smoke gains -A: booting the site runs git, shells out to the
Tailwind CLI, and writes build directories, where before it only needed net and
env.

The file keeps its name. www/deno.json has no test exclude, so not being
*.test.ts is the only thing keeping it out of deno task test — and out of the
hundreds of *.test.ts files that appear under build/ once clones exist. The
header comment now gives the real reason: it is slow and needs the network.

The workflow

Both jobs lose SMOKE_URL and gain the two secrets on the smoke step, and the
step moves above Serve Website so the ordering shows it no longer waits on
anything. Serve Website stays — staticalize still crawls it.

Tests

deno task smoke            ok | 1 passed (7 steps) | 0 failed  (4s)

with nothing else running, which is the whole point — that command fails today.

Checks that mattered:

  • The site really does go away with the suite. Not something to take on
    trust: Deno's sanitizers do not watch a Deno.serve listener here, so an
    earlier revision of this branch passed just as happily with its teardown
    deleted. Proved it directly instead, with a second suite that asks the first
    one's port after it has been destroyed:

    DURING its suite: GET -> 200
    AFTER its suite:  refused  <-- torn down with the suite
    

    which also confirms port: 0 is taking effect.

  • The suite can actually fail. t.step resolves a boolean rather than
    throwing, so I checked that a broken assertion still fails the run rather than
    being swallowed:

    /AGENTS.md serves the behavioral contract ... FAILED
    FAILED | 0 passed (6 steps) | 1 failed (1 step)
    error: Test failed
    
  • The directory overrides actually apply — build/smoke/ ends up with its
    own clones/thefrontside/effectionx, worktrees/{v3,effection-v4.1.1,…} and
    tailwind/main.css, so the server really is building its own copies.

  • No collision with a dev server. With deno run -A main.tsx live on 8000,
    deno task smoke passes, and afterwards the dev server still answers 200 on
    /, /blog, /guides/v4/operations and /x/task-buffer and still serves its
    own /tailwind/main.css. Before the directory overrides this is exactly what
    would have broken.

  • The split is invisible to the app. deno run -A main.tsx still logs
    www -> http://localhost:8000/, and deno task staticalize against it still
    produces a full build — 719 pages, 21 assets, canonical links intact.

  • deno fmt --check, deno lint, deno check, and the 10 unit tests pass, and
    deno task test still does not pick the smoke file up.

Note

CI now starts the site twice per job, about +18s, since the smoke tests no longer
share the one staticalize uses. That is the cost of the tests being runnable on
their own.


Also: the http helper is an operation

Third commit. The helper the cases call wrapped a promise chain in until to
turn a response and its body into one operation. @effectionx/fetch is that
already — the request is an operation and so is reading the body:

-  function get(path: string) {
-    return until(
-      fetch(new URL(path, site)).then(
-        async (response): Promise<[Response, string]> => [
-          response,
-          await response.text(),
-        ],
-      ),
-    );
-  }
+  function* get(path: string) {
+    let response = yield* fetch(new URL(path, site));
+
+    return [response, yield* response.text()] as const;
+  }

markdown() takes its FetchResponse, which carries status and headers
the same way. Nothing in the file is async any more.

It is an npm: specifier because the package is not on JSR. Its own dependency
on @effectionx/context-api@0.6 is the version www already has.


Also: the local bdd harness goes

Second commit. www/testing.ts and www/testing/adapter.ts were a copy of
@effectionx/bdd and the adapter underneath it, untouched since a lint sweep in
January. Now that this branch depends on the package anyway, the fork has no
reason to stay:

  • the two files that used it — hooks/use-markdown.test.ts and
    lib/command-parser.test.ts — take describe and it with identical
    signatures, so only the specifier changes
  • @std/testing leaves the import map with the harness that was its only direct
    consumer; the package resolves its own copy

beforeAll is what prompted it: the local fork never had one, and holding a
resource for a whole suite is exactly what these smoke tests need. Keeping a
fork that is missing the hook we want, next to the package that has it, is the
worse of the two options.

199 lines deleted, 2 changed. deno task test is unchanged at 10 passed,
29 steps, and both migrated files still run every case.

Left alone

www/testing/temp-dir.ts, logging.ts and helpers.ts stay. They are test
utilities rather than a bdd harness, so the package does not replace them —
but it is worth saying that nothing imports any of the three, and nothing has
since January. Removing them is a separate call, not this one.

@pkg-pr-new

pkg-pr-new Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/effection@1256

commit: 6fac970

Comment thread www/tests/markdown-routes.ts Dismissed
Comment thread www/tests/markdown-routes.ts Dismissed
@codspeed

codspeed Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing tm/smoke-tests-own-server (6fac970) with v4 (67bb5b4)

Open in CodSpeed

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Deploy Preview Ready!

Comment thread www/tests/markdown-routes.ts Fixed
Comment thread www/tests/markdown-routes.ts Fixed
The markdown smoke tests asserted against a site someone else had
started. The workflow backgrounded `deno run -A main.tsx &`, polled with
curl until the port answered, and handed the address over in SMOKE_URL.
Nothing said so at the call site, `deno task smoke` on its own produced a
wall of connection errors, and CI leaned on a background process
surviving between steps.

They start their own now. `serve` is split so the part that builds and
starts the site is a resource, `useSite`, and `serve` is that plus the
suspend that keeps the process alive.

The suite takes that resource in a `beforeAll` from `@effectionx/bdd`,
which holds it on the suite's scope: the site is up for every case and
goes away with the suite. There is nothing to poll for and nothing to
shut down by hand — `start()` resolves from `Deno.serve`'s `onListen`,
and `revolution` closes the server in the resource's `finally`.

One site for the suite rather than one per case is also forced:
`initImageStore` can only initialize resvg's wasm once in a process.

The options come from the Configliere command, so the defaults stay
declared in one place, with four changed: port 0, so a dev server on 8000
is left alone, and clones, worktrees and tailwind output under
build/smoke, because the site empties all three at boot and would
otherwise pull them out from under a server already running.

`deno task smoke` gains `-A`: booting the site runs git, shells out to
tailwind and writes build directories.

The workflow no longer passes a url, and the smoke step moves above the
one that serves the site to show it no longer waits on it. That step
stays, because staticalize still crawls it.
`www/testing.ts` and `www/testing/adapter.ts` were a copy of
`@effectionx/bdd` and the test adapter underneath it, and had not been
touched since a lint sweep in January. The published package does the
same thing — `describe` and `it` with operation bodies, a scope per
suite — and keeps up with effection.

The two files that used it import `describe` and `it` with the same
signatures, so only the specifier changes. `@std/testing` leaves the
import map with the harness that was its only consumer; the package
resolves its own copy.

`beforeAll` is the reason to move: the local fork never had it, and
holding a resource for a whole suite is what the smoke tests need.
@taras taras changed the title ♻️ Let the smoke tests start the site they talk to ♻️ Let the smoke tests start the site, on @effectionx/bdd Oct 2, 2026
@taras
taras requested a review from cowboyd October 3, 2026 02:16
The helper the cases call wrapped a promise chain in `until` to turn a
response and its body into one operation. `@effectionx/fetch` is that
already — the request is an operation and so is reading the body:

    function* get(path: string) {
      let response = yield* fetch(new URL(path, site));

      return [response, yield* response.text()] as const;
    }

No promise to adapt, and nothing in the file is `async` any more.

This branch was successfully deployed

1 active deployment
Preview — 6fac970d Deployed Oct 3, 2026 by taras via deploy-preview #1422
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants