Testing¶
The collectors are tested end to end. tests/smoke.sh and tests/smoke.ps1
build a fake disk image with two users, awkward file names, credentials,
excluded directories, an oversized file and symlinks, run the collector
over it in image mode and check the manifest rows, the archive and each
option; tests/catalog-sync.sh checks that the five catalog tables agree
between the two scripts and the analyzer's copy. A change to a collector
runs the sh smoke test under every shell the script must support, the
PowerShell one under PowerShell 7 and Windows PowerShell 5.1, and the drift
check before it is pushed, and CI runs the same matrix.
Run from the collectors/ directory:
tests/smoke.sh # under sh
tests/smoke.sh dash
tests/smoke.sh bash
printf '#!/bin/sh\nexec busybox ash "$@"\n' > /tmp/ash && chmod +x /tmp/ash && tests/smoke.sh /tmp/ash
printf '#!/bin/sh\nexec zsh --emulate sh "$@"\n' > /tmp/zsh-sh && chmod +x /tmp/zsh-sh && tests/smoke.sh /tmp/zsh-sh
tests/smoke.sh posh
shellcheck -s sh -S warning collect-agent-artifacts.sh
tests/catalog-sync.sh # the five tables, --list output and the analyzer copy agree
pwsh -NoProfile -File tests/smoke.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests\smoke.ps1 # on Windows
The linters CI runs on every file (shfmt, shellcheck with .shellcheckrc,
checkbashisms, PSScriptAnalyzer, markdownlint) are listed under "Quality
gates" in AGENTS.md.
Smoke tests¶
tests/smoke.sh [SHELL] runs the collector script under SHELL (default
sh, a name on the PATH or a path), and passes the same shell to the find
-exec workers through COLLECTOR_SH. Every collection run is in image mode
with --max-file-size 1 except three: a live run limited to a user that does
not exist (-u no-such-user-cac --no-docker --no-projects), which checks that
no live/ directory or live row appears; a run with a relative -r and
CDPATH set, which checks that the image in the working directory is
collected; and, when not run as root, a run into a read-only output directory,
which must exit 2 before collecting. A run with -o under a file must exit
2 the same way. It works in a cac-smoke.* directory under TMPDIR
(default /tmp). The directory is removed when every check passes and kept,
with its path printed, when one fails. The exit status is 0 or 1.
tests/smoke.ps1 takes -Collector PATH to test a copy of the script other
than the one beside it, which is how the test runs under Windows PowerShell
5.1 from a WSL checkout. It works under the system temp directory, keeps it
on failure in the same way, and skips the symlink checks, saying so, where
creating symlinks is not permitted. Its symlink-in-archive checks use a
separate small image with a relative, a dangling and a directory symlink
(and, on Windows, a junction) and run it four times: with tar, with the
PowerShell tar writer, with the zip from the start, and, off Windows, with a
tar that fails and the zip after it (the late fall back to zip).
CAC_ARCHIVER is a test hook for the PowerShell collector that sets the
archivers to try, in order, as a comma-separated list of tar, writer
(the PowerShell tar writer) and zip; the default is tar,writer,zip. An
unknown name exits 2, tar is dropped when no tar is on the PATH,
and the archive is a .zip only when zip comes first, in which case no
symlinks are staged. The smoke test uses writer and zip to run each
archiver on every platform, and tar,zip with a failing tar for the late
zip fallback. It also collects an image with Latin-1, Cyrillic and CJK file
names and a symlink with a Cyrillic name: on Windows tar.exe fails on it,
and the test checks the PowerShell tar writer's archive, archiver, the
note and the log line, and reads the archive with its own .NET tar reader
(header checksums, pax records, member names, link targets and hashes),
since tar.exe cannot list those names. With the writer forced, it checks
that the platform tar lists the same members as the default run and
extracts bytes that match the manifest, and with the zip forced that the
entry names use /.
Each smoke test builds a fake disk image with two users, a service account,
awkward filenames, a symlink, credential files, excluded directories, an
oversized file and projects referenced from agent state, then checks the
manifest, hashes and archive contents. The sh test also has a nobody
account to skip. It is POSIX sh too, and its JSON validity and hash
cross-check steps run only when python3 is available. The PowerShell test
uses a Windows profile tree with drive-letter and file:/// project
references and runs under both PowerShell 5.1 and 7. Both tests read
project folders from Antigravity project files with a POSIX or
drive-letter file:/// URL.
Both smoke tests also build root and rootless Docker volumes, one matched
and one unmatched, check --no-docker and the Docker Desktop note, and run
--no-secrets, --full and -u. The sh test adds an unreadable volume and
data root when it is not run as root. Both run --inventory / -Inventory
against the same image and check the key order of both line types, one
known line's files, bytes, first, last and evidence, that an
excluded subtree is not counted (and is with --full), the Docker volume
lines, -u, --no-docker, that nothing is written in the current directory
or under -o, that stdout stays pure JSON Lines with and without -q, and,
on a POSIX host not run as root, that an unreadable home is counted in
users_unreadable.
The sh test also runs a small separate image through the batched stage
worker, which lstats, copies and hashes up to 256 files at a time: names
with a newline, a backslash and a carriage return, an unreadable file
between readable ones (not when run as root), and 300 files, more than one
chunk. It checks the hashes against the archived bytes, the error_copy
row and log line, that a run with CAC_NO_BATCH=1 (the per-file path)
writes the same manifest in the same order apart from atime, and that a
copy of the collector forced onto shasum writes the same manifest.
Table coverage checks¶
Two checkers compare each line of the five catalog tables with what should exist for it elsewhere in the repository. Run them from the repository root; both use the standard library alone:
python3 tools/check_tables_research.py # each line against its agent's research document
python3 tools/check_smoke_coverage.py # each line against both smoke tests
tools/check_tables_research.py reports a line whose glob no code span of
the Collection part of the agent's research document names, a credential
file that document names that no secret glob flags and the Secrets section
does not list, a secret glob an exclusion hides, an exclusion or secret glob
that no home or project entry reaches and no one Docker volume agent's
document names, a Docker volume glob that the document's Per-user storage
subsection about the volume does not name, an exclusion below a Docker
volume's mount directory without its volume-relative twin, two agents' home
entries whose first differing segment differs only by case or whose first
segments differ and can still match one path, a Credentials section saying
a file is
left unflagged instead of naming its secret glob, a timeline row text or
record key under Collection, a catalog agent without a research document or
a row on the agents page, and a row on that page
naming no catalog agent.
tools/check_smoke_coverage.py requires, in both smoke files, a
default-run check asserting secret: true and a --no-secrets check
asserting skipped_secret on a path each SECRET_GLOBS line matches, a
check asserting skipped_excluded for each EXCLUDES line, and for each
CATALOG and PROJECT_CATALOG line a fixture path it matches whose name
the research document gives, and for each agent with a secret case in
the frameworks case list (CASES_FW in smoke.sh, $CasesFrameworks in
smoke.ps1) a --no-secrets check asserting skipped_secret on one of
them. It reads both smoke tests as text: keep
fixture paths and check arguments as literals, variables assigned from
literals, kind|agent|path case lists, or here-document lists read by
while read, and write Row and StatusOf arguments in smoke.ps1 from
the image root (Users/alice/...), never as a suffix.
The disagreements that existed when the checkers were written are
recorded in tools/drift-baseline/, and only a finding missing from it
fails; Drift checkers in analyzer/docs/development.md
describes the finding format and the baseline.
Catalog drift check and CI¶
tests/catalog-sync.sh extracts the five tables from both scripts and fails
if they differ or if analyzer/doubleagent/catalog.txt is not identical to
--list. When pwsh is on the PATH it also compares --list with
-List. The repository's pre-commit hook in .githooks/
(enable it once per clone with git config core.hooksPath .githooks)
refuses a commit that stages either collector or the analyzer copy while the
copy is stale or the tables differ; the linters run in CI only.
CI (.github/workflows/ci.yml) runs the drift test and shellcheck, the sh
smoke test under sh, dash, bash, busybox ash, zsh and posh, the drift test
and the sh smoke test under dash on macOS, where the tools are the BSD
ones, a lint job that also runs the table coverage checks, the
PowerShell smoke test under PowerShell 7 on Linux and Windows PowerShell 5.1
on Windows, and the analyzer tests, then a CI result job that fails when
any of them failed. Every push to main runs all of them; a pull request
runs only the jobs that read a file it changed, as
Validation and testing in AGENTS.md
lists.