1 Commits
Author SHA1 Message Date
clawbot b0d1315f7b Check builds keep Go's modules and caches between runs (refs #118)
check / check (push) Canceled after 0s
On sneak's rulings in sneak/prompts#113 and
sneak/prompts#130. REPO_POLICIES.md and
script/test, script/lint, script/docker and script/cibuild are the
sneak/prompts copies at 0653579: the scripts pass a new CACHEBUST value
instead of --no-cache, and the lint and test builds write no image. In
each Dockerfile stage the system packages and the Go module download
come above ARG CACHEBUST; every step that lints, tests or compiles, and
the node stages' yarn install, comes after it. Go's build cache and
golangci-lint's are cache mounts, and both go test runs carry -count=1.
README.md and TODO.md say the same.

Model: opus-5-5
2026-10-19 01:19:54 +00:00
4 changed files with 14 additions and 40 deletions

No files matched your search

+1 -4
View File
@@ -98,10 +98,7 @@ COPY --from=frontend-test /app/yarn.lock /dev/null
FROM frontend-test AS frontend-build FROM frontend-test AS frontend-build
COPY --from=lint /src/go.sum /dev/null COPY --from=lint /src/go.sum /dev/null
COPY --from=test /src/go.sum /dev/null COPY --from=test /src/go.sum /dev/null
# vite.config.js reads the commit for the page's footer with git. git is # vite.config.js reads the commit for the page's footer with git.
# installed again on every image build, although it comes above
# ARG CACHEBUST, because this stage starts from frontend-test, whose
# steps run again on every build.
RUN apk add --no-cache git RUN apk add --no-cache git
ARG CACHEBUST ARG CACHEBUST
RUN yarn build RUN yarn build
+4 -7
View File
@@ -101,13 +101,10 @@ either one is broken. We provide:
new `CACHEBUST` value, so every step in `Dockerfile` that lints, tests or new `CACHEBUST` value, so every step in `Dockerfile` that lints, tests or
compiles runs again even when nothing has changed. Docker keeps the system compiles runs again even when nothing has changed. Docker keeps the system
packages and the Go modules from earlier builds until the base image, the step packages and the Go modules from earlier builds until the base image, the step
that installs them, or `go.mod` and `go.sum` change. The `frontend-build` stage that installs them, or `go.mod` and `go.sum` change. Go's build cache and
is the exception: it installs git again on every image build, because it starts golangci-lint's are kept as well, and each reuses a result only when everything
from the `frontend-test` stage, whose steps run again on every build. Go's build it was made from is unchanged. Both `go test` runs carry `-count=1`, so no test
cache and golangci-lint's are kept as well, and each reuses a result only when result is reused. The yarn packages are installed again on every build.
everything it was made from is unchanged. Both `go test` runs carry `-count=1`,
so no test result is reused. The yarn packages are installed again on every
build.
## Responsive layout ## Responsive layout
-7
View File
@@ -34,13 +34,6 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of
cache and golangci-lint's are cache mounts, and both `go test` runs carry cache and golangci-lint's are cache mounts, and both `go test` runs carry
`-count=1`. The test phase's Go caches used to be in memory, empty on every `-count=1`. The test phase's Go caches used to be in memory, empty on every
build build
- 2026-10-19: the three unit tests of a check whose result is discarded (its
round given up, the user paused, or the first round) also check that the
host's history is still empty, its status still `pending`, and nothing was
written to the console
([#151](https://git.eeqj.de/sneak/netwatch/issues/151)). They checked only the
row, so a change that recorded the result and skipped only the row would have
kept them passing
- 2026-10-18: when the backend runs on its own, a `DATA_DIR` that cannot be - 2026-10-18: when the backend runs on its own, a `DATA_DIR` that cannot be
created or read, or where no file can be created, stops the start with an created or read, or where no file can be created, stops the start with an
error naming `DATA_DIR` and its value, as the other settings' errors do error naming `DATA_DIR` and its value, as the other settings' errors do
+9 -22
View File
@@ -83,11 +83,6 @@ function statusText(state, host) {
return elements[`.status-text[data-host="${index}"]`]?.innerHTML; return elements[`.status-text[data-host="${index}"]`]?.innerHTML;
} }
// What this test wrote to the browser console with console[method].
function consoleLines(method) {
return console[method].mock.calls.map((call) => call.arguments[0]);
}
// Mocks the clock for test t, so that a check lasting seconds takes no real // Mocks the clock for test t, so that a check lasting seconds takes no real
// time, and replaces fetch with targets that each answer after // time, and replaces fetch with targets that each answer after
// answerAfter(url) milliseconds of that clock, or never when that is // answerAfter(url) milliseconds of that clock, or never when that is
@@ -207,10 +202,9 @@ test("at a 30000ms interval, a target answering after 1000ms shows in its row wh
}); });
// In the next three tests, the answering target's check is still waiting // In the next three tests, the answering target's check is still waiting
// when something happens after which its result must not be recorded or // when something happens after which its result must not show.
// show in its row, and the check must write nothing to the console.
test("at a 30000ms interval, a check still waiting when its round is given up is not recorded and does not show in its row", async (t) => { test("at a 30000ms interval, a check still waiting when its round is given up does not show in its row", async (t) => {
CONFIG.updateInterval = 30000; CONFIG.updateInterval = 30000;
const state = new AppState([ const state = new AppState([
{ name: "Answering", url: "https://answering.test" }, { name: "Answering", url: "https://answering.test" },
@@ -226,13 +220,9 @@ test("at a 30000ms interval, a check still waiting when its round is given up is
roundChecks.abort(); roundChecks.abort();
assert.notEqual(await settled(round), "still waiting"); assert.notEqual(await settled(round), "still waiting");
assert.equal(latencyFigure(state, answering), undefined); assert.equal(latencyFigure(state, answering), undefined);
assert.deepEqual(answering.history, []);
assert.equal(answering.status, "pending");
assert.deepEqual(consoleLines("error"), []);
assert.deepEqual(consoleLines("info"), []);
}); });
test("at a 30000ms interval, a check still waiting when the user pauses is not recorded and does not show in its row", async (t) => { test("at a 30000ms interval, a check still waiting when the user pauses does not show in its row", async (t) => {
CONFIG.updateInterval = 30000; CONFIG.updateInterval = 30000;
const state = new AppState([ const state = new AppState([
{ name: "Answering", url: "https://answering.test" }, { name: "Answering", url: "https://answering.test" },
@@ -247,13 +237,9 @@ test("at a 30000ms interval, a check still waiting when the user pauses is not r
t.mock.timers.tick(500); t.mock.timers.tick(500);
assert.equal(await settled(round), "still waiting"); assert.equal(await settled(round), "still waiting");
assert.equal(latencyFigure(state, answering), undefined); assert.equal(latencyFigure(state, answering), undefined);
assert.deepEqual(answering.history, []);
assert.equal(answering.status, "pending");
assert.deepEqual(consoleLines("error"), []);
assert.deepEqual(consoleLines("info"), []);
}); });
test("at a 30000ms interval, a check in the first round is not recorded and does not show in its row", async (t) => { test("at a 30000ms interval, a check in the first round does not show in its row", async (t) => {
CONFIG.updateInterval = 30000; CONFIG.updateInterval = 30000;
const state = new AppState([ const state = new AppState([
{ name: "Answering", url: "https://answering.test" }, { name: "Answering", url: "https://answering.test" },
@@ -265,10 +251,6 @@ test("at a 30000ms interval, a check in the first round is not recorded and does
t.mock.timers.tick(1000); t.mock.timers.tick(1000);
assert.equal(await settled(round), "still waiting"); assert.equal(await settled(round), "still waiting");
assert.equal(latencyFigure(state, answering), undefined); assert.equal(latencyFigure(state, answering), undefined);
assert.deepEqual(answering.history, []);
assert.equal(answering.status, "pending");
assert.deepEqual(consoleLines("error"), []);
assert.deepEqual(consoleLines("info"), []);
}); });
test("at a 30000ms interval, after the user pauses and resumes during a round, no row reads paused once its last check ends", async (t) => { test("at a 30000ms interval, after the user pauses and resumes during a round, no row reads paused once its last check ends", async (t) => {
@@ -318,6 +300,11 @@ function stateWithOneTarget() {
return { state, target: state.local[0] }; return { state, target: state.local[0] };
} }
// What this test wrote to the browser console with console[method].
function consoleLines(method) {
return console[method].mock.calls.map((call) => call.arguments[0]);
}
test("a check that fails with a network error writes one console line with the target, the time, the error and how long the request took", async (t) => { test("a check that fails with a network error writes one console line with the target, the time, the error and how long the request took", async (t) => {
const { state, target } = stateWithOneTarget(); const { state, target } = stateWithOneTarget();
mockTargets(t, () => 23, target.url); mockTargets(t, () => 23, target.url);