quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
64 lines
6.6 KiB
TypeScript
64 lines
6.6 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
const source = readFileSync(new URL('../agent-scripts/evofw-firewall.sh', import.meta.url), 'utf8')
|
|
const python = process.platform === 'win32' ? 'python' : 'python3'
|
|
const bash = process.platform === 'win32' ? 'C:/Program Files/Git/bin/bash.exe' : 'bash'
|
|
const pythonPath = spawnSync(python, ['-c', 'import sys;print(sys.executable)'], { encoding: 'utf8' }).stdout.trim().replaceAll('\\', '/')
|
|
const dirs: string[] = []
|
|
function workspace() { const d = mkdtempSync(join(tmpdir(), 'evofw-cache-')); dirs.push(d); return d }
|
|
afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) })
|
|
function embedded(name: string) { const s = source.slice(source.indexOf(`${name}() {`)); const a = s.indexOf("<<'PY'") + 7; return s.slice(a, s.indexOf('\nPY', a)).trimStart() }
|
|
function worker(d: string, op: string, ...args: string[]) { return spawnSync(python, ['-c', embedded('telemetry_worker'), d, d, op, ...args], { encoding: 'utf8', env: { ...process.env, EVOFW_TEST_BOOT_ID: 'cache-boot' } }) }
|
|
const policy = (letter = 'a', generation = 1) => ({ hash: 'sha256:' + letter.repeat(64), generation, default_action: 'accept', deny_cidrs: [], allow_cidrs: [], port_rules: [] })
|
|
function cache(d: string) { const body = JSON.stringify(policy()); writeFileSync(join(d, 'candidate.json'), body); writeFileSync(join(d, 'normalized.json'), body); writeFileSync(join(d, 'policy-http-status'), '200'); writeFileSync(join(d, 'policy-headers.txt'), 'HTTP/1.1 200 OK\r\nETag: "etag-first"\r\n'); expect(worker(d, 'init').status).toBe(0); expect(worker(d, 'commit-policy').status).toBe(0); writeFileSync(join(d, 'last_hash'), policy().hash); writeFileSync(join(d, 'kernel_state'), 'nft old-digest\n') }
|
|
const workerFunctions = source.slice(source.indexOf('telemetry_worker() {'), source.indexOf('telemetry_worker init'))
|
|
const requestFunction = source.slice(source.indexOf('curl_policy() {'), source.indexOf('# Do not use `if ! cmd;'))
|
|
const parseFunction = source.slice(source.indexOf('parse_policy() {'), source.indexOf('HASH="";'))
|
|
function shell(d: string, content: string) { writeFileSync(join(d, 'test.sh'), `set -euo pipefail\nSTATE_DIR=.; RUN_DIR=.; HASH_FILE=last_hash; POLICY_FILE=last_policy.json; EVOFW_CP_URL=https://unused.invalid; CLIENT_TOKEN=fixture; BACKEND=nft; DENY=(); ALLOW=(); APPLY_ERROR_CODE=apply_failed;\npython3() { '${pythonPath}' "$@"; }\nlog() { :; }\n${workerFunctions}\n${requestFunction}\n${parseFunction}\n${content}`); return spawnSync(bash, ['--noprofile', '--norc', join(d, 'test.sh').replaceAll('\\', '/')], { cwd: d, encoding: 'utf8', timeout: 15000 }) }
|
|
function curl(status: number) { return `curl() { printf '%s\\n' "$*" > curl-args; local dest= headers=; while (($#)); do case "$1" in -o) dest=$2; shift 2;; -D) headers=$2; shift 2;; *) shift;; esac; done; printf 'HTTP/1.1 ${status} result\\r\\nETag: "etag-next"\\r\\n' > "$headers"; ${status === 304 ? ': > "$dest"' : 'cp response.json "$dest"'}; printf '${status}'; }\n` }
|
|
|
|
describe('agent last-good policy validator cache and typed upstream rejections', () => {
|
|
it('makes a cold-cache request without a validator', () => {
|
|
const d = workspace(); expect(worker(d, 'init').status).toBe(0); writeFileSync(join(d, 'response.json'), JSON.stringify(policy()))
|
|
const r = shell(d, curl(200) + 'curl_policy candidate.json\nparse_policy candidate.json\n')
|
|
expect(r.status, r.stderr).toBe(0); expect(readFileSync(join(d, 'curl-args'), 'utf8')).not.toContain('If-None-Match')
|
|
expect(() => readFileSync(join(d, 'last_policy.json'))).toThrow()
|
|
})
|
|
it('reuses a matching durable body on304 and still repairs drift instead of skipping sync', () => {
|
|
const d = workspace(); cache(d)
|
|
const main = source.slice(source.indexOf('KERNEL_STATE_FILE="$STATE_DIR/kernel_state"'))
|
|
const r = shell(d, curl(304) + `type() { return 0; }\nkernel_digest() { echo repaired; }\ncollect_telemetry() { :; }\nflush_reports() { :; }\nsend_report() { :; }\napply_nft() { echo apply >> applied; KERNEL_METHOD=nft; APPLIED=0; APPLIED_FAMILIES=4,6; }\ncurl_policy candidate.json\nparse_policy candidate.json\n${main}`)
|
|
expect(r.status, r.stderr).toBe(0)
|
|
expect(readFileSync(join(d, 'curl-args'), 'utf8')).toContain('If-None-Match: "etag-first"')
|
|
expect(readFileSync(join(d, 'applied'), 'utf8').trim()).toBe('apply')
|
|
const saved = JSON.parse(readFileSync(join(d, 'last_policy.json'), 'utf8'))
|
|
expect(saved.hash).toBe(policy().hash); expect(saved._response_etag).toBe('"etag-first"'); expect(JSON.parse(saved._response_body)).toEqual(policy())
|
|
})
|
|
it('preserves the durable matching body and validator when a new candidate fails kernel apply', () => {
|
|
const d = workspace(); cache(d); const before = readFileSync(join(d, 'last_policy.json'), 'utf8')
|
|
writeFileSync(join(d, 'response.json'), JSON.stringify(policy('b', 2)))
|
|
const main = source.slice(source.indexOf('KERNEL_STATE_FILE="$STATE_DIR/kernel_state"'))
|
|
const r = shell(d, curl(200) + `type() { return 0; }\nkernel_digest() { echo repaired; }\ncollect_telemetry() { :; }\nflush_reports() { :; }\napply_nft() { return 1; }\ncurl_policy candidate.json\nparse_policy candidate.json\n${main}`)
|
|
expect(r.status).not.toBe(0); expect(readFileSync(join(d, 'last_policy.json'), 'utf8')).toBe(before)
|
|
})
|
|
it('does not accept304 without a durable validated body', () => {
|
|
const d = workspace(); expect(worker(d, 'init').status).toBe(0)
|
|
const r = shell(d, curl(304) + 'if curl_policy candidate.json; then exit 9; fi\n')
|
|
expect(r.status, r.stderr).toBe(0); expect(() => readFileSync(join(d, 'last_policy.json'))).toThrow()
|
|
})
|
|
it.each([
|
|
[422, 'POLICY_BUDGET_EXCEEDED', 1, 'POLICY_BUDGET_EXCEEDED'],
|
|
[422, 'SOME_OTHER_ERROR', 1, 'apply_failed'],
|
|
[403, 'POLICY_BUDGET_EXCEEDED', 2, 'apply_failed'],
|
|
])('handles HTTP%s error %s without false policy acknowledgement', (status, code, expectedRc, expectedCode) => {
|
|
const d = workspace(); cache(d); const before = readFileSync(join(d, 'last_policy.json'), 'utf8')
|
|
writeFileSync(join(d, 'response.json'), JSON.stringify({ error: { code } }))
|
|
const r = shell(d, curl(status as number) + 'rc=0; curl_policy candidate.json || rc=$?; printf "%s %s" "$rc" "$APPLY_ERROR_CODE" > result\n')
|
|
expect(r.status, r.stderr).toBe(0); expect(readFileSync(join(d, 'result'), 'utf8')).toBe(`${expectedRc} ${expectedCode}`)
|
|
expect(readFileSync(join(d, 'last_policy.json'), 'utf8')).toBe(before)
|
|
})
|
|
})
|