fix(ci): support ipset sockets and deployed image smoke checks
quality / changes (push) Successful in 6s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 28s
quality / web (push) Successful in 57s
quality / api (push) Successful in 1m35s
quality / linux-agent-smoke (push) Successful in 24s
quality / docker-check (push) Failing after 3m20s
CD / quality (push) Failing after 6m56s
CD / publish (push) Skipped

This commit is contained in:
Denozordec committed 2026-10-26 14:14:51 +07:00
1 parent 4db34f94f2
commit 57b7652d4d
11 files changed
+206 -15

No files matched your search

+4 -1
View File
@@ -272,6 +272,7 @@ jobs:
pnpm exec turbo run typecheck --filter=@evofw/api --filter=@evofw/db --filter=@evofw/shared
pnpm exec turbo run test --filter=@evofw/api --filter=@evofw/shared
node --test scripts/ci/sqlite-backup.test.mjs
node --test scripts/ci/image-smoke-marker.test.mjs
pnpm exec turbo run build --filter=@evofw/api
node --test scripts/ci/benchmark-fixture.test.mjs
node --test scripts/ci/manifest-platform.test.mjs
@@ -406,4 +407,6 @@ jobs:
BENCH_CONCURRENCY: "10"
BENCH_STEADY_CONCURRENCY: "2"
BENCH_STEADY_INTERVAL_MS: "1200"
run: sh scripts/bench/runtime.sh "$BUILT_IMAGE" "runtime-artifacts/benchmark-${{ github.run_id }}-${{ github.run_attempt }}"
run: |
sh scripts/bench/runtime.test.sh
sh scripts/bench/runtime.sh "$BUILT_IMAGE" "runtime-artifacts/benchmark-${{ github.run_id }}-${{ github.run_attempt }}"
+2 -2
View File
@@ -1,9 +1,9 @@
import { createRequire } from 'node:module'
import { resolve, dirname } from 'node:path'
import { existsSync, mkdirSync, renameSync, rmSync } from 'node:fs'
import { existsSync, mkdirSync, renameSync, rmSync, realpathSync } from 'node:fs'
// Resolve through the production workspace package; pnpm isolates transitive deps.
const require = createRequire(resolve('node_modules/@evofw/db/package.json'))
const require = createRequire(realpathSync(resolve('node_modules/@evofw/db/package.json')))
const Database = require('better-sqlite3')
const [mode, input, output, offline] = process.argv.slice(2)
if (!['backup', 'restore', 'verify'].includes(mode) || !input || (mode !== 'verify' && !output)) {
+7 -1
View File
@@ -8,7 +8,7 @@ Local verification: `node --test scripts/ci/sqlite-backup.test.mjs` passed on Wi
The first pushed `linux-agent-smoke` job failed at `unshare: Operation not permitted`: its Gitea job container did not have the kernel capabilities needed by the nested namespace harness. The workflow now builds `scripts/ci/linux-agent-smoke.Dockerfile` and launches it using `linux-agent-container-smoke.sh`. Prerequisites and the checked-out agent/fixture/harness are copied into the helper image, so a remote Docker daemon needs no runner filesystem bind mounts.
The helper has its own network/PID/mount namespaces, `--network none`, a read-only image filesystem and a bounded temporary filesystem. It drops all capabilities except `SYS_ADMIN` (namespace/mount operations) and `NET_ADMIN` (veth, nftables and ipset inside the test namespaces). It retains Docker's default seccomp policy, enables no-new-privileges and never receives privileged mode, host networking/PIDs, host mounts or a Docker socket. On AppArmor-enabled daemons only this helper uses `apparmor=unconfined`, because Docker's default AppArmor policy denies the required mounts. See Docker's [capability guidance](https://docs.docker.com/engine/containers/run/) and [AppArmor policy guidance](https://docs.docker.com/engine/security/apparmor/).
The helper has its own network/PID/mount namespaces, `--network none`, a read-only image filesystem and a bounded temporary filesystem. It drops all capabilities except `SYS_ADMIN` (namespace/mount operations), `NET_ADMIN` (veth, nftables and ipset inside the test namespaces), and `NET_RAW` (the iptables set extension opens a raw socket while querying ipset). It retains Docker's default seccomp policy, enables no-new-privileges and never receives privileged mode, host networking/PIDs, host mounts or a Docker socket. On AppArmor-enabled daemons only this helper uses `apparmor=unconfined`, because Docker's default AppArmor policy denies the required mounts. See Docker's [capability guidance](https://docs.docker.com/engine/containers/run/) and [AppArmor policy guidance](https://docs.docker.com/engine/security/apparmor/).
The original harness still creates and verifies separate network/mount/PID namespaces before firewall mutations. A preflight namespace probe fails clearly when the daemon/kernel does not support the required isolation; there is no skip, privileged retry or host-firewall fallback. The daemon must support namespaces, IPv6, veth, nftables, iptables and ipset; the helper cannot load missing host kernel modules. Its immutable image ID is used for execution and its container/image are cleaned up while preserving the original test exit code.
@@ -98,3 +98,9 @@ A second fresh Windows in-memory run of the original mixed profile passed all 11
The final fresh run used the separated profiles on Windows Node 25.5 with an in-memory DB and no CPU/RAM quota. All 1611 requests passed with zero rate limits, exact report-ID/sequence and duplicate ACK checks, four raw rows per agent, final per-agent 300 dropped/600 accepted and fleet totals 30000/60000. The 100k import summary remained below 256 bytes. Burst profile: 1000 requests at concurrency 10, new-ingestion p95 372.753ms (300 samples), timeseries p95 782.623ms and cold-policy p95 3451.412ms. Steady profile: 400 requests, two workers paced at 1200ms; new ingestion 100 samples p95 11.0855ms/max 14.4917ms, timeseries 100 samples p95 8.0675ms/max 10.7258ms, duplicate retries 100 samples p95 7.5481ms and summaries 100 samples p95 3.6905ms. This verifies the revised harness and local correctness. It does not prove native 2-vCPU/4GB capacity or establish a performance improvement. The actual constrained CI threshold remains pending execution.
Builder concurrency correction: each quality/publication job uses its own run-ID/attempt builder, with setup-buildx cleanup enabled. The registry cache is durable; local builder state is disposable. The verification script inspects the actual container configuration before use. Jobs never remove a shared builder, so concurrent PRs cannot tear down another build. The old persistent `evofw` builder is no longer selected; remove its stale container/state only during an idle-runner maintenance window after confirming no legacy job is using it.
The subsequent ipset smoke failed with `iptables-restore ... Can't open socket to ipset`. The [iptables set extension source](https://android.googlesource.com/platform/external/iptables/+/8902cb0d42754a35febbc79c077bca770c477bf1/extensions/libxt_set.h) opens an `AF_INET` raw socket in `get_version` before querying the set API. The [Linux raw socket manual](https://man7.org/linux/man-pages/man7/raw.7.html) requires `CAP_NET_RAW` for this operation. The helper therefore adds only `NET_RAW` to its existing capabilities; it keeps its isolated network, default seccomp, and all other restrictions. The wrapper regression checks the exact three-capability set, exit propagation, and cleanup. This source-based fix still requires the next Linux CI smoke to confirm native ipset execution; the Windows test does not execute Docker or kernel commands.
The published upgrade baseline subsequently started successfully and returned `/ready` 200, but the smoke marker failed while resolving `better-sqlite3` through a synthetic `/app/node_modules/@evofw/db/package.json` path. The marker now creates one uniquely named static list with one CIDR through the public API, captures its ID and verifies that same ID/name/entry across upgrade, restart and restoration. Later verification never recreates a missing marker. This API shape was checked against the previous `aa47584` list routes and accepts their HTTP 200 creation response as well as newer HTTP 201 responses. It makes no assumptions about the old image's dependency layout; native SQLite startup and persistence are still exercised by readiness and actual API writes/reads.
The backup utility and benchmark fixture had the same pnpm resolution assumption. Both now resolve the DB package's real filesystem location before constructing its dependency resolver. The backup regression uses an isolated pnpm-shaped symlink/junction tree, removes the former `NODE_PATH` workaround, demonstrates that the synthetic resolver fails and verifies the real fixture loader plus WAL backup/offline restore. That regression passed locally after the required Windows sandbox escalation. The marker regression runs the actual embedded Node program with a fake public API: old/new response codes, retained ID, missing rows and lost entries are covered and passed. CI runs this marker regression beside the backup test, and the benchmark wrapper regression runs before its real image benchmark. These local checks do not establish a successful native upgrade/backup/benchmark; those remain pending CI. The nft namespace backend has already passed native IPv4/IPv6 denial, drift repair, invalid-candidate preservation and unrelated-table preservation; ipset and the full built-image sequence still require the corrected run.
+5 -2
View File
@@ -3,6 +3,10 @@ import { createRequire } from 'node:module'
import { createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { resolve } from 'node:path'
import { realpathSync } from 'node:fs'
export function fixtureDatabaseConstructor() {
return createRequire(realpathSync(resolve('node_modules/@evofw/db/package.json')))('better-sqlite3')
}
export const benchmarkPrefix = (i, first = 10) => `${first}.${i >>> 16}.${(i >>> 8) & 255}.${i & 255}/32`
export function seedFixture(db) {
db.pragma('foreign_keys = ON')
@@ -26,8 +30,7 @@ export function seedFixture(db) {
return { fixture: { agents: 100, prefixes: 50_000 }, integrity: db.pragma('integrity_check', { simple: true }) }
}
if (!process.argv[1] || resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const require = createRequire('/app/node_modules/@evofw/db/package.json')
const Database = require('better-sqlite3')
const Database = fixtureDatabaseConstructor()
const db = new Database('/data/app.db')
try { console.log(JSON.stringify(seedFixture(db))) } finally { db.close() }
}
+21 -3
View File
@@ -7,9 +7,11 @@ out="${2:?OUTPUT_DIR required}"
mkdir -p "$out"
id="evofw-bench-$(date +%s)-$$"
volume="$id-data"
load_id="$id-load"
cleanup() {
docker logs "$id" >"$out/container.log" 2>&1 || true
[ -z "${sampler:-}" ] || { kill "$sampler" 2>/dev/null || true; wait "$sampler" 2>/dev/null || true; }
docker rm -fv "$load_id" >/dev/null 2>&1 || true
docker rm -f "$id" >/dev/null 2>&1 || true
docker volume rm "$volume" >/dev/null 2>&1 || true
}
@@ -20,7 +22,7 @@ docker image inspect "$image" >"$out/image.json"
if docker volume inspect "$volume" >/dev/null 2>&1; then echo 'Refusing reused benchmark volume' >&2; exit 1; fi
docker volume create "$volume" >/dev/null
started="$(date +%s)"
docker run -d --name "$id" --cpus 2 --memory 4g --publish 127.0.0.1::8080 \
docker run -d --name "$id" --cpus 2 --memory 4g --network none \
--mount "type=volume,src=$volume,dst=/data" -e EVOFW_ALLOW_UNSAFE=true -e AUTH_REQUIRED=false "$image" >/dev/null
attempts=0
until docker exec "$id" node -e "fetch('http://127.0.0.1:8080/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" >/dev/null 2>&1; do
@@ -31,8 +33,24 @@ docker exec -i "$id" node --input-type=module <scripts/bench/fixture.mjs >"$out/
# Includes CPU, memory, block IO and PIDs. Metrics are raw observations, no claimed gains.
(while docker inspect -f '{{.State.Running}}' "$id" 2>/dev/null | grep -q true; do docker stats --no-stream --format '{{json .}}' "$id"; sleep 1; done) >"$out/stats.jsonl" &
sampler=$!
port="$(docker port "$id" 8080/tcp | sed 's/.*://')"
BENCH_P95_LIMIT_MS="${BENCH_P95_LIMIT_MS:-500}" BENCH_URL="http://127.0.0.1:$port" node scripts/bench/load.mjs >"$out/load.json" || load_failed=true
# The daemon can be remote or reached from a Gitea job container: runner
# localhost is not the API container. A separate nonroot driver joins only the
# API network namespace, so its CPU work is outside the app's 2-CPU quota.
docker create --name "$load_id" --network "container:$id" \
--user 65534:65534 --cap-drop ALL --security-opt no-new-privileges:true \
--pids-limit 128 --cpus 2 --memory 1g --entrypoint node \
-e BENCH_URL=http://127.0.0.1:8080 \
-e "BENCH_P95_LIMIT_MS=${BENCH_P95_LIMIT_MS:-500}" \
-e "BENCH_CONCURRENCY=${BENCH_CONCURRENCY:-10}" \
-e "BENCH_STEADY_CONCURRENCY=${BENCH_STEADY_CONCURRENCY:-2}" \
-e "BENCH_STEADY_INTERVAL_MS=${BENCH_STEADY_INTERVAL_MS:-1200}" \
"$image" /tmp/load.mjs >/dev/null
# docker cp transfers bytes through the Docker API; no runner bind mount or
# container Docker socket is needed. Both modules are readable by the driver UID.
docker cp scripts/bench/load.mjs "$load_id:/tmp/load.mjs"
docker cp scripts/bench/fixture.mjs "$load_id:/tmp/fixture.mjs"
docker start --attach "$load_id" >"$out/load.json" || load_failed=true
[ "$(docker wait "$load_id")" = 0 ] || load_failed=true
cat "$out/load.json"
docker exec "$id" sh -c 'ls -ln /data/app.db*; cat /proc/1/status; cat /proc/1/io' >"$out/db-process.txt"
sh deploy/docker/backup.sh "$id" "$out/backup.db"
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env sh
# Remote-daemon wiring regression; no Docker daemon, app, or network load.
set -eu
wrapper="$(pwd)/scripts/bench/runtime.sh"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
mkdir "$tmp/bin"
cat >"$tmp/bin/docker" <<'DOCKER'
#!/usr/bin/env sh
set -eu
printf '%s\n' "$*" >>"$FAKE_LOG"
case "$1" in
version|info) printf 'fake remote daemon\n' ;;
image) printf '{}\n' ;;
volume) case "$2" in inspect) exit 1;; *) exit 0;; esac ;;
run|create) printf 'fake-container-id\n' ;;
exec)
case "$*" in
*--input-type=module*) cat >/dev/null; printf '{"fixture":true}\n' ;;
*) exit 0 ;;
esac ;;
inspect) printf 'false\n' ;;
stats) printf '{}\n' ;;
start) printf '{"assertions":{"passed":true}}\n' ;;
wait) printf '%s\n' "$FAKE_LOAD_EXIT" ;;
cp) case "$2" in *:/tmp/*) printf 'fake-db' >"$3";; *) exit 0;; esac ;;
logs|rm) exit 0 ;;
*) echo 'Unexpected Docker operation' >&2; exit 99 ;;
esac
DOCKER
cat >"$tmp/bin/node" <<'NODE'
#!/usr/bin/env sh
echo 'Benchmark must never execute runner-host Node' >&2
exit 97
NODE
chmod +x "$tmp/bin/docker" "$tmp/bin/node"
for expected in 0 23; do
log="$tmp/docker-$expected.log"
if PATH="$tmp/bin:$PATH" FAKE_LOG="$log" FAKE_LOAD_EXIT="$expected" BENCH_P95_LIMIT_MS=321 BENCH_CONCURRENCY=7 BENCH_STEADY_CONCURRENCY=3 BENCH_STEADY_INTERVAL_MS=1350 sh "$wrapper" fixture:immutable "$tmp/out-$expected" >/dev/null; then actual=0; else actual=$?; fi
if [ "$expected" = 0 ]; then [ "$actual" = 0 ]; else [ "$actual" != 0 ]; fi
grep -E '^run -d --name evofw-bench-[^ ]+ --cpus 2 --memory 4g --network none ' "$log" >/dev/null
grep -E '^create --name evofw-bench-[^ ]+-load --network container:evofw-bench-[^ ]+ --user 65534:65534 --cap-drop ALL --security-opt no-new-privileges:true ' "$log" >/dev/null
grep -F -- ' --entrypoint node -e BENCH_URL=http://127.0.0.1:8080 -e BENCH_P95_LIMIT_MS=321 -e BENCH_CONCURRENCY=7 -e BENCH_STEADY_CONCURRENCY=3 -e BENCH_STEADY_INTERVAL_MS=1350 fixture:immutable /tmp/load.mjs' "$log" >/dev/null
grep -E '^cp scripts/bench/load.mjs evofw-bench-[^ ]+-load:/tmp/load.mjs$' "$log" >/dev/null
grep -E '^cp scripts/bench/fixture.mjs evofw-bench-[^ ]+-load:/tmp/fixture.mjs$' "$log" >/dev/null
grep -E '^start --attach evofw-bench-[^ ]+-load$' "$log" >/dev/null
grep -E '^wait evofw-bench-[^ ]+-load$' "$log" >/dev/null
grep -E '^rm -fv evofw-bench-[^ ]+-load$' "$log" >/dev/null
grep -E '^volume rm evofw-bench-[^ ]+-data$' "$log" >/dev/null
if grep -E -- '(--publish|--privileged|--network host|--pid host|type=bind|docker.sock|^port )' "$log" >/dev/null; then echo 'Unsafe or daemon-local benchmark wiring' >&2; exit 1; fi
[ -f "$tmp/out-$expected/backup.db" ]
done
echo 'Runtime benchmark wrapper regression passed (remote daemon, separate driver, failure, cleanup)'
+61
View File
@@ -0,0 +1,61 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { spawnSync } from 'node:child_process'
import { pathToFileURL } from 'node:url'
test('actual image smoke marker uses the old/new public list API and never recreates missing persisted data', () => {
const script = readFileSync(resolve('scripts/ci/image-smoke.sh'), 'utf8')
const program = script.match(/<<'MARKER'\r?\n([\s\S]*?)\r?\nMARKER/)[1]
const directory = mkdtempSync(join(tmpdir(), 'evofw-marker-'))
const marker = join(directory, 'marker.mjs'), mock = join(directory, 'fetch.mjs'), state = join(directory, 'state.json')
writeFileSync(marker, program)
writeFileSync(mock, `
import assert from 'node:assert/strict'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
globalThis.fetch = async (url, options = {}) => {
const state = existsSync(process.env.MARKER_STATE) ? JSON.parse(readFileSync(process.env.MARKER_STATE, 'utf8')) : { creates: 0, entity: null }
let status = 200, body
if (options.method === 'POST') {
assert.equal(url, 'http://127.0.0.1:8080/api/v1/lists')
const payload = JSON.parse(options.body)
state.creates++
state.entity = { id: 'persisted-list-id', name: payload.name, type: payload.type, entries: payload.entries, entry_count: payload.entries.length }
writeFileSync(process.env.MARKER_STATE, JSON.stringify(state))
status = Number(process.env.CREATE_STATUS)
body = { id: state.entity.id, name: state.entity.name, type: state.entity.type }
} else {
assert.equal(url, 'http://127.0.0.1:8080/api/v1/lists/persisted-list-id')
status = state.entity ? 200 : 404
body = state.entity || { error: 'NOT_FOUND' }
}
return { ok: status >= 200 && status < 300, status, json: async () => body }
}
`)
const run = (mode, id, status) => spawnSync(process.execPath, ['--import', pathToFileURL(mock).href, marker, mode, id, 'unique-smoke-marker'], { encoding: 'utf8', env: { ...process.env, MARKER_STATE: state, CREATE_STATUS: String(status) } })
try {
// Previous aa47584 returns 200 for POST /lists; newer implementations may use 201.
for (const status of [200, 201]) {
rmSync(state, { force: true })
const created = run('write', '', status)
assert.equal(created.status, 0, created.stderr)
assert.equal(created.stdout, 'persisted-list-id')
for (const phase of ['read', 'write', 'read', 'read']) {
const verified = run(phase, created.stdout, status)
assert.equal(verified.status, 0, verified.stderr)
assert.equal(verified.stdout, created.stdout)
}
const saved = JSON.parse(readFileSync(state, 'utf8'))
assert.equal(saved.creates, 1, 'Upgrade/restart/restore verification must never create replacement data')
saved.entity.entries = []; saved.entity.entry_count = 0
writeFileSync(state, JSON.stringify(saved))
assert.notEqual(run('read', created.stdout, status).status, 0, 'Lost marker entries must fail smoke')
saved.entity = null
writeFileSync(state, JSON.stringify(saved))
assert.notEqual(run('read', created.stdout, status).status, 0, 'Missing marker must fail smoke')
assert.equal(JSON.parse(readFileSync(state, 'utf8')).creates, 1)
}
} finally { rmSync(directory, { recursive: true, force: true }) }
})
+28 -1
View File
@@ -13,6 +13,8 @@ id="evofw-smoke-$(date +%s)-$$"
volume="$id-data"
restore_volume="$id-restore"
container="$id"
marker_id=''
marker_name="$id-persistence"
tmp="$(mktemp -d)"
cleanup() {
result=$?
@@ -38,7 +40,32 @@ start() {
done
}
marker() {
docker exec "$container" node -e "const D=require('node:module').createRequire('/app/node_modules/@evofw/db/package.json')('better-sqlite3'); const db=new D('/data/app.db'); if(process.argv[1]==='write') db.prepare('INSERT OR REPLACE INTO settings(key,value) VALUES (?,?)').run('ci-smoke','survives'); else if(db.prepare('SELECT value FROM settings WHERE key=?').get('ci-smoke')?.value!=='survives')process.exitCode=1; db.close()" "$1"
value="$(docker exec -i "$container" node --input-type=module - "$1" "$marker_id" "$marker_name" <<'MARKER'
import assert from 'node:assert/strict'
const [mode, existingId, name] = process.argv.slice(2)
assert.ok(mode === 'write' || mode === 'read')
const call = async (path, options = {}) => {
const response = await fetch(`http://127.0.0.1:8080/api/v1${path}`, { ...options, signal: AbortSignal.timeout(10000) })
assert.ok(response.ok, `Persistence marker HTTP ${response.status}`)
return response.json()
}
let id = existingId
if (mode === 'write' && !id) {
const created = await call('/lists', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, type: 'static', entries: ['192.0.2.123/32'] }) })
assert.ok(typeof created.id === 'string' && created.id.length > 0, 'Marker creation did not return an ID')
id = created.id
}
assert.ok(id, 'Persistence marker ID is missing')
const stored = await call(`/lists/${encodeURIComponent(id)}`)
assert.equal(stored.id, id)
assert.equal(stored.name, name)
assert.equal(stored.type, 'static')
assert.deepEqual(stored.entries, ['192.0.2.123/32'], 'Marker entries did not survive')
assert.equal(stored.entry_count, 1)
process.stdout.write(id)
MARKER
)"
if [ "$1" = write ]; then marker_id="$value"; fi
}
stop() {
docker stop -t 30 "$container" >/dev/null
+4 -2
View File
@@ -22,13 +22,15 @@ docker build --file scripts/ci/linux-agent-smoke.Dockerfile --iidfile "$tmp/imag
image="$(cat "$tmp/image-id")"
printf '%s\n' "$image" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo 'Missing immutable helper image ID' >&2; exit 1; }
set -- docker run --rm --name "$name" --network none \
--cap-drop ALL --cap-add SYS_ADMIN --cap-add NET_ADMIN \
--cap-drop ALL --cap-add SYS_ADMIN --cap-add NET_ADMIN --cap-add NET_RAW \
--security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m \
--pids-limit 256 --cpus 2 --memory 512m
# docker-default AppArmor denies the mount operations used by unshare/ip-netns.
# Only this disposable, networkless helper gets the mount-capable exception.
# Default seccomp remains enabled and adapts to the two granted capabilities.
# libxt_set opens an AF_INET/SOCK_RAW/IPPROTO_RAW socket to query ipset;
# NET_RAW permits that query inside the isolated helper network namespace.
# Default seccomp remains enabled and adapts to the granted capabilities.
security="$(docker info --format '{{json .SecurityOptions}}')"
if printf '%s' "$security" | grep -q apparmor; then
set -- "$@" --security-opt apparmor=unconfined
@@ -32,7 +32,9 @@ for expected in 0 23; do
if PATH="$tmp/bin:$PATH" FAKE_LOG="$log" FAKE_APPARMOR="$apparmor" FAKE_RUN_EXIT="$expected" sh "$wrapper"; then actual=0; else actual=$?; fi
[ "$actual" = "$expected" ] || { echo 'Wrapper did not preserve the container exit code' >&2; exit 1; }
grep -F 'build --file scripts/ci/linux-agent-smoke.Dockerfile --iidfile ' "$log" >/dev/null
grep -F ' --network none --cap-drop ALL --cap-add SYS_ADMIN --cap-add NET_ADMIN ' "$log" >/dev/null
grep -F ' --network none --cap-drop ALL --cap-add SYS_ADMIN --cap-add NET_ADMIN --cap-add NET_RAW ' "$log" >/dev/null
capabilities="$(grep '^run ' "$log" | awk '{ for (i=1; i<=NF; i++) if ($i == "--cap-add") print $(i+1) }' | sort | tr '\n' ' ')"
[ "$capabilities" = 'NET_ADMIN NET_RAW SYS_ADMIN ' ] || { echo 'Unexpected helper capability set' >&2; exit 1; }
grep -F ' --security-opt no-new-privileges:true --read-only --tmpfs /tmp:' "$log" >/dev/null
grep -F 'image rm sha256:' "$log" >/dev/null
grep -E '^rm -f evofw-linux-smoke-' "$log" >/dev/null
+18 -2
View File
@@ -1,10 +1,11 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { mkdtempSync, rmSync, writeFileSync, existsSync } from 'node:fs'
import { mkdtempSync, rmSync, writeFileSync, existsSync, mkdirSync, symlinkSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve, join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { fixtureDatabaseConstructor } from '../bench/fixture.mjs'
const root = resolve(import.meta.dirname, '../..')
const Database = createRequire(join(root, 'packages/db/package.json'))('better-sqlite3')
const script = join(root, 'deploy/docker/evofw/sqlite-backup.mjs')
@@ -12,8 +13,23 @@ const script = join(root, 'deploy/docker/evofw/sqlite-backup.mjs')
test('online backup includes committed WAL and restore verifies before offline replacement', () => {
const directory = mkdtempSync(join(tmpdir(), 'evofw-backup-test-'))
let db
const run = (...args) => spawnSync(process.execPath, [script, ...args], { cwd: directory, encoding: 'utf8', env: { ...process.env, NODE_PATH: join(root, 'packages/db/node_modules') } })
try {
// Isolated pnpm-shaped tree: the dependency is a sibling of the real package,
// never a top-level dependency reachable from its logical symlink location.
const store = join(directory, 'virtual-store/node_modules')
const realPackage = join(store, '@evofw/db')
mkdirSync(realPackage, { recursive: true })
writeFileSync(join(realPackage, 'package.json'), '{"name":"@evofw/db"}')
mkdirSync(join(directory, 'node_modules/@evofw'), { recursive: true })
const actualDependency = createRequire(join(root, 'packages/db/package.json')).resolve('better-sqlite3/package.json')
symlinkSync(resolve(actualDependency, '..'), join(store, 'better-sqlite3'), 'junction')
symlinkSync(realPackage, join(directory, 'node_modules/@evofw/db'), 'junction')
const env = { ...process.env }
delete env.NODE_PATH
const run = (...args) => spawnSync(process.execPath, [script, ...args], { cwd: directory, encoding: 'utf8', env })
assert.throws(() => createRequire(join(directory, 'node_modules/@evofw/db/package.json'))('better-sqlite3'), { code: 'MODULE_NOT_FOUND' })
const previousDirectory = process.cwd()
try { process.chdir(directory); assert.equal(fixtureDatabaseConstructor(), Database) } finally { process.chdir(previousDirectory) }
const source = join(directory, 'source.db')
const backup = join(directory, 'backup.db')
const restored = join(directory, 'restored.db')