Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7cbb12c
test: daemon-starting suites refuse on a developer machine; make veri…
atlas-from-plumb Oct 11, 2026
247b3e9
test: verify-full-container supervises the daemon peak and fails clos…
atlas-from-plumb Oct 11, 2026
5348340
test: verify-full-container calls the container gone only on the engi…
atlas-from-plumb Oct 11, 2026
9820422
test: verify-full-container trusts the daemon peak only from a watche…
atlas-from-plumb Oct 11, 2026
90cfcd7
test: verify-full-container's watcher stops on a failed checkpoint wr…
atlas-from-plumb Oct 11, 2026
1a703c5
fix: verify-full-container streams no macOS AppleDouble or xattr entr…
atlas-from-plumb Oct 11, 2026
617dab3
test: verify-full-container does not count a daemon's forked pre-exec…
atlas-from-plumb Oct 12, 2026
e99f219
test: verify-full-container tells a forked, not-yet-exec'd lookalike …
atlas-from-plumb Oct 12, 2026
bad2901
fix: verify-full-container keeps history and tags, uses a git with me…
atlas-from-plumb Oct 12, 2026
e4708b8
docs(changelog): one Changed section under 0.24.1; describe verify-fu…
atlas-from-plumb Oct 12, 2026
27cb693
test: verify-full-container traces the daemons when more than one run…
atlas-from-plumb Oct 12, 2026
369d5b1
test: verify-full-container's census keeps a vanished process's read …
atlas-from-plumb Oct 12, 2026
98165f6
test: verify-full-container's census rejects a stat line without its …
atlas-from-plumb Oct 12, 2026
7d81642
test: verify-full-container runs every test rather than replaying cac…
atlas-from-plumb Oct 12, 2026
4a939bb
test: fold the independent review's should-fixes (PLAN-526)
atlas-from-plumb Oct 12, 2026
56a80d5
test: verify-full-container's signal trap kills the container and sti…
atlas-from-plumb Oct 12, 2026
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
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ See CONTRIBUTING.md for the full guide.

## Checklist

- [ ] `make verify` is green; `make verify-full` if the change touches `//go:build integration` code
- [ ] `make verify` is green; `make verify-full-container` if the change touches `//go:build integration` code
- [ ] `CHANGELOG.md` entry added
- [ ] Prose is in Australian English (-ise/-isation, behaviour, …); spec identifiers left canonical
- [ ] New/changed behaviour is covered by tests (integration tests gated with `//go:build integration`)
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ Detailed rules and exceptions live in [`docs/contributing.md`](docs/contributing
- Keep tests beside code and table-driven where appropriate. `internal/lsp`, `internal/cache`, and `internal/tools` require meaningful coverage; write-tool tests use `WriteDeps{}` and session-isolation tests stay with the package they protect.
- Internal tool-to-tool `Execute` calls use canonical parameter names because aliases resolve only at the MCP dispatch boundary.
- Gate tests needing external binaries with `//go:build integration`; do not chase TUI coverage.
- Use `topology_affected` during the edit loop. Before delivery run `make verify-full`; run `make lint-cross` after platform-constrained or linter-config changes. `make cover` and `make vuln` remain separate CI/on-demand gates.
- Use `topology_affected` during the edit loop. Before delivery run `make verify-full-container` (the full gate in Docker: its daemon-starting suites refuse on a developer machine); run `make lint-cross` after platform-constrained or linter-config changes. `make cover` and `make vuln` remain separate CI/on-demand gates.
- `make test` puts `t.TempDir()` under repository `.testcache`; tests must not assume a system-temp ancestry.

## Delivery and risk

- `make install-hooks` is required after cloning; `make verify-full` (verify + the integration suite) defines ready to commit. `make help` and [`docs/contributing.md`](docs/contributing.md) own the full command matrix.
- `make install-hooks` is required after cloning; `make verify-full` (verify + the integration suite), run via `make verify-full-container` or CI, defines ready to commit. `make help` and [`docs/contributing.md`](docs/contributing.md) own the full command matrix.
- Version resolution is exact tag → `VERSION` → short commit. After rebuilding, run `plumb restart`; source changes do not activate in an old daemon.
- Use conventional commit types (`feat`, `fix`, `refactor`, `test`, `docs`, `ci`, `chore`). Keep changes bisectable and add a `CHANGELOG.md` entry for each discrete change.
- Treat concurrency, the rate limiter, read tracking, and the stats schema as high-risk invariants; inspect their tests and architecture before editing.
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,22 @@
`workspace_sessions`, and `plumb mail`. A live session in another checkout of
the project also counts as a peer for the wake watcher's longer window. No
read path runs git.
- **The daemon-starting test suites no longer run on a developer machine.**
`cmd/smoke` (the `integration` tag) and `cmd/clientsmoke` (the `clients`,
`clients_e2e` and `clients_conformance` tags) start plumb daemons of their
own, and a developer machine should run exactly one: the daemon its sessions
use. Their `TestMain` now refuses, exit status 2, before building or starting
anything, unless it finds `GITHUB_ACTIONS=true`, a Linux container marker
(`/.dockerenv`, `/run/.containerenv`), or `PLUMB_SEPARATE_ENV=vm` on Linux
(`internal/separateenv`). This guards against running them by accident; it is
not a security boundary. CI is unaffected. The new
`make verify-full-container` runs `make verify-full` in a Linux container
with pinned Go, golangci-lint and language servers (and Debian trixie's git),
as a non-root user,
on the repository's history with the working tree laid over it. It fails if
two plumb daemons run at once during the suite or one is still running after
it. It replaces a host `make verify-full` as the pre-submit gate.
`make docker-integration` now runs with `--init`.

### Fixed

Expand Down
33 changes: 22 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,24 @@ would have fixed.
## The definition of "ready to commit"

```sh
make verify-full # build + test + lint + the integration suite — run before every push
make verify-full-container # the full gate, in Docker — run before every push
```

`make verify-full` is the full gate: the same checks as `make verify` plus
`make integration-test`, with the unit tests run once rather than twice.
`make verify` alone is the fast gate CI runs on every pull request; it COMPILES
the `//go:build integration` suite without running it, printing that on success,
and a failing integration test does not turn it red. `make integration-test`
SKIPS tests whose language server is absent, so install gopls and pyright if you
want the whole suite to execute. Run `make verify-full` before submitting, and
`make lint-cross` after platform-constrained or linter-config changes. Other
useful targets: `make test`, `make test-race`, `make lint`, `make tidy`.
`make integration-test`, with the unit tests run once rather than twice. Its
`cmd/smoke` suite starts plumb daemons of its own, and a developer machine must
run exactly one plumb daemon (the one its sessions use), so that suite refuses
to run outside CI, a container or a declared VM. `make verify-full-container`
runs the whole gate in a Linux container with pinned tools, streams in your
working tree, and fails if a plumb daemon outlives it.
`make verify` alone is the fast gate CI runs on every pull request, and the one
to run on your machine; it COMPILES the `//go:build integration` suite without
running it, printing that on success, and a failing integration test does not
turn it red. `make integration-test` SKIPS tests whose language server is
absent; the container has gopls, pyright and the npm language servers. Run
`make verify-full-container` before submitting, and `make lint-cross` after
platform-constrained or linter-config changes. Other useful targets: `make test`,
`make test-race`, `make lint`, `make tidy`.

**Formatting note:** apply formatting via `golangci-lint run --fix ./...`, never the
standalone `gofumpt -w` binary — the two can pin different versions and produce phantom
Expand Down Expand Up @@ -101,6 +107,11 @@ real-client tier also gates releases and runs nightly.
make install-clients # or: ./scripts/install-clients.sh
```

These tiers start plumb daemons of their own, so like `cmd/smoke` they refuse on a
developer machine: run them in CI (the client-conformance workflow), in a container you
have installed the client CLIs into (the `make verify-full-container` image has none), or
in a disposable VM with `PLUMB_SEPARATE_ENV=vm`.

Prerequisites: Node 20+ (npm clients), Python 3.8+ (hermes), `curl` (script-installed
clients). Some installers drop binaries in `~/.local/bin` or `~/.npm-global/bin` — add
those to `PATH` if a binary isn't found.
Expand Down Expand Up @@ -182,15 +193,15 @@ From a fork, the flow is:
```sh
git checkout -b my-change # branch off an up-to-date main
# … make your change, then:
make verify-full # must be green
make verify-full-container # must be green
git push origin my-change # push to YOUR fork
```

Then open a pull request from your fork's branch against `plumbkit/plumb`'s `main`. To
refresh a long-running branch, `git fetch upstream && git rebase upstream/main`.

- Keep PRs focused and reviewable.
- Ensure `make verify-full` is green and a `CHANGELOG.md` entry is included.
- Ensure `make verify-full-container` is green and a `CHANGELOG.md` entry is included.
- Fill out the PR template — it asks the questions that speed up review.

**What to expect on `main`.** `main` is a protected branch: every PR must pass the full
Expand Down
21 changes: 15 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ UNAME_S := $(shell uname -s)
CODESIGN_ID := $(if $(CODESIGN_IDENTITY),$(CODESIGN_IDENTITY),-)
CODESIGN_BUNDLE := com.plumbkit.plumb

.PHONY: build tool-sizes web-ui web-ui-audit test test-race integration-test fuzz build-integration lint lint-tags lint-cross check-size check-brief check-changelog check-site-claims check-verify-disclosure check-changelog-placement check-changelog-placement-test check-pre-commit cover cover-report vuln tidy-check verify verify-full run clean tidy install install-hooks hooks codesign ts-wasm swift-wasm install-clients clients-test clients-test-auth clients-test-conformance build-clients docker-integration docker-cleanroom site blog demo-gif
.PHONY: build tool-sizes web-ui web-ui-audit test test-race integration-test fuzz build-integration lint lint-tags lint-cross check-size check-brief check-changelog check-site-claims check-verify-disclosure check-changelog-placement check-changelog-placement-test check-pre-commit cover cover-report vuln tidy-check verify verify-full run clean tidy install install-hooks hooks codesign ts-wasm swift-wasm install-clients clients-test clients-test-auth clients-test-conformance build-clients docker-integration docker-cleanroom verify-full-container site blog demo-gif

$(TESTCACHE):
mkdir -p $(TESTCACHE)
Expand Down Expand Up @@ -190,12 +190,21 @@ DOCKER_PLATFORM_FLAG := $(if $(DOCKER_PLATFORM),--platform $(DOCKER_PLATFORM),)
# working tree; named volumes cache the Go build + module caches across reruns.
docker-integration:
docker build $(DOCKER_PLATFORM_FLAG) -f build/docker/integration.Dockerfile -t plumb-integration build/docker
docker run --rm $(DOCKER_PLATFORM_FLAG) \
docker run --rm --init $(DOCKER_PLATFORM_FLAG) \
-v "$(CURDIR)":/src \
-v plumb-gocache:/root/.cache/go-build \
-v plumb-gomod:/go/pkg/mod \
plumb-integration

# verify-full-container runs `make verify-full` in a Linux container, the local
# way to run the whole gate. The gate's daemon-starting suites (cmd/smoke,
# cmd/clientsmoke) refuse on a developer machine, which must run exactly one
# plumb daemon; they run only in CI, a container or a declared VM
# (internal/separateenv). The working tree streams in as git sees it; no plumb
# daemon may outlive the suite. See scripts/verify-full-container.sh.
verify-full-container:
DOCKER_PLATFORM=$(DOCKER_PLATFORM) ./scripts/verify-full-container.sh

# docker-cleanroom proves a fresh Debian with NO toolchain can install and run
# plumb end-to-end: a multi-stage build compiles the binary, then a slim runtime
# (bash + python3 only) drives the two-agents MCP demo. The demo's exit code is
Expand Down Expand Up @@ -434,9 +443,9 @@ verify: build test $(VERIFY_CHECKS)
@printf '\n%s\n%s\n%s\n%s\n%s\n\n' \
'verify: PASSED — but the //go:build integration suite was COMPILED, not RUN.' \
' A failing integration test does not fail this target.' \
' Run it before submitting: make integration-test' \
' (skips tests whose language server is absent; install gopls and pyright)' \
' Whole gate in one command: make verify-full'
' Its cmd/smoke suite (make integration-test) starts plumb daemons, so it runs' \
' in CI or a container, never beside the plumb daemon you are using.' \
' Whole gate, in Docker: make verify-full-container'

# verify-full is verify's gates plus the integration suite that verify only
# compiles. It runs the suite once, without verify's separate `test` step, because
Expand All @@ -454,4 +463,4 @@ install-hooks:
mkdir -p "$$hooks"; \
cp scripts/pre-commit "$$hooks/pre-commit"; \
chmod +x "$$hooks/pre-commit"; \
echo "Pre-commit hook installed at $$hooks/pre-commit. Run 'make verify' before every push — and 'make verify-full' when the change touches integration-tagged code."
echo "Pre-commit hook installed at $$hooks/pre-commit. Run 'make verify' before every push — and 'make verify-full-container' when the change touches integration-tagged code."
48 changes: 48 additions & 0 deletions build/docker/verify.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# verify.Dockerfile — the toolchain for `make verify-full-container`.
#
# `make verify-full` runs suites that start plumb daemons of their own
# (cmd/smoke, cmd/clientsmoke), and a developer machine must run exactly one
# plumb daemon, so the full gate runs here instead (or in CI). The image holds
# only tools, never the source: scripts/verify-full-container.sh streams the
# working tree in at run time.
#
# Every tool is pinned, so a green run means the same thing tomorrow. Keep Go in
# step with go.mod's toolchain line, golangci-lint with GOLANGCI_LINT_VERSION in
# .github/workflows/ci.yml, and the language servers with the integration job.
# trixie, not bookworm: its git (2.47) has `merge-tree -X`, which the git tool's
# tests exercise; bookworm's 2.39 does not.
FROM golang:1.27.2-trixie

# Node only to install the npm-published language servers.
COPY --from=node:22.23.3-bookworm /usr/local/bin/node /usr/local/bin/node
COPY --from=node:22.23.3-bookworm /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx

ARG GOPLS_VERSION=v0.23.0
ARG GOLANGCI_LINT_VERSION=v2.14.0
RUN go install "golang.org/x/tools/gopls@${GOPLS_VERSION}" \
&& go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}" \
&& go clean -cache -modcache

# typescript@5: plain `typescript` resolves to 7, which ships no tsserver (see
# the integration job in ci.yml).
RUN npm install -g --no-audit --no-fund \
pyright@1.1.414 typescript-language-server@6.0.2 typescript@5.9.3 vscode-langservers-extracted@4.10.0

ENV PATH="/go/bin:${PATH}"

# The suite runs as an ordinary user: tests that make a directory unwritable
# with chmod prove nothing as root, who can write it anyway. Its caches live in
# its home, created here so the named cache volumes mounted over them start out
# owned by it.
RUN useradd --create-home --uid 1000 verify \
&& mkdir -p /work /home/verify/.cache/go-build /home/verify/.cache/golangci-lint /home/verify/go/pkg/mod \
&& chown -R verify:verify /work /home/verify
USER verify
ENV HOME=/home/verify GOPATH=/home/verify/go GOCACHE=/home/verify/.cache/go-build \
GOMODCACHE=/home/verify/go/pkg/mod GOLANGCI_LINT_CACHE=/home/verify/.cache/golangci-lint

# Fail the image, not the run, when a tool is broken.
RUN gopls version && golangci-lint --version && pyright --version \
&& typescript-language-server --version && command -v vscode-html-language-server
4 changes: 4 additions & 0 deletions cmd/clientsmoke/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (
"testing"
"time"

"github.com/plumbkit/plumb/internal/separateenv"
"github.com/plumbkit/plumb/internal/sqlitex"
)

Expand All @@ -65,6 +66,9 @@ var plumbBin string
var realHome string

func TestMain(m *testing.M) {
// The clients start plumb daemons of their own: refuse on a developer
// machine before anything is built or created (internal/separateenv).
separateenv.RequireForTestMain("cmd/clientsmoke")
realHome = os.Getenv("HOME")
root, err := findRepoRoot()
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions cmd/smoke/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build integration

package smoke_test

import (
"os"
"testing"

"github.com/plumbkit/plumb/internal/separateenv"
)

// TestMain refuses on a developer machine before any test builds plumb or
// starts a `plumb serve`, whose daemon would sit beside the developer's own:
// the suite runs in CI or `make verify-full-container` (internal/separateenv).
func TestMain(m *testing.M) {
separateenv.RequireForTestMain("cmd/smoke")
os.Exit(m.Run())
}
5 changes: 3 additions & 2 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ both behaviours against a stub linter.
| `make lint` | `golangci-lint run` via `scripts/lint-with-retry.sh` — retries with bounded backoff on the shared-cache lock ("parallel golangci-lint is running"), so a peer agent's lint does not read as a failure of this one. |
| `make lint-tags` | Lint and format-check the files behind custom build tags (`integration`, `clients*`, `parity`, `race`), which `make lint` never analyses. `scripts/build-tags.sh` reads the tags from the `//go:build` lines of every Go file that is not ignored, so a new tag is covered without a config change. Part of `make verify`. It lints this OS's files only; `make lint-cross` covers the other's. |
| `make verify` | Build, test, lint (including `make lint-tags`), compile integration/client binaries, check file/brief/changelog limits, and verify `go.mod` tidiness — the fast gate CI runs on every pull request. It does not RUN the integration suite, and prints that on success. |
| `make verify-full` | The same checks as `make verify` plus `make integration-test` — the full **"ready to commit"** gate, with the unit tests run once. `integration-test` SKIPS tests whose language server is absent, so install gopls and pyright to run all of it. |
| `make verify-full` | The same checks as `make verify` plus `make integration-test` — the full **"ready to commit"** gate, with the unit tests run once. Its `cmd/smoke` suite starts plumb daemons of its own and refuses on a developer machine (`internal/separateenv`), so run it in CI or through `make verify-full-container`. `integration-test` SKIPS tests whose language server is absent. |
| `make verify-full-container` | `make verify-full` in a Linux container with pinned Go, golangci-lint and language servers: streams in the working tree as git sees it, runs with `--init` and `--rm`, and fails if a plumb daemon is left after the suite. The local way to run the full gate. |
| `make lint-cross` | Statically lint and vet the other supported OS, its tagged files included; required after platform-constrained or linter-config changes. |
| `make cover` | Enforce the whole-tree statement floor from `scripts/check-coverage.sh`. |
| `make vuln` | Run `govulncheck`; requires network access. |
| `make tidy` | `go mod tidy` |
| `make clean` | Remove build artefacts. |
| `make install-clients` | Install every supported client CLI, for local client-integration testing. |
| `make clients-test` / `make clients-test-auth` | On-demand connection/auth tiers that drive each installed client CLI headless (own build tags, never part of `make verify`) — see the comments above these targets in the `Makefile`. |
| `make clients-test` / `make clients-test-auth` | On-demand connection/auth tiers that drive each installed client CLI headless (own build tags, never part of `make verify`) — see the comments above these targets in the `Makefile`. Like `cmd/smoke` they start plumb daemons, so they refuse outside CI, a container or a declared VM. |

> **Formatting:** always format via `golangci-lint run --fix ./...` (the fix the
> hook names when it fails), not a standalone `gofumpt` binary — the two can pin
Expand Down
1 change: 1 addition & 0 deletions internal/arch/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ var Layers = map[string]Layer{
"internal/textfmt": LayerFoundation, // stdlib-only text primitives, no lipgloss
"internal/toolerror": LayerFoundation, // stdlib-only structured tool-error contract
"internal/redact": LayerFoundation,
"internal/separateenv": LayerFoundation, // where a test suite may start its own plumb daemon
"internal/theme": LayerFoundation, // UI-agnostic hex palettes, no lipgloss
"internal/render": LayerFoundation, // pure presentation helpers
"internal/quality": LayerFoundation, // post-write analyser interface
Expand Down
Loading
Loading