/** * Framework-gap conformance for every app.env that ships outside this repo. * * The deploy/ framework gained six keys' worth of generality in #1108 (health * contract, DATA_DIR ownership, unit extras, the source build step, per-app * binary file lists, a bounded stop) plus the ADR §4/§5 app.env keys that #1110 * (ziptask, DIST=binary) and #1111 (subagentix, DIST=source) each shipped. The * framework's OWN suite proved the new code against synaptomind's app.env, which * sets NONE of them — every new key takes its `:=` default there, so a green * `bun test ./deploy/` says the defaults are intact and nothing about whether a * real app can use the feature. * * THIS file is that missing half. It takes each app's SHIPPED app.env, sandboxed * into a temp tree, and drives the real install.sh against it. Nothing is * hand-fed to the framework: the keys under test come from the file the repo * ships, and the only edits are the paths (INSTALL_DIR / DATA_DIR / RUN_DIR / * UNIT_FILE / HOOKS_DIR / EXEC_START), which is what makes it a sandbox and not * a re-specification. * * See ./app-targets for how targets are named (DEPLOY_APP_ENVS) and why a * requested-but-unresolved target fails the run instead of skipping it. * * SANDBOXING, and the two traps this file is built around: * * 1. `load_app_env()` SOURCES app.env, so a plain assignment in it beats the * process environment. Both new app.envs carry `RUN_DIR=""`, and * resolve_target_user() then derives the operator's REAL ~/.ziptask from * `getent passwd` — the #1101 incident, where a fixture overwrote the live * app.env while every assertion stayed green. So the sandbox is written INTO * the app.env copy (sandboxAppEnvText), assertSandboxed() refuses any * override outside the fixture root, and guardRealStateDir() fingerprints * the real state dir for the whole run. * 2. `sudo` has its own secure PATH, so a PATH stub does NOT intercept it. * A "sandboxed" demo once restarted the live service five times that way * (AGENTS.md §8). sudo/systemctl are therefore stubbed for real here, and * nothing privileged runs. */ import { describe, expect, test, afterEach } from 'bun:test' import { spawnSync } from 'node:child_process' import { chmodSync, cpSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { readAppEnvValues, sandboxAppEnvText } from './app-env-targets' import { checkCoverage, guardProductionPaths, resolveTargets } from './app-targets' import { guardRealStateDir, installCleanup, isolatedEnv, mkTempTree } from './tmp-fixtures' installCleanup() guardRealStateDir() const DEPLOY_DIR = import.meta.dir const LIB = join(DEPLOY_DIR, 'lib', 'common.sh') const TARGETS = resolveTargets() // Containment for the TARGETS, not just for this repo's own app: guardRealStateDir() // watches ~/.synaptomind, which says nothing about ~/.ziptask or ~/.subagentix. // See guardProductionPaths for why the check lives at process exit and why the // live tracker's own data directory is excluded from the watch list. guardProductionPaths(TARGETS.resolved) /** POSIX single-quoting for a bash heredoc body. */ function q(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'` } /** * The /health body both new apps actually answer with: a bare `{"ok":true}`, * no `status`, no `version` (ADR §3d). * * Named rather than written inline because it is a JSON document inside a shell * single-quoted string inside a TypeScript single-quoted string — three quoting * layers, and a mis-quoted one produces a parse error at best and a silently * different body at worst. */ const HEALTHY_BODY = '{"ok":true}' /** The same contract with the ok-value flipped: must fail, or the gate is vacuous. */ const UNHEALTHY_BODY = '{"ok":false}' /** * Replace ONE assignment line in an app.env text, appending it when absent. * * The function replacement is not incidental: `line` is test data that may hold * `$` (a glob, a value under test), and `String.replace` would read a `$pattern` * in it. The caller is responsible for the line being safe to SOURCE — see * renderUnit's rawAppEnvLines, which exists only because the fixture's own * quote() refuses a double quote in an override. */ function replaceAssignment(text: string, key: string, line: string): string { const re = new RegExp(`^${key}=`, 'm') return re.test(text) ? text.replace(re, () => line) : `${text}\n${line}\n` } /** * unit_value_defect called DIRECTLY, with the value as an ARGVUMENT. * * `bash -c